Skip to content

Function: asDisposable()

API / @xmachines/core / utils / asDisposable

Call Signature

function asDisposable<F>(release): F & DisposeMember;

Defined in: packages/core/src/utils/disposable.ts:126

Makes one release function a Disposable. The function is its own release.

The function comes back as itself, with DISPOSE added. Nothing wraps it, so the identity of the release holds: a caller that compares two releases, or that puts one in a Set, sees what it saw before. A caller that runs the release keeps that code, and a caller inside one scope writes using:

// The explicit form.
const stop = watchAtom(count, render);
stop();
// The scoped form. The scope releases it, and an exception releases it too.
{
using stop = asDisposable(watchAtom(count, render));
render(count.get());
}

A value that is a Disposable already comes back unchanged. The function adds the key only where it is absent, so a second call changes nothing. The one exception is a well-known key that a late polyfill installed: the second call stores the release there.

The release must be idempotent. A scope releases the value one time, but a caller that runs the function AND lets the scope end runs it two times. Every release of this workspace accepts that.

The release must be synchronous. Disposable and not AsyncDisposable: a Disposable serves using AND await using, because await using falls back to Symbol.dispose, while an AsyncDisposable raises a TypeError under a plain using and forces every holder into an async function. The void | undefined bound rejects an async release: a plain () => void bound accepts async () => {}, because TypeScript lets a function that returns a value stand where one that returns void is wanted. The scope would then ignore the promise, and nothing could await the work.

The bound holds where the compiler knows the return type. It cannot hold where the caller erased it: a release typed any, such as a bare vi.fn(), satisfies every constraint. Write the release inline, or type it, and the bound applies.

using needs Node 24 or a browser with Explicit Resource Management, and it needs "lib": ["ESNext"] of the consumer. See DisposeKey for a consumer without it.

Type Parameters

Type Parameter
F extends () => void | undefined

Parameters

ParameterTypeDescription
releaseFThe release to publish. It must return nothing. The function is changed in place, so pass a function that this caller owns: a shared or module-level function becomes a Disposable for every other holder of it.

Returns

F & DisposeMember

The same function, now also a Disposable.

Throws

TypeError - When the function is frozen or is otherwise not extensible, and a dispose key holds no member yet.

See

Play RFC

Call Signature

function asDisposable<T, R>(target, release): T & DisposeMember;

Defined in: packages/core/src/utils/disposable.ts:157

Makes one object a Disposable, with a release that the caller names.

The object comes back as itself, with DISPOSE added, so a caller that compares two objects sees what it saw before.

An object that is a Disposable already, with a dispose member own or inherited, makes the call throw. The scope would run that member and never the release that the caller names, so a teardown would go missing without a sign. A second call with the SAME release is not a conflict: it returns the object, and it writes only a well-known key that a late polyfill left empty.

The rules of the function form apply to the release: it must be idempotent, and it must be synchronous.

Type Parameters

Type Parameter
T extends object
R extends void | undefined

Parameters

ParameterTypeDescription
targetTThe object to publish. It is changed in place, so pass an object that this release owns.
release() => RThe release that the scope runs. It must return nothing.

Returns

T & DisposeMember

The same object, now also a Disposable.

Throws

DisposeConflictError - When the object has a dispose member already, and that member is not the named release.

Throws

TypeError - When the object is frozen or is otherwise not extensible, and a dispose key does not hold the release yet.

Example

const subscription = selection.subscribe(render);
using disposable = asDisposable(subscription, () => subscription.unsubscribe());