UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

123 lines (122 loc) 4.34 kB
import { Errors } from "../error/Errors.js"; import { awaitErrors, isAsync } from "./async.js"; import { isFunction } from "./function.js"; import { isNullish, notNullish } from "./null.js"; import { isObject } from "./object.js"; /** * Temporary polyfill for `Symbol.dipose` value. * @todo Remove this once browsers support `Symbol.dispose` */ Symbol.dispose ??= Symbol("Symbol.dispose"); Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose"); /** * Safely dispose one or more synchronous `Disposable` values. * - If one disposal fails, the rest continue. * * @param values Zero or more disposables or callbacks. * - Callbacks are allowed because in real usage `[Symbol.asyncDispose]` may have other things to clean up too and it's neater to throw a single aggregate error. * - `Nullish` values are skipped (for convenience). * * @throws {Errors} Error that aggregates all the disposal errors. */ export function dispose(...values) { const errors = []; for (const value of values) { if (isNullish(value)) continue; try { if (Symbol.dispose in value) value[Symbol.dispose](); else if (isFunction(value)) value(); } catch (thrown) { errors.push(thrown); } } if (errors.length) throw new Errors(errors, "Disposal failed", { caller: dispose }); } /** * Safely dispose one or more `AsyncDisposable` or `Disposable` values in parallel — all are disposed even if some throw, errors are rethrown at the end. * * @param values Zero or more (possibly async) disposables, promises, or callbacks. * - Note that spec says `[Symbol.dispose]` is called on an object if `[Symbol.asyncDispose]` is not found. * - `Promises` and `Callback` are allowed because in real usage `[Symbol.asyncDispose]` may have other things to clean up too and it's neater to throw a single aggregate error. * - `Nullish` values are skipped (for convenience). * * @throws {Errors} Error that aggregates all the disposal errors. * * @todo Potentially rewrite this to use `AsyncDisposableStack` internally. */ export async function awaitDispose(...values) { const errors = await awaitErrors(...values.filter(notNullish).map(_disposeAsync)); if (errors.length) throw new Errors(errors, "Async disposal failed", { caller: awaitDispose }); } async function _disposeAsync(value) { if (Symbol.asyncDispose in value) await value[Symbol.asyncDispose](); else if (Symbol.dispose in value) value[Symbol.dispose](); else if (isAsync(value)) await value; else if (isFunction(value)) value(); } /** Is an unknown value a disposable object? */ export function isDisposable(v) { return isObject(v) && typeof v[Symbol.dispose] === "function"; } /** Is an unknown value an async disposable object? */ export function isAsyncDisposable(v) { return isObject(v) && typeof v[Symbol.asyncDispose] === "function"; } /** * Version of `Map` that has disposable values. * - Old values are disposed when they're set to a new value. * - Values are disposed when they're deleted from the map. * - Values are disposed when all items are cleared. * - All items are cleared and their values are disposed when this map itself is disposed. */ export class DisposableMap extends Map { set(key, value) { const previous = this.get(key); if (previous && previous !== value) dispose(previous); return super.set(key, value); } delete(key) { const value = this.get(key); if (value) dispose(value); return super.delete(key); } clear() { dispose(...this.values()); super.clear(); } [Symbol.dispose]() { this.clear(); } } /** * Version of `Set` that has disposable items. * - Values are disposed when they're deleted from the map. * - Values are disposed when all items are cleared. * - All items are cleared (and disposed) when this map itself is disposed. */ export class DisposableSet extends Set { delete(item) { if (this.has(item)) dispose(item); return super.delete(item); } clear() { dispose(...this); super.clear(); } [Symbol.dispose]() { this.clear(); } }