UNPKG

@v4fire/core

Version:
534 lines (437 loc) 12.7 kB
/*! * V4Fire Core * https://github.com/V4Fire/Core * * Released under the MIT license * https://github.com/V4Fire/Core/blob/master/LICENSE */ import { Value, State, Executor, ResolveHandler, RejectHandler, ConstrRejectHandler, ConstrResolveHandler } from 'core/prelude/structures/sync-promise/interface'; export * from 'core/prelude/structures/sync-promise/interface'; /** * Class is similar to the native promise class but works synchronously */ export default class SyncPromise<T = unknown> implements Promise<T> { /** * Returns a SyncPromise object that is resolved with a given value. * * If the value is a promise, that promise is returned; if the value is a thenable (i.e., has a "then" method), * the returned promise will "follow" that thenable, adopting its eventual state; otherwise, * the returned promise will be fulfilled with the value. * * This function flattens nested layers of promise-like objects * (e.g., a promise that resolves to a promise that resolves to something) into a single layer. * * @param value */ static resolve<T = unknown>(value: Value<T>): SyncPromise<T>; /** * Returns a new resolved SyncPromise object with an undefined value */ static resolve(): SyncPromise<void>; static resolve<T = unknown>(value?: Value<T>): SyncPromise<T> { const Constr = Object.isTruly(this) ? this : SyncPromise; if (value instanceof Constr) { return value; } return new Constr((resolve) => resolve(value)); } /** * Returns a SyncPromise object that is rejected with a given reason * @param [reason] */ static reject<T = never>(reason?: unknown): SyncPromise<T> { const Constr = Object.isTruly(this) ? this : SyncPromise; return new Constr((_, reject) => reject(reason)); } /** * Takes an iterable of promises and returns a single SyncPromise that resolves to an array of the results * of the input promises. This returned promise will resolve when all the input's promises have been resolved or * if the input iterable contains no promises. It rejects immediately upon any of the input promises rejecting or * non-promises throwing an error and will reject with this first rejection message/error. * * @param values */ static all<T extends any[] | []>( values: T ): SyncPromise<{[K in keyof T]: Awaited<T[K]>}>; static all<T extends Iterable<Value>>( values: T ): SyncPromise<Array<T extends Iterable<Value<infer V>> ? V : unknown>>; static all<T extends Iterable<Value>>( values: T ): SyncPromise<Array<T extends Iterable<Value<infer V>> ? V : unknown>> { return new SyncPromise((resolve, reject) => { const promises: SyncPromise[] = []; for (const el of values) { promises.push(SyncPromise.resolve(el)); } if (promises.length === 0) { resolve([]); return; } const results = new Array(promises.length); let done = 0; for (let i = 0; i < promises.length; i++) { const onFulfilled = (val) => { done++; results[i] = val; if (done === promises.length) { resolve(results); } }; promises[i].then(onFulfilled, reject); } }); } /** * Returns a promise that resolves after all the given promises have either been fulfilled or rejected, * with an array of objects describing each promise's outcome. * * It is typically used when you have multiple asynchronous tasks that are not dependent on one another to * complete successfully, or you'd always like to know the result of each promise. * * In comparison, the SyncPromise returned by `SyncPromise.all()` may be more appropriate * if the tasks are dependent on each other / if you'd like to reject upon any of them reject immediately. * * @param values */ static allSettled<T extends any[] | []>( values: T ): SyncPromise<{[K in keyof T]: PromiseSettledResult<Awaited<T[K]>>}>; static allSettled<T extends Iterable<Value>>( values: T ): SyncPromise<Array<T extends Iterable<Value<infer V>> ? PromiseSettledResult<V> : PromiseSettledResult<unknown>>>; static allSettled<T extends Iterable<Value>>( values: T ): SyncPromise<Array<T extends Iterable<Value<infer V>> ? PromiseSettledResult<V> : PromiseSettledResult<unknown>>> { return new SyncPromise((resolve) => { const promises: SyncPromise[] = []; for (const el of values) { promises.push(SyncPromise.resolve(el)); } if (promises.length === 0) { resolve([]); return; } const results = new Array(promises.length); let done = 0; for (let i = 0; i < promises.length; i++) { const onFulfilled = (value) => { done++; results[i] = { status: 'fulfilled', value }; if (done === promises.length) { resolve(results); } }; const onRejected = (reason) => { done++; results[i] = { status: 'rejected', reason }; if (done === promises.length) { resolve(results); } }; promises[i].then(onFulfilled, onRejected); } }); } /** * Returns a promise that fulfills or rejects as soon as one of the promises from the iterable fulfills or rejects, * with the value or reason from that promise * * @param values */ static race<T extends Iterable<Value>>( values: T ): SyncPromise<T extends Iterable<Value<infer V>> ? V : unknown> { return new SyncPromise((resolve, reject) => { const promises: SyncPromise[] = []; for (const el of values) { promises.push(SyncPromise.resolve(el)); } if (promises.length === 0) { resolve(); return; } for (let i = 0; i < promises.length; i++) { promises[i].then(resolve, reject); } }); } /** * Takes an iterable of SyncPromise objects and, as soon as one of the promises in the iterable fulfills, * returns a single promise that resolves with the value from that promise. If no promises in the iterable fulfill * (if all the given promises are rejected), then the returned promise is rejected with an AggregateError, * a new subclass of Error that groups together individual errors. * * @param values */ static any<T extends Iterable<Value>>( values: T ): SyncPromise<T extends Iterable<Value<infer V>> ? V : unknown> { return new SyncPromise((resolve, reject) => { const promises: SyncPromise[] = []; for (const el of values) { promises.push(SyncPromise.resolve(el)); } if (promises.length === 0) { resolve(); return; } const errors: Error[] = []; for (let i = 0; i < promises.length; i++) { promises[i].then(resolve, onReject); } function onReject(err: Error): void { errors.push(err); if (errors.length === promises.length) { reject(new AggregateError(errors, 'No Promise in Promise.any was resolved')); } } }); } /** @override */ readonly [Symbol.toStringTag]: 'Promise'; /** * True if the current promise is pending */ get isPending(): boolean { return this.state === State.pending; } /** * Actual promise state */ protected state: State = State.pending; /** * Resolved promise value */ protected value: unknown; /** * List of handlers to handle the promise fulfilling */ protected fulfillHandlers: ConstrResolveHandler[] = []; /** * List of handlers to handle the promise rejection */ protected rejectHandlers: ConstrRejectHandler[] = []; constructor(executor: Executor) { const clear = () => { this.fulfillHandlers = []; this.rejectHandlers = []; }; const reject = (err) => { if (!this.isPending) { return; } this.value = err; this.state = State.rejected; for (let o = this.rejectHandlers, i = 0; i < o.length; i++) { o[i](err); } setImmediate(() => { if (this.rejectHandlers.length === 0) { void Promise.reject(err); } clear(); }); }; const resolve = (val) => { if (!this.isPending || this.value != null) { return; } this.value = val; if (Object.isPromiseLike(val)) { // eslint-disable-next-line @typescript-eslint/no-use-before-define val.then(forceResolve, reject); return; } this.state = State.fulfilled; for (let o = this.fulfillHandlers, i = 0; i < o.length; i++) { o[i](val); } clear(); }; const forceResolve = (val) => { this.value = undefined; resolve(val); }; this.call(executor, [resolve, reject], reject); } /** * Returns the promise' value if it is fulfilled, otherwise throws an exception */ unwrap(): T { if (this.state !== State.fulfilled) { if (this.isPending) { throw new Error("Can't unwrap a pending promise"); } if (this.rejectHandlers.length === 0) { this.rejectHandlers.push(() => { // Loopback }); } throw this.value; } return <T>this.value; } /** * Attaches handlers for the promise fulfilled and/or rejected states. * The method returns a new promise that will be resolved with a value that returns from the passed handlers. * * @param [onFulfilled] * @param [onRejected] */ then<R>( onFulfilled: Nullable<ResolveHandler<T>>, onRejected: RejectHandler<R> ): SyncPromise<T | R>; then<V>( onFulfilled: ResolveHandler<T, V>, onRejected?: Nullable<RejectHandler<V>> ): SyncPromise<V>; then<V, R>( onFulfilled: ResolveHandler<T, V>, onRejected: RejectHandler<R> ): SyncPromise<V | R>; then( onFulfilled?: Nullable<ResolveHandler<T>>, onRejected?: Nullable<RejectHandler<T>> ): SyncPromise<T>; then( onFulfilled: Nullable<ResolveHandler>, onRejected: Nullable<RejectHandler> ): SyncPromise { return new SyncPromise((resolve, reject) => { const fulfillWrapper = (val) => { this.call(onFulfilled ?? resolve, [val], reject, resolve); }; const rejectWrapper = (err) => { this.call(onRejected ?? reject, [err], reject, resolve); }; this.fulfillHandlers.push(fulfillWrapper); this.rejectHandlers.push(rejectWrapper); if (!this.isPending) { (this.state === State.fulfilled ? fulfillWrapper : rejectWrapper)(this.value); } }); } /** * Attaches a handler for the promise' rejected state. * The method returns a new promise that will be resolved with a value that returns from the passed handler. * * @param [onRejected] */ catch<R>(onRejected: RejectHandler<R>): SyncPromise<R>; catch(onRejected?: Nullable<RejectHandler<T>>): SyncPromise<T>; catch(onRejected?: RejectHandler): SyncPromise { return new SyncPromise((resolve, reject) => { const rejectWrapper = (err) => { this.call(onRejected ?? reject, [err], reject, resolve); }; this.fulfillHandlers.push(resolve); this.rejectHandlers.push(rejectWrapper); if (!this.isPending) { (this.state === State.fulfilled ? resolve : rejectWrapper)(this.value); } }); } /** * Attaches a common callback for the promise fulfilled and rejected states. * The method returns a new promise with the state and value from the current. * A value from the passed callback will be ignored unless it equals a rejected promise or exception. * * @param [cb] */ finally(cb?: Nullable<Function>): SyncPromise<T> { return new SyncPromise((resolve, reject) => { const fulfillWrapper = () => { try { let res = cb?.(); if (Object.isPromiseLike(res)) { res = res.then(() => this.value); } else { res = this.value; } resolve(res); } catch (err) { reject(err); } }; const rejectWrapper = () => { try { let res = cb?.(); if (Object.isPromiseLike(res)) { res = res.then(() => this.value); resolve(res); } else { reject(this.value); } } catch (err) { reject(err); } }; this.fulfillHandlers.push(fulfillWrapper); this.rejectHandlers.push(rejectWrapper); if (!this.isPending) { (this.state === State.fulfilled ? fulfillWrapper : rejectWrapper)(); } }); } /** * Executes a function with the specified parameters * * @param fn * @param args - arguments for the function * @param [onError] - error handler * @param [onValue] - success handler */ protected call<A = unknown, V = unknown>( fn: Nullable<Function>, args: A[] = [], onError?: ConstrRejectHandler, onValue?: AnyOneArgFunction<V> ): void { const reject = onError ?? loopback, resolve = onValue ?? loopback; try { const res = fn?.(...args); if (Object.isPromiseLike(res)) { (<PromiseLike<V>>res).then(resolve, reject); } else { resolve(res); } } catch (err) { reject(err); } function loopback(): void { return undefined; } } }