Source code
Revision control
Copy as Markdown
Other Tools
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {TimeoutError} from '../common/Errors.js';
/**
* @internal
*/
export interface DeferredOptions {
message: string;
timeout: number;
}
/**
* Creates and returns a deferred object along with the resolve/reject functions.
*
* If the deferred has not been resolved/rejected within the `timeout` period,
* the deferred gets resolves with a timeout error. `timeout` has to be greater than 0 or
* it is ignored.
*
* @internal
*/
export class Deferred<T, V extends Error = Error> {
static create<R, X extends Error = Error>(
opts?: DeferredOptions
): Deferred<R, X> {
return new Deferred<R, X>(opts);
}
static async race<R>(
awaitables: Array<Promise<R> | Deferred<R>>
): Promise<R> {
const deferredWithTimeout = new Set<Deferred<R>>();
try {
const promises = awaitables.map(value => {
if (value instanceof Deferred) {
if (value.#timeoutId) {
deferredWithTimeout.add(value);
}
return value.valueOrThrow();
}
return value;
});
// eslint-disable-next-line no-restricted-syntax
return await Promise.race(promises);
} finally {
for (const deferred of deferredWithTimeout) {
// We need to stop the timeout else
// Node.JS will keep running the event loop till the
// timer executes
deferred.reject(new Error('Timeout cleared'));
}
}
}
#isResolved = false;
#isRejected = false;
#value: T | V | TimeoutError | undefined;
// SAFETY: This is ensured by #taskPromise.
#resolve!: (value: void) => void;
#taskPromise = new Promise<void>(resolve => {
this.#resolve = resolve;
});
#timeoutId: ReturnType<typeof setTimeout> | undefined;
#timeoutError: TimeoutError | undefined;
constructor(opts?: DeferredOptions) {
if (opts && opts.timeout > 0) {
this.#timeoutError = new TimeoutError(opts.message);
this.#timeoutId = setTimeout(() => {
this.reject(this.#timeoutError!);
}, opts.timeout);
}
}
#finish(value: T | V | TimeoutError) {
clearTimeout(this.#timeoutId);
this.#value = value;
this.#resolve();
}
resolve(value: T): void {
if (this.#isRejected || this.#isResolved) {
return;
}
this.#isResolved = true;
this.#finish(value);
}
reject(error: V | TimeoutError): void {
if (this.#isRejected || this.#isResolved) {
return;
}
this.#isRejected = true;
this.#finish(error);
}
resolved(): boolean {
return this.#isResolved;
}
finished(): boolean {
return this.#isResolved || this.#isRejected;
}
value(): T | V | TimeoutError | undefined {
return this.#value;
}
#promise: Promise<T> | undefined;
valueOrThrow(): Promise<T> {
if (!this.#promise) {
this.#promise = (async () => {
await this.#taskPromise;
if (this.#isRejected) {
throw this.#value;
}
return this.#value as T;
})();
}
return this.#promise;
}
}