import { CanceledError, TimeoutError } from '@idlebox/errors'; import type { IDisposable } from '../lifecycle/dispose/disposable.js'; import { scheduler } from '../schedule/scheduler.js'; export type ValueCallback = (value: T | Promise) => void; export type ProgressCallback = (value: T) => void; export interface IProgressHolder { progress(fn: ProgressCallback): Promise & IProgressHolder; } /** * a promise can resolve or reject later * @public */ export class DeferredPromise { readonly promise: Promise & IProgressHolder; #completeCallback: ValueCallback; #errorCallback: (err: any) => void; #success?: boolean; #progressList: ProgressCallback[] = []; constructor() { const { promise, resolve, reject } = Promise.withResolvers(); this.#completeCallback = resolve; this.#errorCallback = reject; this.promise = Object.assign(promise, { progress: (fn: ProgressCallback) => { this.progress(fn); return this.p; }, }); } get p() { return this.promise; } /** * notify progress to callbacks * @param progress argument * @returns */ notify(progress: PT): this { if (this.#success !== undefined) throw new Error('no more event after settled'); for (const cb of this.#progressList) { scheduler(cb.bind(undefined, progress)); } return this; } /** * register a progress callback * @param fn progress callback function, will be called when notify is called */ protected progress(fn: ProgressCallback): IDisposable { if (this.#success !== undefined) throw new Error('no more listener after settled'); this.#progressList.push(fn); const dispose = () => { const index = this.#progressList.indexOf(fn); if (index >= 0) { this.#progressList.splice(index, 1); } }; return { dispose, }; } /** * whether the promise is still working (not completed) */ get working(): boolean { return this.#success === undefined; } /** * @deprecated use settled */ get completed() { return this.settled; } /** * whether the promise is settled (resolved or rejected) */ get settled(): boolean { return this.#success !== undefined; } get resolved(): boolean { return this.#success === true; } get rejected(): boolean { return this.#success === false; } /** * resolve the promise */ complete(value: T) { if (this.settled) return; this.#success = true; this.#after_settled(); this.#completeCallback(value); } /** * reject the promise */ error(err: any) { if (this.settled) return; this.#success = false; this.#after_settled(); this.#errorCallback(err); } /** * reject the deferred with {CancelError} */ cancel() { this.error(new CanceledError()); } #after_settled() { this.#progressList.length = 0; this.#cancel_timeout(); } get callback() { if (this.#success !== undefined) throw new Error('can not generate callback after settled'); return (error?: null | undefined | Error, data?: T) => { if (error) { this.error(error); } else { this.complete(data as any); } }; } private timer?: ITimeoutType; #cancel_timeout() { if (this.timer) { clearTimeout(this.timer); this.timer = undefined; } } timeout(ms: number) { if (this.settled) throw new Error('no more timeout after settled'); this.timer = setTimeout(() => { this.error(new TimeoutError(ms, 'promise not settled')); }, ms); return { dispose: () => this.#cancel_timeout(), }; } /** * Convert promise into deferred * returns a DeferredPromise, resolve when prev resolve, reject when prev reject */ static wrap(prev: Promise) { const p = new DeferredPromise(); prev.then( (d) => { p.complete(d); }, (e) => { p.error(e); }, ); return p; } }