UNPKG

lifecycle-utils

Version:

A set of general utilities for the lifecycle of a JS/TS project/library

174 lines 6.36 kB
import { DisposedError } from "./DisposedError.js"; /** * `AsyncDisposeAggregator` is a utility class that allows you to add multiple items and then dispose them all at once. * The items are disposed one by one in the order they were added. * When the `parallel` option is enabled, then all the items are disposed in parallel, * triggered by the order in which they were added. * You can add a function to call, an object with a `dispose` method, an object with a `Symbol.dispose` method, * an object with a `Symbol.asyncDispose` method, or a Promise that resolves to one of the previous types. * To dispose all the items, call `dispose` or use the `Symbol.asyncDispose` symbol. * The difference between `AsyncDisposeAggregator` and `DisposeAggregator` is that `AsyncDisposeAggregator` can dispose async targets. * * For example, * ```typescript * import {AsyncDisposeAggregator, EventRelay} from "lifecycle-utils"; * * const disposeAggregator = new AsyncDisposeAggregator(); * * const eventRelay = new EventRelay<string>(); * disposeAggregator.add(eventRelay); * * disposeAggregator.add(async () => { * await new Promise(resolve => setTimeout(resolve, 0)); * // do some async work * }); * * disposeAggregator.dispose(); * ``` */ export class AsyncDisposeAggregator { /** @internal */ _targets = []; /** @internal */ _disposed = false; /** @internal */ _parallel; constructor({ parallel } = {}) { this._parallel = parallel ?? false; this.add = this.add.bind(this); this.dispose = this.dispose.bind(this); this[Symbol.asyncDispose] = this[Symbol.asyncDispose].bind(this); this._disposeTarget = this._disposeTarget.bind(this); } /** * Adds a target to be disposed. * You can wrap the target with a `WeakRef` to prevent this class from holding a strong reference to the target. */ add(target) { this._ensureNotDisposed(); this._targets.push(target); return this; } /** * Disposes all the targets that have been added and clears the list of targets. * * After all the targets have been disposed, if any throws an error or rejects, * then the error from the first such target will be thrown. */ dispose() { if (this._disposed === true) return Promise.resolve(); else if (this._disposed !== false) return this._disposed; const [tempPromise, acceptPromise] = promiseWithResolver(); tempPromise.catch(doNothing); this._disposed = tempPromise; const disposedPromise = this._dispose(); acceptPromise(disposedPromise); if (this._disposed === tempPromise) this._disposed = disposedPromise; return disposedPromise; } [Symbol.asyncDispose]() { return this.dispose(); } get targetCount() { return this._targets.length; } /** @internal */ async _dispose() { if (this._parallel) { let firstError; let errorIndex; const promises = []; const targets = this._targets; this._targets = []; for (const target of targets) { try { const res = this._disposeTarget(target); if (res instanceof Promise) promises.push(res); } catch (err) { if (errorIndex == null) { firstError = err; errorIndex = promises.length; } } } targets.length = 0; const results = await Promise.allSettled(promises); try { for (let i = 0; i < (errorIndex ?? results.length); i++) { const result = results[i]; if (result?.status === "rejected") throw result.reason; } if (errorIndex != null) throw firstError; } finally { this._disposed = true; } } else { let firstError; let hasError = false; const targets = this._targets; this._targets = []; for (let i = 0; i < targets.length; i++) { const target = targets[i]; targets[i] = null; try { const res = this._disposeTarget(target); if (res instanceof Promise) await res; } catch (err) { if (!hasError) { firstError = err; hasError = true; } } } targets.length = 0; this._disposed = true; if (hasError) throw firstError; } } /** @internal */ _ensureNotDisposed() { if (this._disposed !== false) throw new DisposedError(); } _disposeTarget(target) { if (target instanceof Promise) return target.then(this._disposeTarget); else if (typeof WeakRef !== "undefined" && target instanceof WeakRef) target = target.deref(); if (target == null || target === this) return; else if (typeof target === "function") { if (target === this.dispose || target === this[Symbol.asyncDispose]) return; return target(); } else if (Symbol.asyncDispose != null && Symbol.asyncDispose in target && typeof target[Symbol.asyncDispose] === "function") return target[Symbol.asyncDispose](); else if (Symbol.dispose != null && Symbol.dispose in target && typeof target[Symbol.dispose] === "function") target[Symbol.dispose](); else if ("dispose" in target && typeof target.dispose === "function") return target.dispose(); } } function promiseWithResolver() { let _accept = undefined; const promise = new Promise((accept) => { _accept = accept; }); return [promise, _accept]; } function doNothing() { // do nothing } //# sourceMappingURL=AsyncDisposeAggregator.js.map