The missing utilities for JavaScript's Explicit Resource Management (using and await using declarations).
- 🧹 Resource Management - Convert any resource into a disposable with
toDisposableandtoAsyncDisposable - 🎧 Event Handlers - Transform event emitters into disposable iterators with
onand promises withonce - ⏱️ Timing Utilities - Disposable
timeoutandintervalfor clean timer management - 📡 Channels - Build async iterators from push-based sources with
channel - 🔒 Scoped Execution - Execute code with automatic cleanup using
invoke - 📦 Zero Dependencies - Lightweight and focused
- 🔧 TypeScript First - Full type safety and inference
- 🌳 Tree Shakeable - Import only what you need
npm install indisposed
# or
yarn add indisposed
# or
pnpm add indisposedimport { toAsyncDisposable, once, invoke } from "indisposed";
import { WebSocketServer } from "ws";
// Automatic cleanup with await using
{
await using wss = await invoke(async () => {
const wss = toAsyncDisposable(
new WebSocketServer({ host: "127.0.0.1", port: 0 }),
(wss) =>
new Promise((resolve, reject) => {
wss.close((err) => {
if (err) return reject(err);
resolve(undefined);
});
}),
);
using listening = once(wss, "listening");
using error = once(wss, "error", true);
await Promise.race([listening, error]);
return wss;
});
console.log("Server ready at", wss.address());
// ... handle connections ...
}
// wss is automatically closed once the scope endsMake any object disposable by adding a Symbol.dispose method.
import { toDisposable } from "indisposed";
const resource = toDisposable({ handle: 123 }, (r) =>
console.log(`Closing handle ${r.handle}`),
);
{
using r = resource;
// use resource
} // automatically disposed hereMake any object async disposable by adding a Symbol.asyncDispose method.
import { toAsyncDisposable } from "indisposed";
const resource = toAsyncDisposable(
{ connection: db },
async (r) => await r.connection.close(),
);
{
await using r = resource;
// use resource
} // automatically disposed hereCreate a disposable promise that resolves/rejects when an event fires once.
The promise result is automatically unpacked based on the handler signature:
- 0 parameters →
undefined - 1 parameter → the single value
- 2+ parameters → array of values
import { once } from "indisposed";
{
using promise = once(server, "listening");
await promise; // waits for 'listening' event
} // removes listener if not yet fired
// Single parameter events return the value directly
{
using data = once(socket, "message");
const message = await data; // string (not [string])
}
// Multiple parameters return as array
{
using result = once(emitter, "result");
const [status, data] = await result; // [number, object]
}
// Handle errors
{
using error = once(server, "error", true);
await error; // rejects if 'error' event fires
}Create a disposable async iterator for multiple events.
Yielded values are automatically unpacked based on the handler signature:
- 0 parameters →
undefined - 1 parameter → the single value
- 2+ parameters → array of values
import { on } from "indisposed";
// Single parameter events yield values directly
{
using events = on(emitter, "data");
for await (const data of events) {
console.log(data); // string (not [string])
if (shouldStop) break;
}
} // automatically removes listener
// Multiple parameters yield as array
{
using positions = on(emitter, "move");
for await (const [x, y] of positions) {
console.log(`Position: ${x}, ${y}`);
}
}
// With buffer options
{
using events = on(emitter, "data", { maxBuffer: 10, drain: true });
// ...
}Create a disposable promise that resolves after a delay.
import { timeout } from "indisposed";
// Basic usage
await timeout(1000);
console.log("1 second passed");
// With using - automatically clears timeout when scope exits
{
using timer = timeout(5000);
await timer;
} // timeout cleared if scope exits early
// Racing with other promises
{
using timer = timeout(10000);
using data = once(socket, "data");
await Promise.race([timer, data]);
} // both cleaned up regardless of which winsCreate a disposable async iterator that yields incrementing numbers at a fixed interval.
import { interval } from "indisposed";
// Basic usage - tick every second
{
using ticks = interval(1000);
for await (const tick of ticks) {
console.log(`Tick ${tick}`); // 0, 1, 2, ...
if (tick >= 5) break;
}
} // interval automatically cleared
// Polling pattern
{
using poll = interval(5000);
for await (const _ of poll) {
const status = await checkStatus();
if (status === "complete") break;
}
}
// With options
const ticks = interval(100, { maxBuffer: 10, drain: true });Create a buffered async channel for pushing values and consuming them via async iteration.
This is a low-level primitive for building async iterators from push-based sources.
The channel separates producer (push) and consumer (iterator) concerns - only expose
the iterator to downstream code.
Options:
maxBuffer- Maximum events to buffer (default: 100). Set to 0 for no buffering.drain- Whether to drain buffered events on dispose (default: false)
import { channel } from "indisposed";
// Basic usage - producer keeps the channel, consumer gets the iterator
const ch = channel<string>();
// Producer side
ch.push("hello");
ch.push("world");
// Consumer side - only sees the iterator
{
using iter = ch.iterator;
for await (const value of iter) {
console.log(value);
if (shouldStop) break;
}
}
// Building a custom async source
function createDataStream() {
const ch = channel<Data>();
source.on("data", (d) => ch.push(d));
return ch.iterator; // Only expose the iterator
}Immediately invoke a function - useful for scoping resources.
import { invoke } from "indisposed";
const result = invoke(() => {
using resource1 = getResource1();
using resource2 = getResource2();
return processResources(resource1, resource2);
}); // resources disposed in reverse orderBy default, indisposed automatically polyfills Symbol.dispose and Symbol.asyncDispose when:
- The environment doesn't natively support these symbols
core-jsis installed as a peer dependency
The polyfill is smart and only applies when needed. If your environment already supports explicit resource management or you don't have core-js installed, no polyfill is loaded.
For environments that already support these symbols, you can skip the polyfill check entirely using the no-polyfill entry point:
import { toDisposable } from "indisposed/no-polyfill";Or configure your bundler to alias the main export:
{
"alias": {
"indisposed": "indisposed/no-polyfill"
}
}- Node.js >= 22.20.0
- TypeScript >= 5.2.0 (for
usingdeclarations support)
MIT © andrew-w-ross
Contributions are welcome! Please feel free to submit a Pull Request.