// // Copyright 2020 DXOS.org // import { invariant } from '@dxos/invariant'; import { TimeoutError } from './errors.ts'; import { asyncTimeout } from './timeout.ts'; /** * Returns a tuple containing a Promise that will be resolved when the resolver function is called. * @deprecated Use `Trigger` instead. */ export const trigger = (timeout?: number): [() => Promise, (arg: T) => void] => { // eslint-disable-line prefer-arrow-functions/prefer-arrow-functions let callback: (arg: T) => void; const promise = new Promise((resolve, reject) => { if (timeout) { setTimeout(() => reject(new Error(`Timed out after ${timeout.toLocaleString()}ms`)), timeout); } callback = resolve; }); const provider = () => promise; const resolver = (value: T) => callback(value); return [provider, resolver]; }; export type TriggerOptions = { autoReset: boolean; }; export enum TriggerState { WAITING = 'WAITING', RESOLVED = 'RESOLVED', REJECTED = 'REJECTED', } /** * Enables blocked listeners to be awakened with optional timeouts. * * Has two states: * - WAITING: promise is in pending state and will be resolved once `wake()` is called. * - RESOLVED: promise is already resolved, and all calls to `wait()` resolve immediately. * - REJECTED: promise is rejected, and all calls to `wait()` return rejected promise. * * Trigger starts in WAITING state initially. * Use `reset()` to switch resolved trigger back to WAITING state. */ export class Trigger { private _promise!: Promise; private _resolve!: (value: T | PromiseLike) => void; private _reject!: (error: Error) => void; private _state: TriggerState = TriggerState.WAITING; constructor(private _options: TriggerOptions = { autoReset: false }) { this.reset(); } get state() { return this._state; } /** * Wait until wake is called, with optional timeout. */ async wait({ timeout }: { timeout?: number } = {}): Promise { if (timeout) { return asyncTimeout(this._promise, timeout, new TimeoutError(timeout)); } else { return this._promise; } } /** * Wake blocked callers (if any). * NOOP if the trigger is already resolved. */ wake(value: T): this { if (this._state !== TriggerState.WAITING) { return this; } this._state = TriggerState.RESOLVED; this._resolve(value); if (this._options.autoReset) { return this.reset(); } return this; } /** * Reset promise (new waiters will wait). */ reset(): this { this._state = TriggerState.WAITING; this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); this._promise.catch(() => {}); // Prevent unhandled promise rejections. return this; } /** * Throw error to blocked callers (if any). * NOOP if the trigger is already resolved. */ throw(error: Error): this { if (this._state !== TriggerState.WAITING) { return this; } this._state = TriggerState.REJECTED; this._reject(error); if (this._options.autoReset) { return this.reset(); } return this; } } type LatchProps = { count?: number; timeout?: number; }; type LatchResult = [() => Promise, () => number, (err: Error) => void]; /** * Returns a callback and a promise that's resolved when the callback is called n times. * @deprecated Use `Trigger` instead. */ export const latch = ({ count = 1, timeout }: LatchProps = {}): LatchResult => { invariant(count >= 0); let t: ReturnType; let doResolve: (value: number) => void; let doReject: (err: Error) => void; const promise = new Promise((resolve, reject) => { doResolve = (value) => { clearTimeout(t); resolve(value); }; doReject = (err) => { clearTimeout(t); reject(err); }; }); if (count === 0) { setTimeout(() => { doResolve(0); }); } else { if (timeout) { t = setTimeout(() => { doReject(new Error(`Timed out after ${timeout.toLocaleString()}ms`)); }, timeout); } } let i = 0; return [ async () => await promise, () => { if (++i === count) { doResolve(i); } return i; }, (err: Error) => doReject(err), ]; };