Allow AsyncDisposableStack to also add synchronous items
So it's possible to have resources that define both [Symbol.dispose] and [Symbol.disposeAsync]:
const fileResource = {
[Symbol.dispose]: () => { fs.closeSync(fd); },
[Symbol.disposeAsync]: async () => {
return await new Promise((resolve) => {
fs.close(fd, () => resolve());
});
}
}
Within a block we could use either using or await using to pick which disposal method to use:
{
using fileResourceSync = makeFileResource();
await using fileResourceAsync = makeFileResource();
}
However under the semantics of asyncDisposableStack.use, it will always use [Symbol.disposeAsync] regardless of which we might wish to use.
Furthermore (although less important IMO) is the fact that even if [Symbol.disposeAsync] is not on the resource, a microtask will occur between successive sync cleanups, which could be observed:
asyncDisposableStack.use({
[Symbol.dispose]: () => {
console.log("Cleanup res1");
Promise.resolve().then(() => console.log("Before cleanup res2"));
},
});
asyncDisposableStack.use({
[Symbol.dispose]: () => {
console.log("Cleanup res2");
},
});
I'd like to propose that we have an additional .useSync method on AsyncDisposableStack so that we choose to use [Symbol.dispose] even if [Symbol.disposeAsync] for the resource were available.
(NOTE: I am not proposing that asyncDisposableStack.dispose() changes in any-way, this would still unconditionally return a promise).
This would be a fairly minimal addition, the spec text is pretty much the same as .use just with the other hint:
AsyncDisposableStack.prototype.use( value )
When the use function is called with one argument, the following steps are taken:
1. Let asyncDisposableStack be the this value.
2. Perform ? [RequireInternalSlot](https://tc39.es/ecma262/#sec-requireinternalslot)(asyncDisposableStack, [[AsyncDisposableState]]).
3. If asyncDisposableStack.[[AsyncDisposableState]] is disposed, throw a ReferenceError exception.
4. Perform ? [AddDisposableResource](https://tc39.es/proposal-async-explicit-resource-management/#sec-adddisposableresource-disposable-v-hint-disposemethod)(asyncDisposableStack.[[DisposeCapability]], value, sync-dispose).
5. Return value.
The specified behavior matches that of Symbol.asyncIterator vs. Symbol.iterator. It's generally bad practice to specify both, and if you do, the async version will always be used for any native async APIs.
If you want multiple synchronous resources to be grouped together in an AsyncDisposableStack, you can accomplish this by adding those grouped resources to a single DisposableStack first:
await using asyncStack = new AsyncDisposableStack();
const a = asyncStack.use(getAsyncResourceA());
const syncStack = asyncStack.use(new DisposableStack());
const b = syncStack.use(getSyncResourceB());
const c = syncStack.use(getSyncResourceC());
const d = asyncStack.use(getAsyncResourceD());
// disposes: d, (c, b), a