@v4fire/core
Version:
V4Fire core library
534 lines (437 loc) • 12.7 kB
text/typescript
/*!
* 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;
}
}
}