From baa68e6b0c4b9e99cc301df730fe5f20fc63ef2f Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 20 Jul 2023 22:02:07 -0400 Subject: [PATCH] Clean up DisposeCapability in DisposeResources, plus other spec alignment (#85) --- packages/disposable/src/asyncDisposable.ts | 5 +- .../disposable/src/asyncDisposableStack.ts | 4 +- packages/disposable/src/disposable.ts | 5 +- packages/disposable/src/disposableStack.ts | 4 +- packages/disposable/src/internal/utils.ts | 198 ++++++++---------- 5 files changed, 95 insertions(+), 121 deletions(-) diff --git a/packages/disposable/src/asyncDisposable.ts b/packages/disposable/src/asyncDisposable.ts index 3ec852fd..572477c5 100644 --- a/packages/disposable/src/asyncDisposable.ts +++ b/packages/disposable/src/asyncDisposable.ts @@ -16,13 +16,14 @@ import /*#__INLINE__*/ { isAsyncIterableObject, isFunction, isIterableObject, isObject } from "@esfx/internal-guards"; import { Disposable } from "./disposable.js"; -import { CreateScope, DisposeResources, Is } from "./internal/utils.js"; +import { CreateScope, DisposeResources, execAsync } from "./internal/utils.js"; const asyncDisposeSymbol: unique symbol = typeof (Symbol as any)["asyncDispose"] === "symbol" ? (Symbol as any)["asyncDispose"] : Symbol.for("@esfx/disposable:AsyncDisposable.asyncDispose"); +type Is = T; type AsyncDisposeSymbol = globalThis.SymbolConstructor extends { "asyncDispose": Is } ? S : @@ -82,7 +83,7 @@ export namespace AsyncDisposable { } finally { context.state = "done"; - await DisposeResources("async-dispose", context.disposables, context.throwCompletion); + await execAsync(DisposeResources("async-dispose", context.disposables, context.throwCompletion)); } } diff --git a/packages/disposable/src/asyncDisposableStack.ts b/packages/disposable/src/asyncDisposableStack.ts index bd98f4d6..afe92f1f 100644 --- a/packages/disposable/src/asyncDisposableStack.ts +++ b/packages/disposable/src/asyncDisposableStack.ts @@ -17,7 +17,7 @@ import /*#__INLINE__*/ { isFunction, isObject } from "@esfx/internal-guards"; import { AsyncDisposable } from "./asyncDisposable.js"; import { Disposable } from "./disposable.js"; -import { AddDisposableResource, DisposeCapability, DisposeMethod, DisposeResources, GetDisposeMethod, NewDisposeCapability } from "./internal/utils.js"; +import { AddDisposableResource, DisposeCapability, DisposeMethod, DisposeResources, execAsync, GetDisposeMethod, NewDisposeCapability } from "./internal/utils.js"; const weakAsyncDisposableState = new WeakMap(); const weakDisposeCapability = new WeakMap>(); @@ -99,7 +99,7 @@ export class AsyncDisposableStack implements AsyncDisposable { // 9. Return _promiseCapability_.[[Promise]]. const disposeCapability = weakDisposeCapability.get(this)!; weakDisposeCapability.delete(this); - await DisposeResources("async-dispose", disposeCapability, /*completion*/ undefined); + await execAsync(DisposeResources("async-dispose", disposeCapability, /*completion*/ undefined)); } /** diff --git a/packages/disposable/src/disposable.ts b/packages/disposable/src/disposable.ts index 19c183de..c529ebd6 100644 --- a/packages/disposable/src/disposable.ts +++ b/packages/disposable/src/disposable.ts @@ -15,13 +15,14 @@ */ import /*#__INLINE__*/ { isFunction, isIterableObject, isObject } from "@esfx/internal-guards"; -import { CreateScope, DisposeResources, Is } from "./internal/utils.js"; +import { CreateScope, DisposeResources, execSync } from "./internal/utils.js"; const disposeSymbol: unique symbol = typeof (Symbol as any)["dispose"] === "symbol" ? (Symbol as any)["dispose"] : Symbol.for("@esfx/disposable:Disposable.dispose"); +type Is = T; type DisposeSymbol = globalThis.SymbolConstructor extends { "dispose": Is } ? S : @@ -84,7 +85,7 @@ export namespace Disposable { } finally { context.state = "done"; - DisposeResources("sync-dispose", context.disposables, context.throwCompletion); + execSync(DisposeResources("sync-dispose", context.disposables, context.throwCompletion)); } } diff --git a/packages/disposable/src/disposableStack.ts b/packages/disposable/src/disposableStack.ts index eae96483..523de1bf 100644 --- a/packages/disposable/src/disposableStack.ts +++ b/packages/disposable/src/disposableStack.ts @@ -16,7 +16,7 @@ import /*#__INLINE__*/ { isFunction, isObject } from "@esfx/internal-guards"; import { Disposable } from "./disposable.js"; -import { AddDisposableResource, DisposeCapability, DisposeMethod, DisposeResources, GetDisposeMethod, NewDisposeCapability } from "./internal/utils.js"; +import { AddDisposableResource, DisposeCapability, DisposeMethod, DisposeResources, execSync, GetDisposeMethod, NewDisposeCapability } from "./internal/utils.js"; const weakDisposableState = new WeakMap(); const weakDisposeCapability = new WeakMap>(); @@ -94,7 +94,7 @@ export class DisposableStack { // 5. Return ? DisposeResources(_disposableStack_, NormalCompletion(*undefined*)). const disposeCapability = weakDisposeCapability.get(this)!; weakDisposeCapability.delete(this); - DisposeResources("sync-dispose", disposeCapability, /*completion*/ undefined); + execSync(DisposeResources("sync-dispose", disposeCapability, /*completion*/ undefined)); } /** diff --git a/packages/disposable/src/internal/utils.ts b/packages/disposable/src/internal/utils.ts index 7a0d5395..fb82ed71 100644 --- a/packages/disposable/src/internal/utils.ts +++ b/packages/disposable/src/internal/utils.ts @@ -40,13 +40,20 @@ import { AsyncDisposable } from "../asyncDisposable.js"; /* @internal */ import { Disposable } from "../disposable.js"; -export { }; - -export type Is = T; - /* @internal */ -export function GetMethod(V: V, P: P): V[P] extends ((...args: any[]) => any) ? V[P] : undefined; -export function GetMethod(V: V, P: P) { +export { + DisposeCapability, + NewDisposeCapability, + AddDisposableResource, + DisposeMethod, + GetDisposeMethod, + ThrowCompletion, + DisposeResources, + CreateScope, +}; + +function GetMethod(V: V, P: P): V[P] extends ((...args: any[]) => any) ? V[P] : undefined; +function GetMethod(V: V, P: P) { // ECMA262 7.3.11 GetMethod ( _V_, _P_ ) const func = V[P]; @@ -55,31 +62,40 @@ export function GetMethod(V: V, P: P) { return func; } -/* @internal */ -export const Call: (F: (this: T, ...args: A) => R, V: T, ...argumentsList: A ) => R = +const Call: (F: (this: T, ...args: A) => R, V: T, ...argumentsList: A ) => R = // ECMA262 7.3.14 Call ( _F_, _V_ [ , _argumentsList_ ] ) Function.prototype.call.bind(Function.prototype.call); -/* @internal */ -export interface DisposeCapability { - disposableResourceStack: DisposableResourceRecord[]; +interface DisposeCapability { + disposableResourceStack: DisposableResourceRecord[] | undefined; } -/* @internal */ -export function NewDisposeCapability(): DisposeCapability { +type DisposeMethod = + Hint extends "async-dispose" ? + ((this: object | undefined) => void | PromiseLike) : + ((this: object | undefined) => void); + +interface DisposableResourceRecord { + ResourceValue: object | undefined; + Hint: Hint; + DisposeMethod: DisposeMethod | undefined; +} + +function NewDisposeCapability(): DisposeCapability { return { disposableResourceStack: [] }; } -/* @internal */ -export interface ThrowCompletion { +interface ThrowCompletion { value: unknown; } -/* @internal */ -export function AddDisposableResource(disposeCapability: DisposeCapability, V: unknown, hint: Hint, method?: DisposeMethod) { +function AddDisposableResource(disposeCapability: DisposeCapability, V: unknown, hint: Hint, method?: DisposeMethod) { // 3.1.2 AddDisposableResource ( _disposable_, _V_, _hint_ [ , _method_ ] ) let resource: DisposableResourceRecord; + // *. Assert: _disposeCapability_.[[DisposableResourceStack]] is not ~empty~. + if (!disposeCapability.disposableResourceStack) throw new Error("Illegal state."); + // 1. If _method_ is not present then, if (arguments.length === 3) { // a. If _V_ is either *null* or *undefined* and _hint_ is ~sync-dispose~, then @@ -114,8 +130,7 @@ export function AddDisposableResource(V: any, hint: Hint, method?: DisposeMethod): DisposableResourceRecord { +function CreateDisposableResource(V: any, hint: Hint, method?: DisposeMethod): DisposableResourceRecord { // 3.1.3 CreateDisposableResource ( _V_, _hint_ [, _method_ ] ) // 1. If _method_ is not present, then @@ -152,8 +167,7 @@ export function CreateDisposableResource; } -/* @internal */ -export function GetDisposeMethod(V: object, hint: Hint): DisposeMethod | undefined { +function GetDisposeMethod(V: object, hint: Hint): DisposeMethod | undefined { // 3.1.4 GetDisposeMethod ( _V_, _hint_ ) let method: DisposeMethod | undefined; @@ -167,6 +181,17 @@ export function GetDisposeMethod( if (method === undefined) { // i. Set _method_ to ? GetMethod(_V_, @@dispose). method = GetMethod(V as Disposable, Disposable.dispose); + // ii. Let _closure_ be a new Abstract Closure with no parameters that captures _method_ and performs the following steps when called: + const closure = (void 0, function (this: object | undefined) { + // 1. Let _O_ be the *this* value. + // 2. Perform ? Call(_method_, _O_). + // 3. Return *undefined*. + Call(method!, this); + }); + + // iii. NOTE: This function is not observable to user code. It is used to ensure that a Promise returned from a synchronous `@@dispose` method will not be awaited. + // iv. Return CreateBuiltinFunction(_closure_, 0, *""*, « »). + return closure; } } @@ -180,58 +205,39 @@ export function GetDisposeMethod( return method; } -/* @internal */ -export function Dispose(V: object | undefined, hint: Hint, method: DisposeMethod | undefined) { - // 3.1.5 Dispose ( _V_, _hint_, _method_ ) - - return hint === "async-dispose" ? - DisposeAsync(V, method) : - DisposeSync(V, method); +function * Await(value: T): Generator, unknown> { + return (yield value) as Awaited; } -function DisposeSync(V: object | undefined, method: DisposeMethod<"sync-dispose"> | undefined) { +function * Dispose(V: object | undefined, hint: Hint, method: DisposeMethod | undefined) { // 3.1.5 Dispose ( _V_, _hint_, _method_ ) - // NOTE: when _hint_ is ~sync-dispose~ + let result: unknown; + // 1. If _method_ is *undefined*, let _result_ be *undefined*. + if (method === undefined) { + result = undefined; + } // 2. Else, let _result_ be ? Call(_method_, _V_). - // 3. If _hint_ is ~async-dispose~, then - // a. Perform ? Await(_result_). - - if (method !== undefined) { - Call(method, V); + else { + result = Call(method, V); } - // 4. Return *undefined*. -} - -async function DisposeAsync(V: object | undefined, method: DisposeMethod<"async-dispose"> | undefined) { - // 3.1.5 Dispose ( _V_, _hint_, _method_ ) - // NOTE: when _hint_ is ~async-dispose~ - - // 1. If _method_ is *undefined*, let _result_ be *undefined*. - // 2. Else, let _result_ be ? Call(_method_, _V_). // 3. If _hint_ is ~async-dispose~, then - // a. Perform ? Await(_result_). - if (method !== undefined) { - await Call(method, V); + if (hint === "async-dispose") { + // a. Perform ? Await(_result_). + yield * Await(result); } // 4. Return *undefined*. + return undefined; } -/* @internal */ -export function DisposeResources(hint: Hint, disposeCapability: DisposeCapability, throwCompletion: ThrowCompletion | undefined): Hint extends "async-dispose" ? Promise | void : void; -export function DisposeResources(hint: Hint, disposeCapability: DisposeCapability, throwCompletion: ThrowCompletion | undefined) { +function * DisposeResources(hint: Hint, disposeCapability: DisposeCapability, throwCompletion: ThrowCompletion | undefined) { // 3.1.6 DisposeResources ( _disposeCapability_, _completion_ ) - return hint === "async-dispose" ? - DisposeResourcesAsync(disposeCapability as DisposeCapability<"async-dispose">, throwCompletion) : - DisposeResourcesSync(disposeCapability as DisposeCapability<"sync-dispose">, throwCompletion); -} -function DisposeResourcesSync(disposeCapability: DisposeCapability<"sync-dispose">, throwCompletion: ThrowCompletion | undefined) { - // 3.1.6 DisposeResources ( _disposeCapability_, _completion_ ) - // NOTE: when _hint_ is ~sync-dispose~ + // *. Assert: _disposeCapability_.[[DisposableResourceStack]] is not ~empty~. + if (!disposeCapability.disposableResourceStack) throw new Error("Illegal state."); // 1. For each _resource_ of _disposeCapability_.[[DisposableResourceStack]], in reverse list order, do for (let i = disposeCapability.disposableResourceStack.length - 1; i >= 0; i--) { @@ -239,7 +245,7 @@ function DisposeResourcesSync(disposeCapability: DisposeCapability<"sync-dispose // a. Let _result_ be Dispose(_resource_.[[ResourceValue]], _resource_.[[Hint]], _resource_.[[DisposeMethod]]). try { - Dispose(resource.ResourceValue, resource.Hint, resource.DisposeMethod); + yield * Dispose(resource.ResourceValue, resource.Hint, resource.DisposeMethod); } // b. If _result_.[[Type]] is ~throw~, then catch (e) { @@ -266,64 +272,14 @@ function DisposeResourcesSync(disposeCapability: DisposeCapability<"sync-dispose } } - // 5. Return _completion_. - if (throwCompletion) throw throwCompletion.value; -} - -async function DisposeResourcesAsync(disposeCapability: DisposeCapability<"async-dispose">, throwCompletion: ThrowCompletion | undefined, errors?: unknown[]) { - // 3.1.6 DisposeResources ( _disposeCapability_, _completion_, [ , _errors_ ] ) - // NOTE: when _hint_ is ~async-dispose~ - - // 1. For each _resource_ of _disposeCapability_.[[DisposableResourceStack]], in reverse list order, do - for (let i = disposeCapability.disposableResourceStack.length - 1; i >= 0; i--) { - const resource = disposeCapability.disposableResourceStack[i]; - - // a. Let _result_ be Dispose(_resource_.[[ResourceValue]], _resource_.[[Hint]], _resource_.[[DisposeMethod]]). - try { - await Dispose(resource.ResourceValue, resource.Hint, resource.DisposeMethod); - } - // b. If _result_.[[Type]] is ~throw~, then - catch (e) { - // i. If _completion_.[[Type]] is ~throw~, then - if (throwCompletion) { - // 1. Set _result_ to _result_.[[Value]]. - const result = e; - - // 2. Let _suppressed_ be _completion_.[[Value]]. - const suppressed = throwCompletion.value; - - // 3. Let _error_ be a newly created *SuppressedError* object. - // 4. Perform CreateNonEnumerableDataPropertyOrThrow(_error_, "error", _result_). - // 5. Perform CreateNonEnumerableDataPropertyOrThrow(_error_, "suppressed", _suppressed_). - const error = CreateSuppressedError("async-dispose", result, suppressed); - - // 6. Set _completion_ to ThrowCompletion(_error_). - throwCompletion = { value: error }; - } - else { - // 1. Set _completion_ to _result_. - throwCompletion = { value: e }; - } - } - } + // *. NOTE: After _disposeCapability_ has been disposed, it will never be used again. The contents of _disposeCapability_.[[DisposableResourceStack]] can be discarded at this point. + // *. Set _disposeCapability_.[[DisposableResourceStack]] to ~empty~. + disposeCapability.disposableResourceStack = undefined; // 5. Return _completion_. if (throwCompletion) throw throwCompletion.value; } -/* @internal */ -export type DisposeMethod = - Hint extends "async-dispose" ? - ((this: object | undefined) => void | PromiseLike) : - ((this: object | undefined) => void); - -/* @internal */ -export interface DisposableResourceRecord { - ResourceValue: object | undefined; - Hint: Hint; - DisposeMethod: DisposeMethod | undefined; -} - interface ScopeContext { scope: Scope; state: "initialized" | "exiting" | "done"; @@ -341,8 +297,7 @@ interface Scope { fail(error: unknown): void; } -/* @internal */ -export function CreateScope(hint: Hint): ScopeContext { +function CreateScope(hint: Hint): ScopeContext { // Credit to Mathieu Hofman for initial `for (const { using } of Disposable)` mechanism: https://github.com/mhofman/disposator/ // See THIRD PARTY LICENSE NOTICE at the top of this file. // Modified to return a `fail` callback to emulate error suppression semantics of https://github.com/tc39/proposal-explicit-resource-management/ @@ -391,3 +346,20 @@ function CreateSuppressedError(hi } return e; } + +/* @internal */ +export function execSync(gen: Generator) { + const res = gen.next(); + if (res.done) return res.value; + throw new Error("Illegal state"); +} + +/* @internal */ +export function execAsync(gen: Generator) { + return new Promise(resolve => resolve(step("next", /*value*/ undefined))); + function step(verb: "next" | "throw", value: unknown): T | Promise { + const res = gen[verb](value); + if (res.done) return res.value; + return Promise.resolve(res.value).then(value => step("next", value), value => step("throw", value)); + } +}