UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

144 lines (143 loc) 5.81 kB
import { Errors } from "../error/Errors.js"; import { RequiredError } from "../error/RequiredError.js"; import { BLACKHOLE } from "./function.js"; /** Is a value an asynchronous value implementing a `then()` function. */ export function isAsync(value) { return typeof value === "object" && value !== null && typeof value.then === "function"; } /** Is a value a synchronous value. */ export function notAsync(value) { return !isAsync(value); } /** * Throw the value if it's an async (promised) value. * @returns Synchronous (not promised) value. * @throws Promise if value is an asynchronous (promised) value. */ export function throwAsync(value) { if (isAsync(value)) throw value; return value; } /** Assert an unknown value is synchronous (i.e. does not have a `.then()` method). */ export function assertNotAsync(value) { if (isAsync(value)) throw new RequiredError("Must be synchronous", { received: value, caller: assertNotAsync }); } /** Assert an unknown value is asynchronous (i.e. has a `.then()` method). */ export function assertAsync(value) { if (!isAsync(value)) throw new RequiredError("Must be asynchronous", { received: value, caller: assertAsync }); } /** Assert that an unknown value is a `Promise` */ export function assertPromise(value) { if (!(value instanceof Promise)) throw new RequiredError("Must be promise", { received: value, caller: assertPromise }); } /** Run any queued microtasks now. */ export function runMicrotasks() { // Timeouts are part of the main event queue, and events in the main queue are run _after_ all microtasks complete. return new Promise(resolve => setTimeout(resolve)); } export async function awaitValues(...promises) { const values = []; const errors = []; for (const result of await Promise.allSettled(promises)) { if (result.status === "rejected") errors.push(result.reason); else values.push(result.value); } if (errors.length) throw new Errors(errors, "Concurrent promise rejections", { caller: awaitValues }); return values; } /** * Get the rejection reasons of multiple promises (concurrently). * * @param promises Values (usually async, but not necessarily) that we need to wait for. * @returns Array of rejection reasons of all promises (or empty array if no promises threw). */ export async function awaitErrors(...promises) { const errors = []; for (const result of await Promise.allSettled(promises)) if (result.status === "rejected") errors.push(result.reason); return errors; } /** `Promise` designed for extending with `._resolve()` and `._reject()` methods that can be accessed by subclasses. */ export class BasePromise extends Promise { // Make `this.then()` create a `Promise` not a `Deferred` // Done with a getter because some implementations implement this with a getter and we need to override it. static get [Symbol.species]() { return Promise; } /** Resolve this promise with a value. */ _resolve; /** Reject this promise with a reason. */ _reject; constructor() { let _resolve; let _reject; super((x, y) => { _resolve = x; _reject = y; }); // biome-ignore lint/style/noNonNullAssertion: This is set inside the executor callback. this._resolve = _resolve; // biome-ignore lint/style/noNonNullAssertion: This is set inside the executor callback. this._reject = _reject; } } /** * Create a deferred to access the `resolve()` and `reject()` functions of a promise. * - See https://github.com/tc39/proposal-promise-with-resolvers/ */ export function createDeferred() { let resolve; let reject; return { promise: new Promise((x, y) => { resolve = x; reject = y; }), // biome-ignore lint/style/noNonNullAssertion: This is set inside the executor callback. resolve: resolve, // biome-ignore lint/style/noNonNullAssertion: This is set inside the executor callback. reject: reject, }; } /** Get a promise that automatically resolves after a delay. */ export function getDelay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get a promise that rejects with the signal's reason when an `AbortSignal` fires. * - Rejects immediately if the signal is already aborted. * - Use with `awaitRace()` to cancel a concurrent operation when a signal fires. * * @example await awaitRace(getDelay(300), awaitAbort(signal)); */ export function awaitAbort(signal) { const promise = new Promise((_, reject) => { if (signal.aborted) reject(signal.reason); else signal.addEventListener("abort", () => reject(signal.reason), { once: true }); }); promise.catch(BLACKHOLE); return promise; } /** * Race promises like `Promise.race()` but silently swallow rejections from the losers. * - Returns a promise that settles with the first input to settle, exactly like `Promise.race()`. * - The losing inputs keep running (Promises cannot be cancelled), but their eventual rejection — if any — is silently absorbed instead of bubbling up as an unhandled rejection. * - Built for cancellation/timeout patterns, where the loser's eventual fate is genuinely uninteresting once another arm has settled. Do not use when both arms might surface meaningful errors that the caller should see. * * @example await awaitRace(getDelay(300), awaitAbort(signal)); // delay or abort, no leaked ABORT rejection if delay wins */ export function awaitRace(...promises) { for (const promise of promises) promise.catch(BLACKHOLE); return Promise.race(promises); }