Skip to content

Commit

Permalink
Reintroduce bound dispose/disposeAsync getters
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Jun 15, 2024
1 parent 38c1329 commit 07e83b2
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 31 deletions.
118 changes: 116 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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<void>} 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> | 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> | 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<void>}
*/
[Symbol.asyncDispose]();

[Symbol.toStringTag];
}
```
`AsyncDisposableStack` is the async version of `DisposableStack` and is a container used to aggregate async disposables,
Expand Down Expand Up @@ -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
Expand Down
107 changes: 78 additions & 29 deletions spec.emu
Original file line number Diff line number Diff line change
Expand Up @@ -4487,9 +4487,10 @@ contributors: Ron Buckton, Ecma International
<p>This function performs the following steps when called:</p>
<emu-alg>
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_.
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -4549,15 +4550,20 @@ contributors: Ron Buckton, Ecma International
</emu-alg>
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.dispose">
<h1>DisposableStack.prototype.dispose ()</h1>
<p>This method performs the following steps when called:</p>
<emu-clause id="sec-get-disposablestack.prototype.dispose">
<h1>get DisposableStack.prototype.dispose</h1>
<p>`DisposableStack.prototype.dispose` is an accessor property whose set accessor function is *undefined*. Its get accessor function performs the following steps:</p>
<emu-alg>
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]].
</emu-alg>
</emu-clause>

Expand All @@ -4573,15 +4579,16 @@ contributors: Ron Buckton, Ecma International
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.move">
<h1>DisposableStack.prototype.move()</h1>
<h1>DisposableStack.prototype.move ( )</h1>
<p>This method performs the following steps when called:</p>
<emu-alg>
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_.
Expand All @@ -4601,8 +4608,15 @@ contributors: Ron Buckton, Ecma International
</emu-clause>

<emu-clause id="sec-disposablestack.prototype-@@dispose">
<h1>DisposableStack.prototype [ @@dispose ] ()</h1>
<p>The initial value of the @@dispose property is %DisposableStack.prototype.dispose%, defined in <emu-xref href="#sec-disposablestack.prototype.dispose"></emu-xref>.</p>
<h1>DisposableStack.prototype [ @@dispose ] ( )</h1>
<p>This method performs the following steps when called:</p>
<emu-alg>
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*)).
</emu-alg>
</emu-clause>

<emu-clause id="sec-disposablestack.prototype-@@toStringTag">
Expand Down Expand Up @@ -4651,6 +4665,17 @@ contributors: Ron Buckton, Ecma International
Holds the stack of disposable resources.
</td>
</tr>
<tr>
<td>
[[BoundDispose]]
</td>
<td>
*undefined* or a function object
</td>
<td>
Caches the function returned by the `DisposableStack.prototype.dispose` accessor (<emu-xref href="#sec-get-disposablestack.prototype.dispose"></emu-xref>).
</td>
</tr>
</tbody>
</table>
</emu-table>
Expand Down Expand Up @@ -4682,9 +4707,10 @@ contributors: Ron Buckton, Ecma International
<p>This function performs the following steps when called:</p>
<emu-alg>
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_.
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -4743,23 +4769,20 @@ contributors: Ron Buckton, Ecma International
</emu-alg>
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype.disposeAsync">
<h1>AsyncDisposableStack.prototype.disposeAsync()</h1>
<p>This method performs the following steps when called:</p>
<emu-clause id="sec-get-asyncdisposablestack.prototype.disposeAsync">
<h1>get AsyncDisposableStack.prototype.disposeAsync</h1>
<p>`AsyncDisposableStack.prototype.disposeAsync` is an accessor property whose set accessor function is *undefined*. Its get accessor function performs the following steps:</p>
<emu-alg>
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]].
</emu-alg>
</emu-clause>

Expand Down Expand Up @@ -4803,8 +4826,23 @@ contributors: Ron Buckton, Ecma International
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype-@@asyncDispose">
<h1>AsyncDisposableStack.prototype [ @@asyncDispose ] ()</h1>
<p>The initial value of the @@asyncDispose property is %AsyncDisposableStack.prototype.disposeAsync%, defined in <emu-xref href="#sec-asyncdisposablestack.prototype.disposeAsync"></emu-xref>.</p>
<h1>AsyncDisposableStack.prototype [ @@asyncDispose ] ( )</h1>
<p>This method performs the following steps when called:</p>
<emu-alg>
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]].
</emu-alg>
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype-@@toStringTag">
Expand Down Expand Up @@ -4853,6 +4891,17 @@ contributors: Ron Buckton, Ecma International
Resources to be disposed when the disposable stack is disposed.
</td>
</tr>
<tr>
<td>
[[BoundDisposeAsync]]
</td>
<td>
*undefined* or a function object
</td>
<td>
Caches the function returned by the `AsyncDisposableStack.prototype.disposeAsync` accessor (<emu-xref href="#sec-get-asyncdisposablestack.prototype.disposeAsync"></emu-xref>).
</td>
</tr>
</tbody>
</table>
</emu-table>
Expand Down

0 comments on commit 07e83b2

Please sign in to comment.