diff --git a/.changeset/stale-bobcats-develop.md b/.changeset/stale-bobcats-develop.md new file mode 100644 index 00000000000..6281af490a3 --- /dev/null +++ b/.changeset/stale-bobcats-develop.md @@ -0,0 +1,5 @@ +--- +'@whatwg-node/disposablestack': patch +--- + +Improve `disposed` flag and cleanup callbacks on AsyncDisposable on disposeAsync call diff --git a/packages/disposablestack/src/AsyncDisposableStack.ts b/packages/disposablestack/src/AsyncDisposableStack.ts index 3105cdc8d8d..0aeb20c2575 100644 --- a/packages/disposablestack/src/AsyncDisposableStack.ts +++ b/packages/disposablestack/src/AsyncDisposableStack.ts @@ -4,7 +4,7 @@ import { isAsyncDisposable, isSyncDisposable, MaybePromise } from './utils.js'; export class PonyfillAsyncDisposableStack implements AsyncDisposableStack { private callbacks: (() => MaybePromise)[] = []; get disposed(): boolean { - return false; + return this.callbacks.length === 0; } use(value: T): T { @@ -17,12 +17,16 @@ export class PonyfillAsyncDisposableStack implements AsyncDisposableStack { } adopt(value: T, onDisposeAsync: (value: T) => MaybePromise): T { - this.callbacks.push(() => onDisposeAsync(value)); + if (onDisposeAsync) { + this.callbacks.push(() => onDisposeAsync(value)); + } return value; } defer(onDisposeAsync: () => MaybePromise): void { - this.callbacks.push(onDisposeAsync); + if (onDisposeAsync) { + this.callbacks.push(onDisposeAsync); + } } move(): AsyncDisposableStack { @@ -36,10 +40,42 @@ export class PonyfillAsyncDisposableStack implements AsyncDisposableStack { return this[DisposableSymbols.asyncDispose](); } - async [DisposableSymbols.asyncDispose](): Promise { - for (const cb of this.callbacks) { - await cb(); + private _error?: Error; + + private _iterateCallbacks(): MaybePromise { + const cb = this.callbacks.pop(); + if (cb) { + try { + const res$ = cb(); + if (res$?.then) { + return res$.then( + () => this._iterateCallbacks(), + error => { + this._error = this._error ? new SuppressedError(error, this._error) : error; + return this._iterateCallbacks(); + }, + ); + } + } catch (error: any) { + this._error = this._error ? new SuppressedError(error, this._error) : error; + } + return this._iterateCallbacks(); + } + } + + [DisposableSymbols.asyncDispose](): Promise { + const res$ = this._iterateCallbacks(); + if (res$?.then) { + return res$.then(() => { + if (this._error) { + throw this._error; + } + }) as Promise; + } + if (this._error) { + throw this._error; } + return undefined as any as Promise; } readonly [Symbol.toStringTag]: string = 'AsyncDisposableStack'; diff --git a/packages/disposablestack/src/DisposableStack.ts b/packages/disposablestack/src/DisposableStack.ts index 81ce614a0ac..efbe08cc851 100644 --- a/packages/disposablestack/src/DisposableStack.ts +++ b/packages/disposablestack/src/DisposableStack.ts @@ -4,11 +4,7 @@ import { isSyncDisposable } from './utils.js'; export class PonyfillDisposableStack implements DisposableStack { private callbacks: (() => void)[] = []; get disposed(): boolean { - return false; - } - - dispose(): void { - return this[DisposableSymbols.dispose](); + return this.callbacks.length === 0; } use(value: T): T { @@ -19,12 +15,16 @@ export class PonyfillDisposableStack implements DisposableStack { } adopt(value: T, onDispose: (value: T) => void): T { - this.callbacks.push(() => onDispose(value)); + if (onDispose) { + this.callbacks.push(() => onDispose(value)); + } return value; } defer(onDispose: () => void): void { - this.callbacks.push(onDispose); + if (onDispose) { + this.callbacks.push(onDispose); + } } move(): DisposableStack { @@ -34,11 +34,26 @@ export class PonyfillDisposableStack implements DisposableStack { return stack; } - [DisposableSymbols.dispose](): void { - for (const cb of this.callbacks) { - cb(); + dispose(): void { + return this[DisposableSymbols.dispose](); + } + + private _error?: Error; + + private _iterateCallbacks(): void { + const cb = this.callbacks.pop(); + if (cb) { + try { + cb(); + } catch (error: any) { + this._error = this._error ? new SuppressedError(error, this._error) : error; + } + return this._iterateCallbacks(); } - this.callbacks = []; + } + + [DisposableSymbols.dispose](): void { + return this._iterateCallbacks(); } readonly [Symbol.toStringTag]: string = 'DisposableStack'; diff --git a/packages/disposablestack/src/SupressedError.ts b/packages/disposablestack/src/SupressedError.ts new file mode 100644 index 00000000000..e1a4476d494 --- /dev/null +++ b/packages/disposablestack/src/SupressedError.ts @@ -0,0 +1,12 @@ +export class PonyfillSuppressedError extends Error implements SuppressedError { + // eslint-disable-next-line n/handle-callback-err + constructor( + public error: any, + public suppressed: any, + message?: string, + ) { + super(message); + this.name = 'SuppressedError'; + Error.captureStackTrace(this, SuppressedError); + } +} diff --git a/packages/disposablestack/src/index.ts b/packages/disposablestack/src/index.ts index cacf5d221a5..de6b970c137 100644 --- a/packages/disposablestack/src/index.ts +++ b/packages/disposablestack/src/index.ts @@ -1,6 +1,8 @@ import { PonyfillAsyncDisposableStack } from './AsyncDisposableStack.js'; import { PonyfillDisposableStack } from './DisposableStack.js'; +import { PonyfillSuppressedError } from './SupressedError.js'; export const DisposableStack = globalThis.DisposableStack || PonyfillDisposableStack; export const AsyncDisposableStack = globalThis.AsyncDisposableStack || PonyfillAsyncDisposableStack; +export const SuppressedError = globalThis.SuppressedError || PonyfillSuppressedError; export * from './symbols.js';