Skip to main content

Managing Context

When making calls in to another place or time, you often need to maintain context: information from the call site that must be available for the callee to accomplish its work.

Lumenize uses a Dual-Context Strategy to handle this:

  1. Call Context (this.lmz.callContext): For information about the caller/callee including mesh node type and auth as well as custom annotations (callContext.state) added by hooks, like the onBeforeCall auth hook, to assist with later access control decisions, like made in the @mesh() handler.
  2. Application Context (Continuations): For your business logic and data flow that only the continuation needs.

1. Call Context (this.lmz.callContext)

Lumenize automatically propagates Call Context across any call chain (e.g., Client → DO → Worker → DO). this.lmz.callContext is always accurate for the current request—even when calls chain through multiple mesh nodes, when local continuation handlers execute later, when using two-one-way call patterns, or when alarms fire.

Why We Do This

  • Zero Boilerplate: You don't have to manually pass identity, calling node type, etc. into every single function. They are "just there" when you need them.
  • Race-Safe: Even when multiple requests to the same mesh node interleave, each request sees its own unique context. Note, storage is not similarly isolated so that race-condition risk must still be kept in mind.
// Any method, anywhere in your call chain
@mesh()
doSomething() {
// Always correct for the current request
const user = this.lmz.callContext.originAuth?.sub;
}
Implementation Detail

Under the hood, this.lmz.callContext uses Node.js's confusingly named AsyncLocalStorage to maintain context across hops through space and time. There is no reason to use it directly in Lumenize Mesh. Just use this.lmz.callContext.

Where Identity Enters the Mesh

User identity (callContext.originAuth) enters the mesh only through LumenizeClient via LumenizeClientGateway. The Gateway verifies the JWT and populates originAuth with the verified claims — the actual token is never propagated. Both callChain and originAuth are immutable throughout the chain; intermediate nodes cannot modify them.

Client (alice) → DocumentDO → ValidationWorker → NotificationDO
↓ ↓ ↓
originAuth has originAuth has originAuth has
alice's claims alice's claims alice's claims (propagated!)

For the complete CallContext interface — including which fields are immutable, per-hop, or mutable — see Mesh API: CallContext.

Using state

The mutable callContext.state property can be thought of as a passive side-channel for communication sort like HTTP cookies — once data is added, it persists for the life of the call chain. Common uses:

  • Avoid redundant work: Compute or fetch something (like loading a user record or permissions), then access it in method guards or @mesh methods without re-computing or re-fetching
  • Cross-cutting concerns: Add metadata that any downstream node in the chain might need for instance state that's available in the current node's storage but not the later node's

For the canonical example of using state to cache session data for access control, see Security: Call Context State.

Tracing

For simple call path tracing, you might not need state at all — callChain already captures the full path ([origin, hop1, hop2, ...]). Use callChain.map(n => n.bindingName).join(' → ') for a quick trace.

When Call Chains Break

Call context is automatically maintained when one mesh call triggers another or when a local continuation is called. However, certain events, like alarm(s) in LumenizeDO, and browser events in LumenizeClient start new chains with fresh context:

use newChain to proactively break call chain

Call chain extension may not be desirable in all cases, for instance in fan-out patterns where you don't want tracing to bleed across recipients. For these cases, use { newChain: true } — see Breaking Call Chains.

2. Application Context (Continuations)

While this.lmz.callContext is great for mesh-oriented context, you should use extra parameters in your continuations for your own application context. Alternatively, for DOs and other nodes with storage capability, you could store such context locally and merely pass the key for such data into the continuation.

this.lmz.call(
'DOCUMENT_DO',
documentId,
this.ctn<DocumentDO>().subscribe(),
this.ctn().handleSubscribeResult(documentId, this.ctn().$result, 'open-document') // $result can go anywhere
);
// ...

// Response handler for subscribe - receives initial content or Error
handleSubscribeResult(documentId: string, result: string | Error, source: string) {
// ...
if (result instanceof Error) {
console.error(`Failed to subscribe to ${documentId}:`, result);
return;
}
// ...
}

Eviction/Hibernation Risk

Context is maintained in most cases, but if a node hibernates/evicts while awaiting a response, the local handler might never execute.

For restart-safe patterns:

  • Use Two One-Way Calls — the callback continuation is persisted by the recipient. When the callback is made, it reawakens your instance with a fresh call chain (the recipient is now the origin).
  • Use Alarms — the continuation and its parameters are persisted. Note that alarms start fresh call chains — if you need the original callContext, pass it as a parameter.
  • Use Manual Persistence — explicitly store continuations and context for custom restart-safe patterns.

Manual Persistence (Power Users)

If you're building your own restart-safe patterns, you need to persist both the continuation and any context you want restored:

// Create a continuation to our own logMessage method
const continuation = this.ctn<DocumentDO>().logMessage(message);

// Extract the operation chain from the continuation proxy
const chain = getOperationChain(continuation);
// ...

// Capture the current call context
const context = this.lmz.callContext;

// Store both for later execution (KV handles complex types natively)
this.ctx.storage.kv.put(`task:${taskId}`, { chain, context } as PendingTask);

When restoring:

const pending = this.ctx.storage.kv.get(`task:${taskId}`) as PendingTask | undefined;
// ...

const { chain, context } = pending;

// Execute the chain - context is available for manual use
// requireMeshDecorator: false allows calling methods without @mesh decorator.
// This is safe here because we're executing a chain we created and stored ourselves.
await executeOperationChain(chain, this, { requireMeshDecorator: false });

API Reference

getOperationChain(continuation)

Extracts the serializable operation chain from a continuation proxy.

import { getOperationChain } from '@lumenize/mesh';

const continuation = this.ctn<MyDO>().someMethod(arg);
const chain = getOperationChain(continuation); // OperationChain | undefined

Returns: OperationChain | undefined — the operation chain, or undefined if the argument isn't a continuation proxy.

Note: This does not capture callContext — you must store that separately if needed (see example above).

executeOperationChain(chain, target, options?)

Executes an operation chain on a target object.

import { executeOperationChain } from '@lumenize/mesh';

await executeOperationChain(chain, this, { requireMeshDecorator: false });

Parameters:

  • chain — The operation chain to execute
  • target — The object to execute the chain on (usually this)
  • options — Optional configuration:
OptionDefaultDescription
requireMeshDecoratortrueWhen true, the entry-point method must have @mesh() decorator. Set to false only for trusted chains you created yourself.
maxDepth50Maximum chain length (security limit)
maxArgs100Maximum arguments per method call (security limit)

Returns: Promise<any> — the result of executing the chain.