From 07e83b266ea06f0ca08498493a75e1ef1d01a206 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sat, 15 Jun 2024 17:16:25 -0400 Subject: [PATCH] Reintroduce bound dispose/disposeAsync getters --- README.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++- spec.emu | 107 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 194 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7718d0d..09d5a2d 100644 --- a/README.md +++ b/README.md @@ -1214,9 +1214,10 @@ class DisposableStack { get disposed(); /** - * Alias for `[Symbol.dispose]()`. + * Gets a bound function that when called invokes `Symbol.dispose` on this object. + * @returns {() => void} A function that when called disposes of any resources currently in this stack. */ - dispose(); + get dispose(); /** * Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`. @@ -1256,6 +1257,60 @@ class DisposableStack { [Symbol.toStringTag]; } + +class AsyncDisposableStack { + constructor(); + + /** + * Gets a value indicating whether the stack has been disposed. + * @returns {boolean} + */ + get disposed(); + + /** + * Gets a bound function that when called invokes `Symbol.asyncDispose` on this object. + * @returns {() => Pormise} A function that when called disposes of any resources currently in this stack. + */ + get disposeAsync(); + + /** + * Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`. + * @template {AsyncDisposable | Disposable | null | undefined} T + * @param {T} value - An `AsyncDisposable` object, `null`, or `undefined`. + * @returns {T} The provided value. + */ + use(value); + + /** + * Adds a non-disposable resource and a disposal callback to the top of the stack. + * @template T + * @param {T} value - A resource to be disposed. + * @param {(value: T) => PromiseLike | void} onDisposeAsync - A callback invoked to dispose the provided value. + * @returns {T} The provided value. + */ + adopt(value, onDisposeAsync); + + /** + * Adds a disposal callback to the top of the stack. + * @param {() => PromiseLike | void} onDisposeAsync - A callback to evaluate when this object is disposed. + * @returns {void} + */ + defer(onDisposeAsync); + + /** + * Moves all resources currently in this stack into a new `AsyncDisposableStack`. + * @returns {AsyncDisposableStack} The new `AsyncDisposableStack`. + */ + move(); + + /** + * Disposes of resources within this object. + * @returns {Promise} + */ + [Symbol.asyncDispose](); + + [Symbol.toStringTag]; +} ``` `AsyncDisposableStack` is the async version of `DisposableStack` and is a container used to aggregate async disposables, @@ -1546,6 +1601,65 @@ In this example, we can simply add new resources to the `stack` and move its con `this.#disposables`. In the subclass `[Symbol.dispose]()` method we don't need to call `super[Symbol.dispose]()` since that has already been tracked by the `stack.defer` call in the constructor. +### Bound `dispose`/`disposeAsync` + +The `dispose` and `disposeAsync` methods of `DisposableStack` and `AsyncDisposableStack` are getters that produce +functions bound to the `[Symbol.dispose]()` and `[Symbol.asyncDispose]()` methods of their respective classes to assist +with lightweight object creation in function-oriented APIs: + +```js +function createPluginHost() { + using stack = new DisposableStack(); + const channel = stack.use(new NodeProcessIpcChannelAdapter(process)); + const socket = stack.use(new NodePluginHostIpcSocket(channel)); + return { + loadPlugin(file) { ... }, + [Symbol.dispose]: stack.move().dispose, + }; +} +``` + +### Subclassing `DisposableStack`/`AsyncDisposableStack` + +When subclassing a `DisposableStack` or `AsyncDisposableStack`, it is only necesary to override the respective +`[Symbol.dispose]()` and `[Symbol.asyncDispose]()` methods, since the more convenient `dispose`/`disposeAsync` methods +are merely getters: + +```js +class MyDisposableStack extends DisposableStack { + [Symbol.dispose]() { + super[Symbol.dispose](); + } +} +``` + +Since neither `DisposableStack` nor `AsyncDisposableStack` support `Symbol.species`, special care must be taken if you +wish to return an instance of your subclass as the return value of the `move()` method of each class: + +```js +class MyDisposableStack extends DisposableStack { + #state; + + constructor(state) { + super(); + this.#state = state; + } + + move() { + // `super.move()` returns a `DisposableStack`, not a `MyDisposableStack`. Overwriting the prototype with + // `Object.setPrototypeOf` would result in an object without a `#state` field. We can address this by adding the + // result of `super.move()` to a new instance of `MyDisposableStack` with the appropriate state, though this is + // somewhat inefficient as repeatedly calling `move()` will create a stack with further and further nesting. + + const myStack = new MyDisposableStack(this.#state); + myStack.use(super.move()); + return myStack; + } + + ... +} +``` + # Relation to `Iterator` and `for..of` Iterators in ECMAScript also employ a "cleanup" step by way of supplying a `return` method. This means that there is diff --git a/spec.emu b/spec.emu index fd904f8..adeab73 100644 --- a/spec.emu +++ b/spec.emu @@ -4487,9 +4487,10 @@ contributors: Ron Buckton, Ecma International

