evnty
Version:
Async-first, reactive event handling library for complex event flows in browser and Node.js
167 lines (141 loc) • 4.82 kB
text/typescript
import { Fn, MaybePromise } from './types.js';
import { isThenable, noop } from './utils.js';
const ERR_BRAND = Symbol.for('evnty.ResultError');
/**
* @internal
*/
export interface ResultError<E = unknown> {
readonly error: E;
readonly [ERR_BRAND]: true;
}
function ResultError<E>(this: ResultError<E>, error: E): void {
(this as { error: E }).error = error;
}
(ResultError.prototype as { [ERR_BRAND]: true })[ERR_BRAND] = true;
/**
* @internal
*/
export function err<E>(error: E): ResultError<E> {
return new (ResultError as unknown as new (error: E) => ResultError<E>)(error);
}
/**
* @internal
*/
export function isErr(result: unknown): result is ResultError<unknown> {
return typeof result === 'object' && result !== null && (result as Record<symbol, boolean>)[ERR_BRAND] === true;
}
/**
* @internal
*/
export function isOk(result: unknown): boolean {
return typeof result !== 'object' || result === null || !(result as Record<symbol, boolean>)[ERR_BRAND];
}
/**
* @internal
*/
export type DispatchResultItem<T> = MaybePromise<T> | ResultError;
/**
* @internal
*/
export function unwrap<T>(results: DispatchResultItem<T>[]): MaybePromise<T>[] {
const len = results.length;
const unwrapped = new Array<MaybePromise<T>>(len);
for (let i = 0; i < len; i++) {
const r = results[i];
unwrapped[i] = isErr(r) ? Promise.reject(r.error) : r;
}
return unwrapped;
}
async function resolveMaybePromises<T>(items: MaybePromise<T>[], asyncIndices: number[]): Promise<T[]> {
const pending = new Array<PromiseLike<T>>(asyncIndices.length);
for (let j = 0; j < asyncIndices.length; j++) {
pending[j] = items[asyncIndices[j]] as PromiseLike<T>;
}
const resolved = await Promise.all(pending);
for (let j = 0; j < asyncIndices.length; j++) {
items[asyncIndices[j]] = resolved[j];
}
return items as T[];
}
function resolveAll<T>(results: DispatchResultItem<T>[]): T[] | Promise<T[]> {
const len = results.length;
if (len === 0) return results as T[];
let firstError: unknown;
let hasError = false;
let asyncIndices: number[] | null = null;
for (let i = 0; i < len; i++) {
const r = results[i];
if (isErr(r)) {
if (!hasError) {
hasError = true;
firstError = r.error;
}
} else if (isThenable(r)) {
(asyncIndices ??= []).push(i);
}
}
if (hasError) {
if (asyncIndices !== null) {
for (let j = 0; j < asyncIndices.length; j++) {
(results[asyncIndices[j]] as PromiseLike<T>).then(noop, noop);
}
}
return Promise.reject(firstError);
}
if (asyncIndices === null) return results as T[];
return resolveMaybePromises(results as MaybePromise<T>[], asyncIndices);
}
function settleAll<T>(results: DispatchResultItem<T>[]): PromiseSettledResult<T>[] | Promise<PromiseSettledResult<T>[]> {
const len = results.length;
if (len === 0) return [] as PromiseSettledResult<T>[];
let asyncIndices: number[] | null = null;
const settled = new Array<MaybePromise<PromiseSettledResult<T>>>(len);
for (let i = 0; i < len; i++) {
const r = results[i];
if (isErr(r)) {
settled[i] = { status: 'rejected', reason: r.error };
} else if (isThenable(r)) {
(asyncIndices ??= []).push(i);
settled[i] = r.then(
(value): PromiseFulfilledResult<T> => ({ status: 'fulfilled', value }),
(reason: unknown): PromiseRejectedResult => ({ status: 'rejected', reason }),
);
} else {
settled[i] = { status: 'fulfilled', value: r };
}
}
if (asyncIndices === null) return settled as PromiseSettledResult<T>[];
return resolveMaybePromises(settled, asyncIndices);
}
/**
* Wraps an array of values or promises (typically listener results) and provides batch resolution.
*
* @template T
*/
export class DispatchResult<T> implements PromiseLike<T[]> {
#results: DispatchResultItem<T>[];
readonly [Symbol.toStringTag] = 'DispatchResult';
constructor(results: DispatchResultItem<T>[]) {
this.#results = results;
}
then<TResult1 = T, TResult2 = never>(
onfulfilled?: Fn<[T[]], MaybePromise<TResult1>> | null,
onrejected?: Fn<[any], MaybePromise<TResult2>> | null,
): PromiseLike<TResult1 | TResult2> {
const resolved = this.all();
if (resolved instanceof Promise) return resolved.then(onfulfilled, onrejected);
return Promise.resolve(resolved).then(onfulfilled, onrejected);
}
/**
* Resolves all listener results, rejecting if any promise rejects or any ResultError exists.
*/
all(): T[] | Promise<T[]> {
return resolveAll(this.#results);
}
/**
* Waits for all listener results to settle, regardless of fulfillment or rejection.
*/
settled(): PromiseSettledResult<T>[] | Promise<PromiseSettledResult<T>[]> {
return settleAll(this.#results);
}
}