Skip to content

Commit

Permalink
Split 'use' overloads into multiple methods (#103)
Browse files Browse the repository at this point in the history
* Split 'use' overloads into multiple methods

* PR Feedback
  • Loading branch information
rbuckton authored Nov 15, 2022
1 parent 4a0f3d4 commit 2df6c9e
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 53 deletions.
135 changes: 120 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,21 +695,28 @@ class DisposableStack {
get dispose();

/**
* Adds a resource to the top of the stack.
* @template {Disposable | (() => void) | null | undefined} T
* @param {T} value - A `Disposable` object, or a callback to evaluate
* when this object is disposed.
* Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`.
* @template {Disposable | null | undefined} T
* @param {T} value - A `Disposable` object, `null`, or `undefined`.
* @returns {T} The provided value.
*/
use(value);

/**
* Adds a resource to the top of the stack.
* 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) => void} onDispose - A callback invoked to dispose the provided value.
* @returns {T} The provided value.
*/
use(value, onDispose);
adopt(value, onDispose);

/**
* Adds a disposal callback to the top of the stack.
* @param {() => void} onDispose - A callback to evaluate when this object is disposed.
* @returns {void}
*/
defer(onDispose);

/**
* Moves all resources currently in this stack into a new `DisposableStack`.
Expand All @@ -736,21 +743,28 @@ class AsyncDisposableStack {
get disposeAsync();

/**
* Adds a resource to the top of the stack.
* @template {AsyncDisposable | Disposable | (() => void | Promise<void>) | null | undefined} T
* @param {T} value - An `AsyncDisposable` or `Disposable` object, or a callback to evaluate
* when this object is disposed.
* 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` or `Disposable` object, `null`, or `undefined`.
* @returns {T} The provided value.
*/
use(value);

/**
* Adds a resource to the top of the stack.
* 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) => void | Promise<void>} onDisposeAsync - A callback invoked to dispose the provided value.
* @returns {T} The provided value.
*/
use(value, onDisposeAsync);
adopt(value, onDisposeAsync);

/**
* Adds a disposal callback to the top of the stack.
* @param {() => void | Promise<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`.
Expand Down Expand Up @@ -805,8 +819,7 @@ The ability to create a disposable resource from a callback has several benefits
```js
{
using stack = new DisposableStack();
const reader = ...;
stack.use(() => reader.releaseLock());
const reader = stack.adopt(createReader(), reader => reader.releaseLock());
...
}
```
Expand All @@ -816,7 +829,7 @@ The ability to create a disposable resource from a callback has several benefits
function f() {
using stack = new DisposableStack();
console.log("enter");
stack.use(() => console.log("exit"));
stack.defer(() => console.log("exit"));
...
}
```
Expand All @@ -830,6 +843,7 @@ easily manage lifetime in these scenarios:
```js
class PluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
Expand Down Expand Up @@ -857,12 +871,103 @@ class PluginHost {
// `#socket` to be GC'd.
}

loadPlugin(file) {
// A disposable should try to ensure access is consistent with its "disposed" state, though this isn't strictly
// necessary since some disposables could be reusable (i.e., a Connection with an `open()` method, etc.).
if (this.#disposed) throw new ReferenceError("Object is disposed.");
// ...
}

[Symbol.dispose]() {
if (!this.#disposed) {
this.#disposed = true;
const disposables = this.#disposables;

// NOTE: we can free `#socket` and `#channel` here since they will be disposed by the call to
// `disposables[Symbol.dispose]()`, below. This isn't strictly a requirement for every Disposable, but is
// good housekeeping since these objects will no longer be useable.
this.#socket = undefined;
this.#channel = undefined;
this.#disposables = undefined;

// Dispose all resources in `disposables`
disposables[Symbol.dispose]();
}
}
}
```
### Subclassing `Disposable` Classes
You can also use a `DisposableStack` to assist with disposal in a subclass constructor whose superclass is disposable:
```js
class DerivedPluginHost extends PluginHost {
constructor() {
super();

// Create a DisposableStack to cover the subclass constructor.
using stack = new DisposableStack();

// Defer a callback to dispose resources on the superclass. We use `defer` so that we can invoke the version of
// `[Symbol.dispose]` on the superclass and not on this or any subclasses.
stack.defer(() => super[Symbol.dispose]());

// If any operations throw during subclass construction, the instance will still be disposed, and superclass
// resources will be freed
doSomethingThatCouldPotentiallyThrow();

// As the last step before exiting, empty out the DisposableStack so that we don't dispose ourselves.
stack.move();
}
}
```
Here, we can use `stack` to track the result of `super()` (i.e., the `this` value). If any exception occurs during
subclass construction, we can ensure that `[Symbol.dispose]()` is called, freeing resources. If the subclass also needs
to track its own disposable resources, this example is modified slightly:
```js
class DerivedPluginHostWithOwnDisposables extends PluginHost {
#logger;
#disposables;

constructor() {
super()

// Create a DisposableStack to cover the subclass constructor.
using stack = new DisposableStack();

// Defer a callback to dispose resources on the superclass. We use `defer` so that we can invoke the version of
// `[Symbol.dispose]` on the superclass and not on this or any subclasses.
stack.defer(() => super[Symbol.dispose]());

// Create a logger that uses the file system and add it to our own disposables.
this.#logger = stack.use(new FileLogger());

// If any operations throw during subclass construction, the instance will still be disposed, and superclass
// resources will be freed
doSomethingThatCouldPotentiallyThrow();

// Persist our own disposables. If construction fails prior to the call to `stack.move()`, our own disposables
// will be disposed before they are set, and then the superclass `[Symbol.dispose]` will be invoked.
this.#disposables = stack.move();
}

[Symbol.dispose]() {
this.#logger = undefined;

// Dispose of our resources and those of our superclass. We do not need to invoke `super[Symbol.dispose]()` since
// that is already tracked by the `stack.defer` call in the constructor.
this.#disposables[Symbol.dispose]();
}
}
```
In this example, we can simply add new resources to the `stack` and move its contents into the subclass instance's
`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.
# 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
110 changes: 72 additions & 38 deletions spec.emu
Original file line number Diff line number Diff line change
Expand Up @@ -3562,38 +3562,42 @@ contributors: Ron Buckton, Ecma International
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.use">
<h1>DisposableStack.prototype.use( _value_ [, _onDispose_ ] )</h1>
<p>When the `use` function is called with one or two arguments, the following steps are taken:</p>
<emu-note>
<p>The _onDispose_ argument is optional. If it is not provided, *undefined* is used.</p>
</emu-note>
<h1>DisposableStack.prototype.use( _value_ )</h1>
<p>When the `use` function is called with one argument, the following steps are taken:</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. If _onDispose_ is not *undefined*, then
1. If IsCallable(_onDispose_) is *false*, throw a *TypeError* exception.
1. Let _F_ be a new built-in function object as defined in <emu-xref href="#sec-disposablestack-callback-functions"></emu-xref>.
1. Set _F_.[[Argument]] to _value_.
1. Set _F_.[[OnDisposeCallback]] to _onDispose_.
1. Perform ? AddDisposableResource(_disposableStack_, *undefined*, ~sync-dispose~, _F_).
1. Else, if _value_ is neither *null* nor *undefined*, then
1. If _value_ is neither *null* nor *undefined*, then
1. If Type(_value_) is not Object, throw a *TypeError* exception.
1. Let _method_ be GetDisposeMethod(_value_, ~sync-dispose~).
1. If _method_ is *undefined*, then
1. If IsCallable(_value_) is *true*, then
1. Perform ? AddDisposableResource(_disposableStack_, *undefined*, ~sync-dispose~, _value_).
1. Else,
1. Throw a *TypeError* exception.
1. Else,
1. Perform ? AddDisposableResource(_disposableStack_, _value_, ~sync-dispose~, _method_).
1. Return _value_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.adopt">
<h1>DisposableStack.prototype.adopt( _value_, _onDispose_ )</h1>
<p>When the `adopt` function is called with two arguments, the following steps are taken:</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. If IsCallable(_onDispose_) is *false*, throw a *TypeError* exception.
1. Let _F_ be a new built-in function object as defined in <emu-xref href="#sec-disposablestack-adopt-callback-functions"></emu-xref>.
1. Set _F_.[[Argument]] to _value_.
1. Set _F_.[[OnDisposeCallback]] to _onDispose_.
1. Perform ? AddDisposableResource(_disposableStack_, *undefined*, ~sync-dispose~, _F_).
1. Return _value_.
</emu-alg>

<emu-clause id="sec-disposablestack-callback-functions">
<h1>DisposableStack Callback Functions</h1>
<p>A <dfn>DisposableStack callback function</dfn> is an anonymous built-in function object that has [[Argument]] and [[OnDisposeCallback]] internal slots.</p>
<p>When a DisposableStack callback function is called, the following steps are taken:</p>
<emu-clause id="sec-disposablestack-adopt-callback-functions">
<h1>DisposableStack Adopt Callback Functions</h1>
<p>A <dfn>DisposableStack adopt callback function</dfn> is an anonymous built-in function object that has [[Argument]] and [[OnDisposeCallback]] internal slots.</p>
<p>When a DisposableStack adopt callback function is called, the following steps are taken:</p>
<emu-alg>
1. Let _F_ be the active function object.
1. Assert: IsCallable(_F_.[[OnDisposeCallback]]) is *true*.
Expand All @@ -3602,6 +3606,19 @@ contributors: Ron Buckton, Ecma International
</emu-clause>
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.defer">
<h1>DisposableStack.prototype.defer( _onDispose_ )</h1>
<p>When the `defer` function is called with one argument, the following steps are taken:</p>
<emu-alg>
1. Let _disposableStack_ be the *this* value.
1. Perform ? RequireInternalSlot(_disposableStack_, [[DisposableState]]).
1. If IsCallable(_onDispose_) is *false*, throw a *TypeError* exception.
1. If _disposableStack_.[[DisposableState]] is ~disposed~, throw a *ReferenceError* exception.
1. Perform ? AddDisposableResource(_disposableStack_, *undefined*, ~sync-dispose~, _onDispose_).
1. Return *undefined*.
</emu-alg>
</emu-clause>

<emu-clause id="sec-disposablestack.prototype.move">
<h1>DisposableStack.prototype.move()</h1>
<p>When the `move` function is called, the following steps are taken:</p>
Expand Down Expand Up @@ -3769,38 +3786,42 @@ contributors: Ron Buckton, Ecma International
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype.use">
<h1>AsyncDisposableStack.prototype.use( _value_ [, _onDisposeAsync_ ] )</h1>
<p>When the `use` function is called with one or two arguments, the following steps are taken:</p>
<emu-note>
<p>The _onDisposeAsync_ argument is optional. If it is not provided, *undefined* is used.</p>
</emu-note>
<h1>AsyncDisposableStack.prototype.use( _value_ )</h1>
<p>When the `use` function is called with one argument, the following steps are taken:</p>
<emu-alg>
1. Let _asyncDisposableStack_ be the *this* value.
1. Perform ? RequireInternalSlot(_asyncDisposableStack_, [[AsyncDisposableState]]).
1. If _asyncDisposableStack_.[[AsyncDisposableState]] is ~disposed~, throw a *ReferenceError* exception.
1. If _onDisposeAsync_ is not *undefined*, then
1. If IsCallable(_onDisposeAsync_) is *false*, throw a *TypeError* exception.
1. Let _F_ be a new built-in function object as defined in <emu-xref href="#sec-asyncdisposablestack-callback-functions"></emu-xref>.
1. Set _F_.[[Argument]] to _value_.
1. Set _F_.[[OnDisposeAsyncCallback]] to _onDisposeAsync_.
1. Perform ? AddDisposableResource(_asyncDisposableStack_, *undefined*, ~async-dispose~, _F_).
1. Else, if _value_ is neither *null* nor *undefined*, then
1. If _value_ is neither *null* nor *undefined*, then
1. If Type(_value_) is not Object, throw a *TypeError* exception.
1. Let _method_ be GetDisposeMethod(_value_, ~async-dispose~).
1. If _method_ is *undefined*, then
1. If IsCallable(_value_) is *true*, then
1. Perform ? AddDisposableResource(_disposableStack_, *undefined*, ~async-dispose~, _value_).
1. Else,
1. Throw a *TypeError* exception.
1. Else,
1. Perform ? AddDisposableResource(_disposableStack_, _value_, ~async-dispose~, _method_).
1. Return _value_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype.adopt">
<h1>AsyncDisposableStack.prototype.adopt( _value_, _onDisposeAsync_ )</h1>
<p>When the `adopt` function is called with two arguments, the following steps are taken:</p>
<emu-alg>
1. Let _asyncDisposableStack_ be the *this* value.
1. Perform ? RequireInternalSlot(_asyncDisposableStack_, [[AsyncDisposableState]]).
1. If _asyncDisposableStack_.[[AsyncDisposableState]] is ~disposed~, throw a *ReferenceError* exception.
1. If IsCallable(_onDisposeAsync_) is *false*, throw a *TypeError* exception.
1. Let _F_ be a new built-in function object as defined in <emu-xref href="#sec-asyncdisposablestack-adopt-callback-functions"></emu-xref>.
1. Set _F_.[[Argument]] to _value_.
1. Set _F_.[[OnDisposeAsyncCallback]] to _onDisposeAsync_.
1. Perform ? AddDisposableResource(_asyncDisposableStack_, *undefined*, ~async-dispose~, _F_).
1. Return _value_.
</emu-alg>

<emu-clause id="sec-asyncdisposablestack-callback-functions">
<h1>AsyncDisposableStack Callback Functions</h1>
<p>An AsyncDisposableStack callback function is an anonymous built-in function that has [[Argument]] and [[OnDisposeAsyncCallback]] internal slots.</p>
<p>When an AsyncDisposableStack callback function is called, the following steps are taken:</p>
<emu-clause id="sec-asyncdisposablestack-adopt-callback-functions">
<h1>AsyncDisposableStack Adopt Callback Functions</h1>
<p>An <dfn>AsyncDisposableStack adopt callback function</dfn> is an anonymous built-in function that has [[Argument]] and [[OnDisposeAsyncCallback]] internal slots.</p>
<p>When an AsyncDisposableStack adopt callback function is called, the following steps are taken:</p>
<emu-alg>
1. Let _F_ be the active function object.
1. Assert: IsCallable(_F_.[[OnDisposeAsyncCallback]]) is *true*.
Expand All @@ -3809,6 +3830,19 @@ contributors: Ron Buckton, Ecma International
</emu-clause>
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype.defer">
<h1>AsyncDisposableStack.prototype.defer( _onDisposeAsync_ )</h1>
<p>When the `defer` function is called with one argument, the following steps are taken:</p>
<emu-alg>
1. Let _asyncDisposableStack_ be the *this* value.
1. Perform ? RequireInternalSlot(_asyncDisposableStack_, [[AsyncDisposableState]]).
1. If _asyncDisposableStack_.[[AsyncDisposableState]] is ~disposed~, throw a *ReferenceError* exception.
1. If IsCallable(_onDisposeAsync_) is *false*, throw a *TypeError* exception.
1. Perform ? AddDisposableResource(_asyncDisposableStack_, *undefined*, ~async-dispose~, _onDisposeAsync_).
1. Return *undefined*.
</emu-alg>
</emu-clause>

<emu-clause id="sec-asyncdisposablestack.prototype.move">
<h1>AsyncDisposableStack.prototype.move()</h1>
<p>When the `move` function is called, the following steps are taken:</p>
Expand Down

0 comments on commit 2df6c9e

Please sign in to comment.