This package provides an API inspired from the Explicit Resource Management TC39 proposal, leveraging the existing JavaScript semantics of for-of
and for-await-of
. It provides the same convenience of automatically disposing of resources when exiting a block, without requiring new syntax.
import { Disposable } from "disposator";
for (const { using } of Disposable) {
const resource = using(getResource());
resource.doSomething();
const other = using(resource.getOther());
const stuff = other.doStuff();
using(() => cleanUpStuff(stuff));
} // automatically cleanup, even when something throws
This package can be used either as a ponyfill through the default entrypoint, or as a polyfill modifying the global by using the /polyfill.js
import path.
An object is disposable if has a @@dispose
symbol method that performs explicit cleanup. The symbol is exported as symbolDispose
by this package, or installed as Symbol.dispose
through the polyfill.
import { symbolDispose } from "disposator";
interface Disposable {
/**
* Disposes resources within this object.
*/
[symbolDispose](): void;
}
An object is async disposable if it has a @@asyncDispose
symbol async method that performs explicit cleanup. The symbol is exported as symbolAsyncDispose
by this package, or installed as Symbol.asyncDispose
through the polyfill.
import { symbolAsyncDispose } from "disposator";
interface AsyncDisposable {
/**
* Disposes resources within this object.
*/
[symbolAsyncDispose](): Promise<void>;
}
A function can be used in places where a disposable is expected. In that case the provided dispose function will simply be called, with no this
context.
type OnDispose = () => void;
An async function can be used in places where an async disposable is expected. In that case the provided dispose function will simply be called, with no this
context, and the result will be awaited.
type OnAsyncDispose = () => void | PromiseLike<void>;
The package provides classes implementing the Disposable
and AsyncDisposable
interfaces allowing to wrap one or aggregate multiple disposable like or async disposable like resources. The resources can be added at construction and/or later using the aggregate object's using
helper. The aggregated resources are disposed of in reverse order. If multiple resources are aggregated, any error thrown during their disposal are aggregated, and an AggregateError
is thrown once all tracked resources have been disposed of. If any error occurs during construction (such as adding an invalid resource), all resources added are automatically disposed of.
Initial aggregated resources are added eagerly, either as multiple values passed to the constructor, or through an iterable provided to the static from
method. The latter is preferred to create an aggregated object from initial resources, especially in the case of AsyncDisposable
. With static from
, resources are added for tracking as soon as they are iterated over, and any disposal error is merged with errors that triggered the construction-time disposal. Additionally the from
helper optionally takes a mapping function similar to Array.from()
allowing to reactively create during iteration a disposable like or async disposable like resource from any iterated value.
If the resources to aggregate are not all available at the same time, they can be added later with the using
helper. If only a single resource needs to be in use while iterating over a collection, the usingFrom
helper streamlines an acquire-use-dispose iteration.
type DisposableResource = Disposable | OnAsyncDispose;
interface AggregateDisposableConstructor {
/**
* Creates a disposable object aggregating the given disposable resources
*
* @param disposables An iterable containing resources to be disposed of
* when the returned object is itself disposed of
*/
from(disposables: Iterable<DisposableResource>): AggregateDisposable;
/**
* Creates a disposable object aggregating the given disposable resources
*
* @param values An iterable containing values for which the mapped
* resource will be disposed of when the returned object is itself disposed of
* @param mapFn A function returning a disposable resource from the
* iterated value
*/
from<T>(
values: Iterable<T>,
mapFn: (value: T) => DisposableResource
): AggregateDisposable;
/**
* Creates an aggregate disposable object
*
* @param args Initial resources to add for tracking
*/
new (...args: DisposableResource[]): AggregateDisposable;
}
export const Disposable: AggregateDisposableConstructor;
Disposable.from()
consumes any Iterable
. The optional mapFn
will be called for each iterated value and must return a disposable like resource.
new Disposable()
accepts zero or any number of disposable like resources.
type AsyncDisposableResource = AsyncDisposable | Disposable | OnDispose;
interface AggregateAsyncDisposableConstructor {
/**
* Creates an async disposable object aggregating the given disposable or
* async disposable resources
*
* @param disposables An iterable or async iterable containing resources to
* be disposed of when the returned object is itself disposed of
*/
from(
disposables:
| Iterable<AsyncDisposableResource>
| AsyncIterable<AsyncDisposableResource>
): Promise<AggregateAsyncDisposable>;
/**
* Creates an async disposable object aggregating the given disposable or
* async disposable resources
*
* @param values An iterable or async iterable containing values for which
* the mapped resource will be disposed of when the returned object is itself
* disposed of
* @param mapFn A function returning a disposable or async disposable
* resource from the iterated value
*/
from<T>(
values: Iterable<T> | AsyncIterable<T>,
mapFn: (value: T) => AsyncDisposableResource
): Promise<AggregateAsyncDisposable>;
/**
* Creates an aggregate async disposable object
*
* Note: Prefer AsyncDisposable.from()
* Any error adding the initial tracked resources may result in an unhandled
* rejection resulting from the automatic disposal of the added resources
*
* @param args Initial resources to add for tracking
*/
new (...args: AsyncDisposableResource[]): AggregateAsyncDisposable;
}
export const AsyncDisposable: AggregateAsyncDisposableConstructor;
AsyncDisposable.from()
consumes any Iterable
or AsyncIterable
. The optional mapFn
will be called for each iterated value and must return a disposable like or async iterable like resource.
new AsyncDisposable()
accepts zero or any number of disposable like or async disposable like resources. If an error occurs while adding a resource for tracking, the resources added will be disposed and construction will throw. However any error occurring during this disposal will result in an unhandled rejection. To handle this case, prefer AsyncDisposable.from()
.
The AggregateDisposable
and AggregateAsyncDisposable
objects expose a using
helper on their instance which can be used to add resources for tracking after construction of the aggregate object. The using
helper can be detached from the aggregate object (it's bound at construction). It passes through its value for chaining, or assignment at acquisition time. using
accepts a dispose callback function as an optional argument. This can be used to implement disposal for values which do not implement the disposables interfaces. In that case the value is passed as this
context to the dispose callback
interface AggregateDisposable extends Disposable {
/**
* Helper adding new resources to track in the aggregated disposable.
* The helper can be detached
*/
readonly using: AggregateDisposableUsing;
}
/**
* Add a disposable resource for tracking
*/
interface AggregateDisposableUsing {
/**
* @param disposable The disposable resource to track
* @returns The disposable resource
*/
<T extends DisposableResource>(disposable: T): T;
/**
* @param value A value to consider as a resource to dispose
* @param onDispose The dispose callback invoked with the value
* as `this` context
* @returns The value
*/
<T>(value: T, onDispose: OnDispose): T;
}
The Disposable
's using
helper function can be used to track any disposable like resource. It captures the disposable and its dispose method, or the dispose callback, then passes through the value. Additionally using
can be called with an onDispose
callback as second argument, which will be called with the value as this
context. When the aggregate object is disposed of, the tracked resources are disposed of in reverse order to which they were added.
interface AggregateAsyncDisposable extends AsyncDisposable {
/**
* Helper adding new resources to track in the aggregated async disposable.
* The helper can be detached
*/
readonly using: AggregateAsyncDisposableUsing;
}
/**
* Add a disposable or async disposable resource for tracking
*/
interface AggregateAsyncDisposableUsing {
/**
* @param disposable The disposable or async disposable resource to track
* @returns The disposable or async disposable resource
*/
<T extends AsyncDisposableResource>(disposable: T): T;
/**
* @param value A value to consider as a resource to dispose
* @param onDispose The async dispose callback invoked with the value
* as `this` context
* @returns The value
*/
<T>(value: T, onDispose: OnAsyncDispose): T;
}
The Disposable
's using
helper function can be used to track any disposable like resource. It captures the disposable and its dispose method, or the dispose callback, then passes through the value. Additionally using
can be called with an onDispose
callback as second argument, which will be called with the value as this
context. When the aggregate object is disposed of, the tracked resources are disposed of in reverse order to which they were added.
The AsyncDisposable
's using
helper function can be used to track any disposable or async disposable like resource. It captures the disposable and its dispose method, the async disposable and its async dispose method, or the async dispose callback, then passes through the value. Additionally using
can be called with an onDispose
async callback as second argument, which will be called with the value as this
context. When the aggregate async object is disposed of, the tracked resources are disposed of in reverse order to which they were added.
The disposal of an async disposable like resource is awaited before moving to the next resource. The disposal of a disposable resource is not awaited. The aggregate disposal step is always awaited even if all tracked resources are disposable which are disposed of synchronously.
The Disposable
and AsyncDisposable
exports both implement a special iterator helper which streamlines creating an aggregated resource object and disposing of resources added for tracking. While these iterators only ever yield a single value (the aggregate object), they are meant to be used with respectively the for-of
and for-await-of
statements which automatically closes their iterator in case of an early return or thrown error. The iterator closure triggers the disposal of the aggregate object and the resources it tracks.
Combined with the detachable using
helper of the aggregate object, it allows seamlessly tracking multiple disposable like or async disposable like resources and ensuring that they are properly disposed of when exiting a scope block, without dealing directly with the aggregate object itself.
If the disposal of the aggregate resource was triggered by an error thrown during the evaluation of the for-of
or for-await-of
block, that error takes precedence and errors occurring during the disposal are ignored. This is unlike try-finally
statements where an error during the finally
block takes precedence over the try
block.
interface AggregateDisposableConstructor {
/**
* Returns an iterator which yields a new aggregate instance. Its `using`
* helper can be used to track disposable resources which will be disposed
* of when the iterator is closed. Use with a `for-of` statement to perform
* RAII style explicit resource management
*/
[Symbol.iterator](): Iterator<AggregateDisposable, void, void>;
}
When the iterator closes, either from an early return, thrown error, or once the block completes, the aggregate object disposes of its tracked resources in reverse order to which they were added.
interface AggregateAsyncDisposableConstructor {
/**
* Returns an iterator which yields a new async aggregate instance. Its `using`
* helper can be used to track disposable or async disposable resources
* which will be disposed of when the iterator is closed. Use with a
* `for-await-of` statement to perform RAII style explicit resource management
*/
[Symbol.asyncIterator](): AsyncIterator<AggregateAsyncDisposable, void, void>;
}
When the iterator closes, either from an early return, thrown error, or once the block completes, the async aggregate object disposes of its tracked resources in reverse order to which they were added.
The following show examples of using the iterator helper with various APIs, assuming those APIs implement the disposable or async disposable interfaces.
WHATWG Streams Reader API
for await (const { using } of AsyncDisposable) {
const reader = using(stream.getReader());
const { value, done } = await reader.read();
}
NodeJS FileHandle
for await (const { using } of AsyncDisposable) {
const f1 = using(await fs.promises.open(s1, constants.O_RDONLY)),
const f2 = using(await fs.promises.open(s2, constants.O_WRONLY));
const buffer = Buffer.alloc(4092);
const { bytesRead } = await f1.read(buffer);
await f2.write(buffer, 0, bytesRead);
} // both handles are closed
Transactional Consistency (ACID)
// roll back transaction if either action fails
for await (const { using } of AsyncDisposable) {
const tx = using(transactionManager.startTransaction(account1, account2));
await account1.debit(amount);
await account2.credit(amount);
// mark transaction success
tx.succeeded = true;
} // transaction is committed
Logging and tracing
// audit privileged function call entry and exit
function privilegedActivity() {
for (const { using } of Disposable) {
using(auditLog.startActivity("privilegedActivity")); // log activity start
...
} // log activity end
}
Async Coordination
import { Semaphore } from "...";
const sem = new Semaphore(1); // allow one participant at a time
export async function tryUpdate(record) {
for (const { using } of Disposable) {
using(await sem.wait()); // asynchronously block until we are the sole participant
...
} // synchronously release semaphore and notify the next participant
}
The following show examples of integrating with API which do not implement the Disposable
or AsyncDisposable
interface**
Working with existing resources
for await (const { using } of AsyncDisposable) {
const reader = ...;
using(() => reader.releaseLock());
...
}
Schedule other cleanup work to evaluate at the end of the block similar to Go's defer
statement
for (const { using } of Disposable) {
console.log("enter");
using(() => console.log("exit"));
...
}
The Disposable.usingFrom()
and AsyncDisposable.usingFrom()
helpers streamline iterating over resources, ensuring that each iterated resource is disposed of before acquiring the next resource. They do not dispose of resources that are not iterated over, e.g. if the iteration is terminated early.
The helpers works by generating a new iterable which captures a provided iterable, and an optional mapFn
function. When an iterator is subsequently requested, the captured iterable's iterator is requested and wrapped. For each iteration, the iterator requests the next value from the wrapped iterator, tracks the resource, then yields the value. The optional mapFn
function can be used to generate a disposable or async disposable resource from the iterated value.
After each iteration step, the resource is disposed of, regardless of how the step ended. When iterated through a for-of
or for-await-of
, an error thrown in the statement's block will take precedence and hide any error thrown during the disposal of the resource.
interface AggregateDisposableConstructor {
/**
* Wraps an iterable to ensure that iterated resources are disposed of
*
* @param disposables An iterable containing disposable resources over
* which to iterate then dispose
*/
usingFrom<T extends DisposableResource>(
disposables: Iterable<T>
): Iterable<T>;
/**
* Wraps an iterable to ensure that iterated resources are disposed of
*
* @param values An iterable containing values over which to iterate
* @param mapFn A function returning a disposable resource from the
* iterated value
*/
usingFrom<T>(
values: Iterable<T>,
mapFn: (value: T) => DisposableResource
): Iterable<T>;
}
Disposable.usingFrom()
can wrap any Iterable
. The optional mapFn
will be called for each iterated value and must return a disposable like resource.
interface AggregateAsyncDisposableConstructor {
/**
* Wraps an iterable or async iterable to ensure that iterated resources are
* disposed of
*
* @param disposables An iterable or async iterable containing disposable
* or async disposable resources over which to iterate then dispose
*/
usingFrom<T extends AsyncDisposableResource>(
disposables: Iterable<T> | AsyncIterable<T>
): AsyncIterable<T>;
/**
* Wraps an iterable or async iterable to ensure that iterated resources are
* disposed of
*
* @param values An iterable or async iterable containing values over which
* to iterate
* @param mapFn A function returning a disposable or async disposable
* resource from the iterated value
*/
usingFrom<T>(
values: Iterable<T> | AsyncIterable<T>,
mapFn: (value: T) => AsyncDisposableResource
): AsyncIterable<T>;
}
AsyncDisposable.usingFrom()
can wrap any Iterable
or AsyncIterable
. The optional mapFn
will be called for each iterated value and must return a disposable or async disposable like resource.
for (const res of Disposable.usingFrom(iterateResources())) {
// use res
}
for (const value of Disposable.usingFrom(
values,
(value) => () => cleanup(value)
)) {
// use value
}
for await (const res of AsyncDisposable.usingFrom(iterateAsyncResources())) {
// use res
}
for await (const res of AsyncDisposable.usingFrom(asyncIterateResources())) {
// use res
}