Skip to content

Function: asCleanup()

API / @xmachines/play-dom-router / asCleanup

function asCleanup<R>(release): Cleanup;

Defined in: play/src/disposable.ts:149

Makes one release function serve as a Cleanup.

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 this protocol existed.

The release therefore 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 already accepts that, because a teardown that runs two times is older than this protocol.

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. Nothing in this workspace releases asynchronously. An AsyncCleanup can arrive the day something does.

The R extends void | undefined bound is what holds that line. A plain () => void parameter would ACCEPT async () => {}, because TypeScript lets a function that returns a value stand where one returning void is wanted. The promise would then go on the floor: the scope releases, the release looks finished, and the work runs later with nobody able to await it. The bound rejects that at the call, and it rejects a release that returns any other value as well.

The bound holds where the compiler knows the return type. It cannot hold where the caller erased it: R infers any for a release typed any, such as a bare vi.fn(), and any satisfies every constraint. A release declared () => void whose body is async passes for the same reason — the declaration, and not the body, is what the call site sees. Write the release inline, or type it, and the bound applies.

The release lands under DISPOSE and, when the two differ, under the Symbol.dispose that exists at the CALL. DISPOSE resolves when the module evaluates, so a polyfill that installs the well-known symbol later would otherwise leave the release on the registered key while a downlevelled using reads the well-known one. Writing both closes that window for every release this function publishes.

Type Parameters

Type Parameter
R extends void | undefined

Parameters

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

Returns

Cleanup

The same function, now also a Disposable.

Throws

TypeError - When the release is frozen or is otherwise not extensible. The protocol adds a property, and a sealed function accepts none.

Example

export function watchAtom<T>(atom: Readable<T>, onValue: (v: T) => void): Cleanup {
const subscription = atom.subscribe(() => onValue(atom.get()));
return asCleanup(() => subscription.unsubscribe());
}