This function performs the following steps when called:

1. If NewTarget is *undefined*, throw a *TypeError* exception. - 1. Let _disposableStack_ be ? OrdinaryCreateFromConstructor(NewTarget, *"%DisposableStack.prototype%"*, « [[DisposableState]], [[DisposeCapability]] »). + 1. Let _disposableStack_ be ? OrdinaryCreateFromConstructor(NewTarget, *"%DisposableStack.prototype%"*, « [[DisposableState]], [[DisposeCapability]], [[BoundDispose]] »). 1. Set _disposableStack_.[[DisposableState]] to ~pending~. 1. Set _disposableStack_.[[DisposeCapability]] to NewDisposeCapability(). + 1. Set _disposableStack_.[[BoundDispose]] to *undefined*. 1. Return _disposableStack_. @@ -4549,15 +4550,20 @@ contributors: Ron Buckton, Ecma International - -

DisposableStack.prototype.dispose ()

-

This method performs the following steps when called:

+ +

get DisposableStack.prototype.dispose

+

`DisposableStack.prototype.dispose` is an accessor property whose set accessor function is *undefined*. Its get accessor function performs the following steps:

1. Let _disposableStack_ be the *this* value. 1. Perform ? RequireInternalSlot(_disposableStack_, [[DisposableState]]). - 1. If _disposableStack_.[[DisposableState]] is ~disposed~, return *undefined*. - 1. Set _disposableStack_.[[DisposableState]] to ~disposed~. - 1. Return DisposeResources(_disposableStack_.[[DisposeCapability]], NormalCompletion(*undefined*)). + 1. If _disposableStack_.[[BoundDispose]] is *undefined*, then + 1. Let _dispose_ be GetMethod(_disposableStack_, @@dispose). + 1. If _dispose_ is *undefined*, throw a *TypeError* exception. + 1. Let _closure_ be a new Abstract Closure with no parameters that captures _disposableStack_ and _dispose_ and performs the following steps when called: + 1. Return ? Call(_dispose_, _disposableStack_, « »). + 1. Let _F_ be CreateBuiltinFunction(_closure_, 0, *""*, « »). + 1. Set _disposableStack_.[[BoundDispose]] to _F_. + 1. Return _disposableStack_.[[BoundDispose]].
@@ -4573,15 +4579,16 @@ contributors: Ron Buckton, Ecma International
-

DisposableStack.prototype.move()

+

DisposableStack.prototype.move ( )

This method performs the following steps when called:

1. Let _disposableStack_ be the *this* value. 1. Perform ? RequireInternalSlot(_disposableStack_, [[DisposableState]]). 1. If _disposableStack_.[[DisposableState]] is ~disposed~, throw a *ReferenceError* exception. - 1. Let _newDisposableStack_ be ? OrdinaryCreateFromConstructor(%DisposableStack%, *"%DisposableStack.prototype%"*, « [[DisposableState]], [[DisposeCapability]] »). + 1. Let _newDisposableStack_ be ? OrdinaryCreateFromConstructor(%DisposableStack%, *"%DisposableStack.prototype%"*, « [[DisposableState]], [[DisposeCapability]], [[BoundDispose]] »). 1. Set _newDisposableStack_.[[DisposableState]] to ~pending~. 1. Set _newDisposableStack_.[[DisposeCapability]] to _disposableStack_.[[DisposeCapability]]. + 1. Set _newDisposableStack_.[[BoundDispose]] to *undefined*. 1. Set _disposableStack_.[[DisposeCapability]] to NewDisposeCapability(). 1. Set _disposableStack_.[[DisposableState]] to ~disposed~. 1. Return _newDisposableStack_. @@ -4601,8 +4608,15 @@ contributors: Ron Buckton, Ecma International
-

DisposableStack.prototype [ @@dispose ] ()

-

The initial value of the @@dispose property is %DisposableStack.prototype.dispose%, defined in .

+

DisposableStack.prototype [ @@dispose ] ( )

+

This method performs the following steps when called:

+ + 1. Let _disposableStack_ be the *this* value. + 1. Perform ? RequireInternalSlot(_disposableStack_, [[DisposableState]]). + 1. If _disposableStack_.[[DisposableState]] is ~disposed~, return *undefined*. + 1. Set _disposableStack_.[[DisposableState]] to ~disposed~. + 1. Return ? DisposeResources(_disposableStack_.[[DisposeCapability]], NormalCompletion(*undefined*)). +
@@ -4651,6 +4665,17 @@ contributors: Ron Buckton, Ecma International Holds the stack of disposable resources. + + + [[BoundDispose]] + + + *undefined* or a function object + + + Caches the function returned by the `DisposableStack.prototype.dispose` accessor (). + + @@ -4682,9 +4707,10 @@ contributors: Ron Buckton, Ecma International

This function performs the following steps when called:

