Deferred.ts (3159B)
1 /** 2 * @license 3 * Copyright 2024 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 import {TimeoutError} from '../common/Errors.js'; 7 8 /** 9 * @internal 10 */ 11 export interface DeferredOptions { 12 message: string; 13 timeout: number; 14 } 15 16 /** 17 * Creates and returns a deferred object along with the resolve/reject functions. 18 * 19 * If the deferred has not been resolved/rejected within the `timeout` period, 20 * the deferred gets resolves with a timeout error. `timeout` has to be greater than 0 or 21 * it is ignored. 22 * 23 * @internal 24 */ 25 export class Deferred<T, V extends Error = Error> { 26 static create<R, X extends Error = Error>( 27 opts?: DeferredOptions, 28 ): Deferred<R, X> { 29 return new Deferred<R, X>(opts); 30 } 31 32 static async race<R>( 33 awaitables: Array<Promise<R> | Deferred<R>>, 34 ): Promise<R> { 35 const deferredWithTimeout = new Set<Deferred<R>>(); 36 try { 37 const promises = awaitables.map(value => { 38 if (value instanceof Deferred) { 39 if (value.#timeoutId) { 40 deferredWithTimeout.add(value); 41 } 42 43 return value.valueOrThrow(); 44 } 45 46 return value; 47 }); 48 // eslint-disable-next-line no-restricted-syntax 49 return await Promise.race(promises); 50 } finally { 51 for (const deferred of deferredWithTimeout) { 52 // We need to stop the timeout else 53 // Node.JS will keep running the event loop till the 54 // timer executes 55 deferred.reject(new Error('Timeout cleared')); 56 } 57 } 58 } 59 60 #isResolved = false; 61 #isRejected = false; 62 #value: T | V | TimeoutError | undefined; 63 // SAFETY: This is ensured by #taskPromise. 64 #resolve!: (value: void) => void; 65 // TODO: Switch to Promise.withResolvers with Node 22 66 #taskPromise = new Promise<void>(resolve => { 67 this.#resolve = resolve; 68 }); 69 #timeoutId: ReturnType<typeof setTimeout> | undefined; 70 #timeoutError: TimeoutError | undefined; 71 72 constructor(opts?: DeferredOptions) { 73 if (opts && opts.timeout > 0) { 74 this.#timeoutError = new TimeoutError(opts.message); 75 this.#timeoutId = setTimeout(() => { 76 this.reject(this.#timeoutError!); 77 }, opts.timeout); 78 } 79 } 80 81 #finish(value: T | V | TimeoutError) { 82 clearTimeout(this.#timeoutId); 83 this.#value = value; 84 this.#resolve(); 85 } 86 87 resolve(value: T): void { 88 if (this.#isRejected || this.#isResolved) { 89 return; 90 } 91 this.#isResolved = true; 92 this.#finish(value); 93 } 94 95 reject(error: V | TimeoutError): void { 96 if (this.#isRejected || this.#isResolved) { 97 return; 98 } 99 this.#isRejected = true; 100 this.#finish(error); 101 } 102 103 resolved(): boolean { 104 return this.#isResolved; 105 } 106 107 finished(): boolean { 108 return this.#isResolved || this.#isRejected; 109 } 110 111 value(): T | V | TimeoutError | undefined { 112 return this.#value; 113 } 114 115 #promise: Promise<T> | undefined; 116 valueOrThrow(): Promise<T> { 117 if (!this.#promise) { 118 this.#promise = (async () => { 119 await this.#taskPromise; 120 if (this.#isRejected) { 121 throw this.#value; 122 } 123 return this.#value as T; 124 })(); 125 } 126 return this.#promise; 127 } 128 }