Skip to content

Commit

Permalink
refactor: make timeout a thenable
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Nov 8, 2024
1 parent 19ecf07 commit 40a24ee
Showing 1 changed file with 18 additions and 20 deletions.
38 changes: 18 additions & 20 deletions src/timeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { clearTimeout, setTimeout } from 'timers';

import { MongoInvalidArgumentError, MongoOperationTimeoutError, MongoRuntimeError } from './error';
import { type ClientSession } from './sessions';
import { csotMin, noop } from './utils';
import { csotMin, promiseWithResolvers } from './utils';

/** @internal */
export class TimeoutError extends Error {
Expand All @@ -11,9 +11,9 @@ export class TimeoutError extends Error {
return 'TimeoutError';
}

constructor(message: string, options: { cause?: Error; duration: number }) {
constructor(message: string, options?: { cause?: Error; duration?: number }) {
super(message, options);
this.duration = options.duration;
this.duration = options?.duration ?? 0;
}

static is(error: unknown): error is TimeoutError {
Expand All @@ -23,22 +23,21 @@ export class TimeoutError extends Error {
}
}

type Executor = ConstructorParameters<typeof Promise<never>>[0];
type Reject = Parameters<ConstructorParameters<typeof Promise<never>>[0]>[1];
/**
* @internal
* This class is an abstraction over timeouts
* The Timeout class can only be in the pending or rejected states. It is guaranteed not to resolve
* if interacted with exclusively through its public API
* */
export class Timeout extends Promise<never> {
export class Timeout implements PromiseLike<never> {
private id?: NodeJS.Timeout;

public readonly start: number;
public ended: number | null = null;
public duration: number;
private timedOut = false;
public cleared = false;
private promise: Promise<never>;

get remainingTime(): number {
if (this.timedOut) return 0;
Expand All @@ -51,10 +50,7 @@ export class Timeout extends Promise<never> {
}

/** Create a new timeout that expires in `duration` ms */
private constructor(
executor: Executor = () => null,
options?: { duration: number; unref?: true; rejection?: Error }
) {
private constructor(options?: { duration: number; unref?: true; rejection?: Error }) {
const duration = options?.duration ?? 0;
const unref = !!options?.unref;
const rejection = options?.rejection;
Expand All @@ -63,13 +59,8 @@ export class Timeout extends Promise<never> {
throw new MongoInvalidArgumentError('Cannot create a Timeout with a negative duration');
}

let reject!: Reject;
super((_, promiseReject) => {
reject = promiseReject;

executor(noop, promiseReject);
});

const { promise, reject } = promiseWithResolvers<never>();
this.promise = promise;
this.duration = duration;
this.start = Math.trunc(performance.now());

Expand All @@ -90,6 +81,13 @@ export class Timeout extends Promise<never> {
}
}

then<_TResult1 = never, TResult2 = never>(
_?: undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): PromiseLike<never> {
return this.promise.then<never, never>(undefined, onrejected as any);
}

/**
* Clears the underlying timeout. This method is idempotent
*/
Expand All @@ -105,11 +103,11 @@ export class Timeout extends Promise<never> {
}

public static expires(duration: number, unref?: true): Timeout {
return new Timeout(undefined, { duration, unref });
return new Timeout({ duration, unref });
}

static override reject(rejection?: Error): Timeout {
return new Timeout(undefined, { duration: 0, unref: true, rejection });
static reject(rejection?: Error): Timeout {
return new Timeout({ duration: 0, unref: true, rejection });
}
}

Expand Down

0 comments on commit 40a24ee

Please sign in to comment.