1. If NewTarget is *undefined*, throw a *TypeError* exception. - 1. Let _asyncDisposableStack_ be ? OrdinaryCreateFromConstructor(NewTarget, *"%AsyncDisposableStack.prototype%"*, « [[AsyncDisposableState]], [[DisposeCapability]] »). + 1. Let _asyncDisposableStack_ be ? OrdinaryCreateFromConstructor(NewTarget, *"%AsyncDisposableStack.prototype%"*, « [[AsyncDisposableState]], [[DisposeCapability]], [[BoundDisposeAsync]] »). 1. Set _asyncDisposableStack_.[[AsyncDisposableState]] to ~pending~. 1. Set _asyncDisposableStack_.[[DisposeCapability]] to NewDisposeCapability(). + 1. Set _asyncDisposableStack_.[[BoundDisposeAsync]] to *undefined*. 1. Return _asyncDisposableStack_.
@@ -4743,23 +4769,20 @@ contributors: Ron Buckton, Ecma International - -

AsyncDisposableStack.prototype.disposeAsync()

-

This method performs the following steps when called:

+ +

get AsyncDisposableStack.prototype.disposeAsync

+

`AsyncDisposableStack.prototype.disposeAsync` is an accessor property whose set accessor function is *undefined*. Its get accessor function performs the following steps:

1. Let _asyncDisposableStack_ be the *this* value. - 1. Let _promiseCapability_ be ! NewPromiseCapability(%Promise%). - 1. If _asyncDisposableStack_ does not have an [[AsyncDisposableState]] internal slot, then - 1. Perform ! Call(_promiseCapability_.[[Reject]], *undefined*, « a newly created *TypeError* object »). - 1. Return _promiseCapability_.[[Promise]]. - 1. If _asyncDisposableStack_.[[AsyncDisposableState]] is ~disposed~, then - 1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, « *undefined* »). - 1. Return _promiseCapability_.[[Promise]]. - 1. Set _asyncDisposableStack_.[[AsyncDisposableState]] to ~disposed~. - 1. Let _result_ be DisposeResources(_asyncDisposableStack_.[[DisposeCapability]], NormalCompletion(*undefined*)). - 1. IfAbruptRejectPromise(_result_, _promiseCapability_). - 1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, « _result_ »). - 1. Return _promiseCapability_.[[Promise]]. + 1. Perform ? RequireInternalSlot(_disposableStack_, [[DisposableState]]). + 1. If _asyncDisposableStack_.[[BoundDisposeAsync]] is *undefined*, then + 1. Let _disposeAsync_ be GetMethod(_asyncDisposableStack_, @@dispose). + 1. If _disposeAsync_ is *undefined*, throw a *TypeError* exception. + 1. Let _closure_ be a new Abstract Closure with no parameters that captures _asyncDisposableStack_ and _disposeAsync_ and performs the following steps when called: + 1. Return ? Call(_disposeAsync_, _asyncDisposableStack_, « »). + 1. Let _F_ be CreateBuiltinFunction(_closure_, 0, *""*, « »). + 1. Set _asyncDisposableStack_.[[BoundDisposeAsync]] to _F_. + 1. Return _asyncDisposableStack_.[[BoundDisposeAsync]].
@@ -4803,8 +4826,23 @@ contributors: Ron Buckton, Ecma International
-

AsyncDisposableStack.prototype [ @@asyncDispose ] ()

-

The initial value of the @@asyncDispose property is %AsyncDisposableStack.prototype.disposeAsync%, defined in .

+

AsyncDisposableStack.prototype [ @@asyncDispose ] ( )

+

This method performs the following steps when called:

+ + 1. Let _asyncDisposableStack_ be the *this* value. + 1. Let _promiseCapability_ be ! NewPromiseCapability(%Promise%). + 1. If _asyncDisposableStack_ does not have an [[AsyncDisposableState]] internal slot, then + 1. Perform ! Call(_promiseCapability_.[[Reject]], *undefined*, « a newly created *TypeError* object »). + 1. Return _promiseCapability_.[[Promise]]. + 1. If _asyncDisposableStack_.[[AsyncDisposableState]] is ~disposed~, then + 1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, « *undefined* »). + 1. Return _promiseCapability_.[[Promise]]. + 1. Set _asyncDisposableStack_.[[AsyncDisposableState]] to ~disposed~. + 1. Let _result_ be Completion(DisposeResources(_asyncDisposableStack_.[[DisposeCapability]], NormalCompletion(*undefined*))). + 1. IfAbruptRejectPromise(_result_, _promiseCapability_). + 1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, « _result_ »). + 1. Return _promiseCapability_.[[Promise]]. +
@@ -4853,6 +4891,17 @@ contributors: Ron Buckton, Ecma International Resources to be disposed when the disposable stack is disposed. + + + [[BoundDisposeAsync]] + + + *undefined* or a function object + + + Caches the function returned by the `AsyncDisposableStack.prototype.disposeAsync` accessor (). + +