Continuations
A Continuation is a description of work that gets executed in some other place or time — usually during or after some native async operation like a call or alarm.
const remote = this.ctn<RemoteDO>().getUserData(userId);
const local = this.ctn().handleResult(this.ctn().$result);
this.lmz.call('REMOTE_DO', 'instance-id', remote, local);
For practical patterns — chaining, nesting, $result placeholders, error handling, and more — see Making Calls.
Why Continuations?
Continuations solve several problems in Durable Objects:
- Serializable — Can be sent over the wire or stored and then restored after hibernation/eviction
- Type-safe — TypeScript checks method names and signatures at compile time
- No
awaitneeded — Avoidasync/awaitwhich can break DO consistency guarantees. While race conditions are still possible, the explicitness and the fact that handlers need not be declaredasyncreduces the likelihood you will do so accidentally.
The this.ctn() Factory
Your classes that extend LumenizeDO, LumenizeWorker, or LumenizeClient have access to this.ctn():
// Typesafe: TypeScript knows RemoteDO's methods
this.ctn<RemoteDO>().someMethod(arg)
// Default: uses local class type when no generic provided
this.ctn().myHandler(this.ctn().$result)
// Chaining: multiple operations in sequence
this.ctn<RemoteDO>().validate().save().getRevision()
// Nesting: output of one operation feeds another
this.ctn<RemoteDO>().multiply(this.ctn<RemoteDO>().add(1, 2), 10)
How It Works
1. Proxy-Based Chain Building
this.ctn() returns a JavaScript Proxy. Each property access or method call adds an operation to the continuation's operation chain.
2. Serialization
When you pass a continuation to this.lmz.call(), it's serialized using @lumenize/structured-clone so rich types (Date, Set, Map, aliases, cycles, etc.) are supported for parameters and results :
// The chain becomes a serializable object
{
ops: [
{ type: 'call', name: 'getProfile', args: ['user-123'] },
{ type: 'call', name: 'formatName', args: [] }
],
context: { /* captured callContext */ }
}
This can be:
- Sent over the wire to another DO/Worker/Client
- Stored for later execution
- Passed to
this.svc.alarms, which will automatically store it
3. Execution
On the receiving end, executeOperationChain() walks the chain:
// Conceptual execution logic
let result = target;
for (const op of chain.ops) {
if (op.type === 'call') {
result = result[op.name](...op.args);
} else if (op.type === 'get') {
result = result[op.name];
}
}
return result;
Before execution, callContext is restored from the captured snapshot — ensuring your handler sees the correct auth and context even when the execution is on another node. Similarly, you can explicitly store/restore the serialized form to add resiliency through hibernation/eviction.
OCAN: Operation Chaining And Nesting
Continuations use OCAN (Operation Chaining And Nesting) — a serialization format that supports:
| Feature | Example |
|---|---|
| Chaining | a().b().c() — sequential calls on returned values |
| Nesting | a(b(), c()) — results of inner calls as arguments |
| Mixed | a(b().c()).d() — arbitrary combinations |
| Placeholders | $result — substitute async results |
For the full specification, see @lumenize/rpc: Operation Chaining and Nesting. Note, however, that batching is not automatically supported for Lumenize Mesh. After using the old Lumenize RPC for a while, we found that defining methods on the callee to do batch operations was a better approach for production code. It remains in the old Lumenize RPC for backward compatibility.
Manual Serialization (Advanced)
For power users who need direct access to the serialized form — extracting chains with getOperationChain() and executing them with executeOperationChain() — see Manual Persistence for a complete example and API Reference for function signatures.