import asyncFlatMap, { Transform, AsyncMemberNP, PromiseArray, } from '../functions/asyncFlatMap'; import identity from '../functions/identity'; import noop from '../functions/noop'; import once from '../functions/once'; export interface FutureOptions { /** Should the executor only be called when this Future is awaited? */ lazy?: boolean; /** how often .poll() is called */ interval?: number; /** @returns a value which resolves this Future, or undefined */ poll?: () => undefined | PromiseLike; timeout?: number; } export const TIMEOUT = Symbol('FUTURE_TIMEOUT'); export type Executor = ( resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void ) => any; export const VoidExecutor: Executor = (resolve) => resolve(); export type ErrorHandler = (_error: unknown) => any; export type FutureCallback = (_value: Awaited) => any; export type Then = (_cb: FutureCallback, _err: ErrorHandler) => any; export type Thennable = { then: Then }; export class Future implements Promise { value: Awaited | undefined; reason: any; resolved = false; rejected = false; callbacks = new Set<(self: this) => any>(); then( onfulfilled?: | ((value: Awaited) => TResult1 | PromiseLike) | null | undefined, onrejected?: | ((reason: any) => TResult2 | PromiseLike) | null | undefined ): Future { if (this.resolved || this.rejected) { setTimeout(() => this.flush()); } return new Future((resolve, reject) => { this.callbacks.add((self) => { try { if (self.rejected) { if (onrejected) { resolve(onrejected(self.reason)); } else { reject(self.reason); } } else if (onfulfilled) { resolve(onfulfilled(self.value!)); } else { resolve(self.value as TResult1); } } catch (error) { reject(error); } }); }); } catch( onrejected?: | ((reason: any) => TResult | PromiseLike) | null | undefined ): Future | TResult> { return this.then(identity, onrejected); } finally( onfinally?: ((value?: T) => void | Promise) | null | undefined ): Future { return this.then(onfinally, onfinally).then(() => this); } await( executor_or_promise: Executor | PromiseLike | null | undefined ): Future { const awaiter = new Future( executor_or_promise || VoidExecutor ); return awaiter.then(() => this); } async asyncFlatMap( transform: Transform>> ): Promise>[]> { const value = await this; return await asyncFlatMap( Symbol.iterator in value ? (value as unknown as T[keyof T & number][]) : [value as unknown as T[keyof T & number]], transform ); } [Symbol.toStringTag] = 'Future'; constructor( executor_or_promise: Executor | PromiseLike, options: FutureOptions = {} ) { if (options.lazy) { return new LazyFuture(executor_or_promise, options); } if (typeof executor_or_promise === 'function') { this.init_with_executor(executor_or_promise); } else { this.init_with_promise(executor_or_promise); } if (options.timeout) { const timeout = setTimeout( () => this.reject(TIMEOUT), options.timeout ); this.finally(() => { clearTimeout(timeout); }); } if (options.poll) { this._poll = options.poll; } if (options.interval) { this._interval = options.interval; } if (options.poll && options.interval) { this.schedule_poll(); } } protected async init_with_executor(executor: Executor) { try { await executor( (value) => this.resolve(value), (reason) => this.reject(reason) ); } catch (error) { this.reject(error); } } protected async init_with_promise(promise: PromiseLike) { try { this.resolve(await promise); } catch (reason) { this.reject(reason); } } flush() { for (const callback of this.callbacks) { this.callbacks.delete(callback); callback(this); } } protected async resolve(value: T | PromiseLike) { try { const final = await value; this.value = final; this.resolved = true; this.flush(); } catch (reason) { this.reject(reason); } } protected reject(reason: any) { if (!this.rejected) { this.reason = reason; this.rejected = true; } this.flush(); } static resolve(value: T | PromiseLike) { return new Future((resolve) => resolve(value)); } static reject(reason: any) { return new Future((_, reject) => reject(reason)); } static all(...items: Array>): Future { return Future.resolve( asyncFlatMap, T>(items, identity) ); } static race(items: Array>): Future { return new Future((resolve) => { const futures = items.map((item) => Future.resolve(item)); const resolver = (item: T) => { for (const future of futures) { future.callbacks.clear(); } resolve(item); }; futures.map((item) => item.then(resolver)); }); } /** calls a callback with an awaited value */ static callback( value: T, callback?: FutureCallback, errorHandler?: ErrorHandler ): void; /** calls a callback after awaiting a Future */ static callback( value: Future, callback?: FutureCallback, errorHandler?: ErrorHandler ): void; /** calls a callback after awaiting a Promise */ static callback( value: Promise, callback?: FutureCallback, errorHandler?: ErrorHandler ): void; /** calls a callback after awaiting a Thennable object */ static callback( value: Thennable, callback: FutureCallback = noop, errorHandler: ErrorHandler = noop ): void { try { if ( value && (typeof value === 'object' || typeof value === 'function') && 'then' in value && typeof value.then === 'function' ) { Future.callback( value.then((value) => { Future.callback(value, callback, errorHandler); }, errorHandler) ); } else if (callback !== noop) { Future.callback( callback(value as Awaited), noop, errorHandler ); } } catch (error) { errorHandler(error); } } /** * Transforms a callback consumer call into a `Future`. * # Usage * ```js * function myFunction(callback) { * if (condition) { * callback(null, "Success") * } else { * callback("Error") * } * } * * const future = Future.fromCallback(myFunction) * ``` * Many [Node APIs](https://docs.nodejs.org/) and [old npm packages](https://npmjs.com/package/imap) behave like this. */ static fromCallback( callback: (callback: (err: E, val: T) => void) => void ): Future; /** * Transforms a callback consumer call into a `Future`. * # Usage * ```js * const myObject = { * condition: sky.color === blue, * myMethod(callback) { * if (this.condition) { * callback(null, "Success") * } else { * callback("Error") * } * } * } * * const future = Future.fromCallback(myObject, myObject.myMethod) * ``` * Many [Node APIs](https://docs.nodejs.org/) and [old npm packages](https://npmjs.com/package/imap) behave like this. */ static fromCallback( thisArg: object, callback: (callback: (err: E, val: T) => void) => void ): Future; static fromCallback( thisArg: object | ((callback: (err: E, val: T) => void) => void), callback?: (callback: (err: E, val: T) => void) => void ): Future { return new Future((resolve, reject) => { const resolver = (err: E, val: T) => { err ? reject(err) : resolve(val); }; if (typeof callback === 'function') { callback.call(thisArg, resolver); } else if (typeof thisArg === 'function') { thisArg(resolver); } else { throw new TypeError( `Future.fromCallback(${thisArg}) is not valid.` ); } }); } _interval?: number; _poll?: FutureOptions['poll']; _poll_timeout?: ReturnType; /** * calls this Future's poll callback * @returns a Future that resolves to this Future, or undefined */ poll(): Future { return new Future(async (resolve) => { try { clearTimeout(this._poll_timeout); if (this.resolved || this.rejected) { return resolve(this); } const value = await this._poll?.(); if (value === undefined) { this.schedule_poll(); return resolve(undefined); } else { this.resolve(value); } } catch (reason) { this.reject(reason); } return resolve(this); }); } schedule_poll() { this._poll_timeout = setTimeout(() => this.poll(), this._interval); } } export class LazyFuture extends Future { protected init; callbacks = new Set<(self: Future) => any>(); constructor( executor_or_promise: Executor | PromiseLike, options: FutureOptions = {} ) { super(noop); this.init = once(() => { const future = new Future(executor_or_promise, { ...options, lazy: false, }); future.then( (value) => this.resolve(value), (error) => this.reject(error) ); return future; }); } then( onfulfilled?: | ((value: Awaited) => TResult1 | PromiseLike) | null | undefined, onrejected?: | ((reason: any) => TResult2 | PromiseLike) | null | undefined ): Future { setTimeout(this.init); return super.then(onfulfilled, onrejected); } /** * Transforms a callback consumer call into a `LazyFuture`. * # Usage * ```js * function myFunction(callback) { * if (condition) { * callback(null, "Success") * } else { * callback("Error") * } * } * * const future = LazyFuture.fromCallback(myFunction) * ``` * Many [Node APIs](https://docs.nodejs.org/) and [old npm packages](https://npmjs.com/package/imap) behave like this. */ static fromCallback( callback: (callback: (err: E, val: T) => void) => void ): LazyFuture; /** * Transforms a callback consumer call into a `LazyFuture`. * # Usage * ```js * const myObject = { * condition: sky.color === blue, * myMethod(callback) { * if (this.condition) { * callback(null, "Success") * } else { * callback("Error") * } * } * } * * const future = LazyFuture.fromCallback(myObject, myObject.myMethod) * ``` * Many [Node APIs](https://docs.nodejs.org/) and [old npm packages](https://npmjs.com/package/imap) behave like this. */ static fromCallback( thisArg: object, callback: (callback: (err: E, val: T) => void) => void ): LazyFuture; static fromCallback( thisArg: object | ((callback: (err: E, val: T) => void) => void), callback?: (callback: (err: E, val: T) => void) => void ): LazyFuture { return new LazyFuture((resolve, reject) => { const resolver = (err: E, val: T) => { err ? reject(err) : resolve(val); }; if (typeof callback === 'function') { callback.call(thisArg, resolver); } else if (typeof thisArg === 'function') { thisArg(resolver); } else { throw new TypeError( `LazyFuture.fromCallback(${thisArg}) is not valid.` ); } }); } } export default Future; Object.defineProperties(Future, { default: { get: () => Future }, Future: { get: () => Future }, LazyFuture: { get: () => LazyFuture }, });