shelving
Version:
Toolkit for using data in JavaScript.
144 lines (143 loc) • 5.81 kB
JavaScript
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);
}