216 lines
6.8 KiB
JavaScript
216 lines
6.8 KiB
JavaScript
/**
|
|
* Well-known `Symbol.dispose`. Resolves to the native well-known symbol when
|
|
* the runtime ships the explicit resource management proposal; otherwise falls
|
|
* back to `Symbol.for('Symbol.dispose')`, the same registry symbol used by the
|
|
* fallback stacks below, so `DisposableStack#use` finds the method.
|
|
*
|
|
* Consumers should key dispose methods on this constant —
|
|
* `class Foo { [disposeSymbol]() {} }` — rather than `Symbol.dispose`, so the
|
|
* class works on runtimes without the proposal.
|
|
*/
|
|
export const disposeSymbol = typeof Symbol.dispose === 'symbol' ? Symbol.dispose : Symbol.for('Symbol.dispose');
|
|
|
|
/**
|
|
* Well-known `Symbol.asyncDispose`. See {@link disposeSymbol} for the
|
|
* native/fallback resolution rules.
|
|
*/
|
|
export const asyncDisposeSymbol = typeof Symbol.asyncDispose === 'symbol' ? Symbol.asyncDispose : Symbol.for('Symbol.asyncDispose');
|
|
|
|
/** Object disposable via {@link disposeSymbol}. Mirrors `Disposable` from `esnext.disposable`. */
|
|
|
|
/** Object disposable via {@link asyncDisposeSymbol}. Mirrors `AsyncDisposable` from `esnext.disposable`. */
|
|
|
|
/**
|
|
* Mirrors the `DisposableStack` interface from `esnext.disposable`, declared
|
|
* locally so consumers don't need that lib in their TypeScript config.
|
|
*/
|
|
|
|
/**
|
|
* Mirrors the `AsyncDisposableStack` interface from `esnext.disposable`, declared
|
|
* locally so consumers don't need that lib in their TypeScript config.
|
|
*/
|
|
|
|
const NativeSuppressedError = typeof SuppressedError === 'function' ? SuppressedError : undefined;
|
|
|
|
/**
|
|
* Builds a `SuppressedError` (native when available, otherwise a compatible
|
|
* `{ error, suppressed }` Error) so {@link unwrapSuppressedErrors} can flatten
|
|
* the chain produced when several disposers throw.
|
|
*/
|
|
function createSuppressedError(error, suppressed) {
|
|
if (NativeSuppressedError) {
|
|
return new NativeSuppressedError(error, suppressed);
|
|
}
|
|
const wrapper = /* minify-error-disabled */new Error('An error was suppressed during disposal.');
|
|
return Object.assign(wrapper, {
|
|
name: 'SuppressedError',
|
|
error,
|
|
suppressed
|
|
});
|
|
}
|
|
function assertNotDisposed(disposed) {
|
|
if (disposed) {
|
|
throw /* minify-error-disabled */new ReferenceError('MUI X: The disposable stack is disposed.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minimal fallback for `DisposableStack`, used on runtimes that do not ship the
|
|
* explicit resource management proposal. Implements the subset of the proposal
|
|
* MUI X uses — `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and
|
|
* `[Symbol.dispose]` — so the `core-js-pure` ponyfill no longer has to be bundled.
|
|
*/
|
|
class FallbackDisposableStack {
|
|
stack = [];
|
|
isDisposed = false;
|
|
get disposed() {
|
|
return this.isDisposed;
|
|
}
|
|
use(value) {
|
|
assertNotDisposed(this.isDisposed);
|
|
if (value != null) {
|
|
const method = value[disposeSymbol];
|
|
if (typeof method !== 'function') {
|
|
throw /* minify-error-disabled */new TypeError('MUI X: The value is not disposable.');
|
|
}
|
|
this.stack.push(() => method.call(value));
|
|
}
|
|
return value;
|
|
}
|
|
adopt(value, onDispose) {
|
|
assertNotDisposed(this.isDisposed);
|
|
this.stack.push(() => onDispose(value));
|
|
return value;
|
|
}
|
|
defer(onDispose) {
|
|
assertNotDisposed(this.isDisposed);
|
|
this.stack.push(onDispose);
|
|
}
|
|
move() {
|
|
assertNotDisposed(this.isDisposed);
|
|
const next = new FallbackDisposableStack();
|
|
next.stack = this.stack;
|
|
this.stack = [];
|
|
this.isDisposed = true;
|
|
return next;
|
|
}
|
|
dispose() {
|
|
if (this.isDisposed) {
|
|
return;
|
|
}
|
|
this.isDisposed = true;
|
|
let hasError = false;
|
|
let error;
|
|
// Dispose in reverse (LIFO) order, aggregating failures into a SuppressedError chain.
|
|
for (let i = this.stack.length - 1; i >= 0; i -= 1) {
|
|
try {
|
|
this.stack[i]();
|
|
} catch (caught) {
|
|
error = hasError ? createSuppressedError(caught, error) : caught;
|
|
hasError = true;
|
|
}
|
|
}
|
|
this.stack = [];
|
|
if (hasError) {
|
|
throw error;
|
|
}
|
|
}
|
|
[disposeSymbol]() {
|
|
this.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minimal fallback for `AsyncDisposableStack`. See {@link FallbackDisposableStack};
|
|
* disposers may return promises and are awaited sequentially in LIFO order.
|
|
*/
|
|
class FallbackAsyncDisposableStack {
|
|
stack = [];
|
|
isDisposed = false;
|
|
get disposed() {
|
|
return this.isDisposed;
|
|
}
|
|
use(value) {
|
|
assertNotDisposed(this.isDisposed);
|
|
if (value != null) {
|
|
const record = value;
|
|
const method = record[asyncDisposeSymbol] ?? record[disposeSymbol];
|
|
if (typeof method !== 'function') {
|
|
throw /* minify-error-disabled */new TypeError('MUI X: The value is not async disposable.');
|
|
}
|
|
this.stack.push(() => method.call(value));
|
|
}
|
|
return value;
|
|
}
|
|
adopt(value, onDisposeAsync) {
|
|
assertNotDisposed(this.isDisposed);
|
|
this.stack.push(() => onDisposeAsync(value));
|
|
return value;
|
|
}
|
|
defer(onDisposeAsync) {
|
|
assertNotDisposed(this.isDisposed);
|
|
this.stack.push(onDisposeAsync);
|
|
}
|
|
move() {
|
|
assertNotDisposed(this.isDisposed);
|
|
const next = new FallbackAsyncDisposableStack();
|
|
next.stack = this.stack;
|
|
this.stack = [];
|
|
this.isDisposed = true;
|
|
return next;
|
|
}
|
|
async disposeAsync() {
|
|
if (this.isDisposed) {
|
|
return;
|
|
}
|
|
this.isDisposed = true;
|
|
let hasError = false;
|
|
let error;
|
|
for (let i = this.stack.length - 1; i >= 0; i -= 1) {
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.stack[i]();
|
|
} catch (caught) {
|
|
error = hasError ? createSuppressedError(caught, error) : caught;
|
|
hasError = true;
|
|
}
|
|
}
|
|
this.stack = [];
|
|
if (hasError) {
|
|
throw error;
|
|
}
|
|
}
|
|
[asyncDisposeSymbol]() {
|
|
return this.disposeAsync();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spec-compliant `DisposableStack`. Prefers the platform's native class when
|
|
* present; falls back to a minimal built-in implementation otherwise.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
export const DisposableStack = globalThis.DisposableStack ?? FallbackDisposableStack;
|
|
|
|
/**
|
|
* Spec-compliant `AsyncDisposableStack`. Prefers the platform's native class
|
|
* when present; falls back to a minimal built-in implementation otherwise.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
export const AsyncDisposableStack = globalThis.AsyncDisposableStack ?? FallbackAsyncDisposableStack;
|
|
|
|
/**
|
|
* Unwraps a `SuppressedError` chain (as produced by `DisposableStack.dispose()`
|
|
* when multiple disposers throw) into a flat list, outermost failure first.
|
|
* Returns `[error]` unchanged if it isn't a `SuppressedError`.
|
|
*/
|
|
export function unwrapSuppressedErrors(error) {
|
|
const failures = [];
|
|
let current = error;
|
|
while (typeof current === 'object' && current !== null && 'error' in current && 'suppressed' in current) {
|
|
failures.push(current.error);
|
|
current = current.suppressed;
|
|
}
|
|
failures.push(current);
|
|
return failures;
|
|
} |