js/context
js/context/index.ts
fino:context - Async context propagation.
A Context is a named slot whose value follows the causal chain of async
execution through promise continuations, including await, .then(),
queueMicrotask(), timers implemented on the runtime loop, and promise-based
runtime APIs such as file I/O. It works by hooking into V8's
ContinuationPreservedEmbedderData (CPED): when a promise continuation is
enqueued, the current frame is captured; when the continuation runs, the
frame is restored. Because the frame array is copied on every write, values
captured by already-enqueued continuations are never mutated retroactively -
each async branch sees exactly the values that were active when it was
scheduled.
Synchronous dispatch and callback re-entry observe whatever context is active
during the dispatch/callback call (e.g. an EventTarget listener sees the
context of the dispatchEvent() caller). External or native schedulers that
do not enqueue through these runtime paths are not promised to preserve
context unless they are explicitly covered by tests or the callback is
manually wrapped with a Snapshot.
The API supports both callback and disposable scopes. runWithValue() scopes
a value to a callback and restores the previous frame afterwards, similar to
Node's AsyncLocalStorage.run(). withValue() returns a disposable scope for
using blocks. enterWith()/exit() remain the unscoped escape hatch, and
Snapshot replays a frame across schedulers that do not propagate. For
context-bound publish/subscribe, see fino:context/topic.
import { Context } from 'fino:context';
const requestId = new Context<string>('requestId');
{
using scope = requestId.withValue('abc-123');
await someAsyncOp();
console.log(requestId.get()); // 'abc-123' - propagated through await
}
Interfaces
interface ContextScope {
Disposable context scope returned by Context.withValue() and
Context.withClear().
Disposing the scope restores the complete context frame captured before the
scope was entered. using declarations dispose in reverse order, so nested
scopes restore naturally.
import { Context } from 'fino:context';
const request = new Context<string>('request');
{
using scope = request.withValue('req-1');
console.log(request.get());
}
Classes
class Context<T = unknown> {
Async-local context slot whose value follows promise continuations.
Each Context instance owns one independent slot. Values are scoped to the
current async execution frame and are restored after runWithValue() or
runClear() completes, even when the callback throws. Values are not shared
across unrelated realms or processes.
import { Context } from 'fino:context';
const requestId = new Context<string>('requestId');
await requestId.runWithValue('req-1', async () => {
await Promise.resolve();
console.log(requestId.get());
});
Constructors
constructor(name: string)
Create a context slot with a stable debug name.
The name is informational and does not affect lookup identity. Two
contexts with the same name remain separate slots. The constructor does not
install a value; get() returns undefined until a scope is entered.
import { Context } from 'fino:context';
const tenant = new Context<string>('tenant');
console.log(tenant.name);
Getters
get name()
Read the debug name supplied to the constructor.
The value is read-only and never falls back to a generated name. It is safe to use in logs, but it is not a unique key because callers may create multiple contexts with the same name.
import { Context } from 'fino:context';
const ctx = new Context('request');
console.log(ctx.name);
Methods
get(): T | undefined
Return the current value of this context slot, or undefined if no value
has been set (or if runClear is active) in the current async scope.
The lookup is synchronous and allocation-free in the common case. A stored
value of undefined is indistinguishable from an unset slot, so use a
sentinel object if that distinction matters.
import { Context } from 'fino:context';
const locale = new Context<string>('locale');
console.log(locale.get());
locale.runWithValue('en-US', () => console.log(locale.get()));
runWithValue<R>(value: T, fn: () => R): R
Run fn with this context slot set to value. The previous value is
restored when fn returns or throws, preserving proper scope nesting.
Works for both sync and async functions. When fn is async, all
continuations within the returned promise will see value.
If fn throws, the previous frame is restored before the error propagates.
The returned value has exactly the same shape as fn's return value; async
callbacks therefore return their original Promise.
import { Context } from 'fino:context';
const trace = new Context<string>('trace');
const result = await trace.runWithValue('abc', async () => {
await Promise.resolve();
return trace.get();
});
withValue(value: T): ContextScope
Enter a disposable scope with this context slot set to value.
Use this with a using declaration when the scope is naturally represented
by a block instead of a callback. Disposing the returned scope restores the
complete context frame that was active before withValue() was called.
Promise continuations scheduled while the scope is active inherit value.
import { Context } from 'fino:context';
const trace = new Context<string>('trace');
{
using scope = trace.withValue('abc');
await Promise.resolve();
console.log(trace.get());
}
runClear<R>(fn: () => R): R
Run fn with this context slot explicitly cleared, making get() return
undefined for the duration of fn even if an outer scope has a value.
The previous value is restored afterwards.
Use this when calling code that must not inherit sensitive request state.
As with runWithValue(), thrown errors propagate after restoration and
async continuations created inside fn observe the cleared slot.
import { Context } from 'fino:context';
const auth = new Context<string>('auth');
auth.runWithValue('token', () => {
auth.runClear(() => console.log(auth.get()));
});
withClear(): ContextScope
Enter a disposable scope with this context slot cleared.
Use this when a block must not inherit an outer value. Disposing the scope
restores the complete context frame that was active before withClear() was
called.
import { Context } from 'fino:context';
const auth = new Context<string>('auth');
auth.runWithValue('token', () => {
using scope = auth.withClear();
console.log(auth.get());
});
enterWith(value: T): void
Unconditionally set this slot to value in the current async execution
scope without scheduling a restore. All future microtasks enqueued from
this point - and their continuations - will inherit the value.
Use sparingly. Unlike runWithValue, there is no automatic cleanup:
the caller is responsible for calling exit() when appropriate.
Analogous to Node.js AsyncLocalStorage.enterWith().
Prefer runWithValue() for bounded scopes. enterWith() is useful for
event-loop integration points that need to set state before scheduling
callbacks and then clear it explicitly.
import { Context } from 'fino:context';
const request = new Context<string>('request');
request.enterWith('req-42');
queueMicrotask(() => console.log(request.get()));
request.exit();
exit(): void
Clear this slot in the current async execution scope.
Code running synchronously after this call will see undefined.
Previously-enqueued microtasks keep their captured value unchanged.
exit() only clears this context's slot, not other Context instances.
It does not throw when the slot is already empty.
import { Context } from 'fino:context';
const ctx = new Context('request');
ctx.enterWith('req-1');
ctx.exit();
console.log(ctx.get());
snapshot()
Capture the current frame as a Snapshot that can be re-entered later -
useful when manually scheduling callbacks outside the normal async flow
(e.g. passing callbacks to third-party queues or custom event emitters).
The snapshot captures all context slots, not just this instance. Re-enter
it with Snapshot.runWithValue() around callbacks whose scheduler does
not preserve promise continuation state.
import { Context } from 'fino:context';
const ctx = new Context<string>('request');
const saved = ctx.runWithValue('req-1', () => ctx.snapshot());
saved.runWithValue(() => console.log(ctx.get()));
class Snapshot {
A frozen capture of the full context frame at a point in time, across all
Context instances. Can be re-entered later via runWithValue().
Obtain via context.snapshot() or the module-level snapshotAll().
import { Context, snapshotAll } from 'fino:context';
const ctx = new Context<string>('request');
const snap = ctx.runWithValue('req-1', () => snapshotAll());
snap.runWithValue(() => console.log(ctx.get()));
Constructors
constructor(handle: unknown)
Create a snapshot wrapper around an opaque runtime context frame.
Application code normally obtains snapshots through Context.snapshot()
or snapshotAll(). The handle is intentionally opaque; passing an invalid
value can restore an unusable frame.
import { snapshotAll } from 'fino:context';
const snap = snapshotAll();
snap.runWithValue(() => console.log('restored'));
Methods
runWithValue<R>(fn: () => R): R
Run fn inside the snapshotted frame. The previous frame is restored
afterwards regardless of whether fn throws.
The return value is the exact return value from fn. If fn schedules
promise continuations, those continuations inherit the snapshotted frame.
import { Context, snapshotAll } from 'fino:context';
const ctx = new Context<string>('trace');
const snap = ctx.runWithValue('abc', () => snapshotAll());
await snap.runWithValue(async () => ctx.get());
Functions
function snapshotAll(): Snapshot
Capture all current context slot values as a Snapshot. Useful at the
point where a callback is registered, so it can be replayed with the full
context frame intact when the callback is invoked later.
The returned object is independent of later enterWith() or exit() calls.
It may be reused for multiple callbacks.
import { Context, snapshotAll } from 'fino:context';
const ctx = new Context<string>('tenant');
const snap = ctx.runWithValue('acme', () => snapshotAll());
setTimeout(() => snap.runWithValue(() => console.log(ctx.get())), 0);