Request / Response
One parent machine owns an ActorMap of short-lived request handlers, one per
order. Each handler settles exactly once (answered or timed out) and reports
back through declared outputs.
The request handler
Section titled “The request handler”One short-lived machine per request. idle until told to work, then pending
with its own timeout, then one final state: answered or timedOut.
const request = event("request")<{ orderId: string; timeoutMs: number }>();const answer = event("answer")<{ orderId: string; result: string }>();const { timeout } = events("timeout");const requestSettled = event("requestSettled")<Settled>();
const { idle, pending } = states("idle", "pending");const answered = state("answered")().final();const timedOut = state("timedOut")().final();
function createRequestHandler(orderId: string, clock: Clock) { return new Actor({ inputs: [request, answer], internal: [timeout], outputs: [requestSettled], states: [idle, pending, answered, timedOut], initial: idle, clock, context: { orderId, timeoutMs: 0 }, setup: (m) => { m.on(idle, request, (event, opts) => { const s = opts.context.get(); s.timeoutMs = event.payload.timeoutMs; opts.context.set(s); return { state: pending }; }); m.effect(pending, { name: "startResponseTimeout", fn: (input) => { withTimeout(input.context.get().timeoutMs, input, () => timeout.create()); }, }); m.on(pending, answer, (event) => ({ state: answered, emit: [ requestSettled.create({ orderId, status: "answered", result: event.payload.result }), ], })); m.on(pending, timeout, () => ({ state: timedOut, emit: [requestSettled.create({ orderId, status: "timedOut" })], })); }, });}requestSettled is declared in outputs. The handler emits it and walks
away. Emitted events that are not internal or input become outputs delivered
to the wiring. That is the child-to-parent reporting mechanism.
The coordinator
Section titled “The coordinator”One parent owns an ActorMap of handlers keyed by order id. Routes answers
to the right handler, collects settlements.
const requests = new ActorMap( (orderId) => { const child = createRequestHandler(orderId, clock); onOutput(child, (e) => { if (requestSettled.is(e)) manager.send(e); }); return child; }, { autoReap: true },);ActorMap children do not wire automatically. One onOutput line per child.
The is() guard narrows the child’s output to the parent’s declared input.
Promises live at the boundary, never in the machine. The bridge maps
orderSettled output to a resolver keyed by order id.
Auto-reap
Section titled “Auto-reap”{ autoReap: true } removes a handler the moment it reaches a final state.
Answer arrives, handler final, done, removed. Timeout fires, same.
Late answers are no-ops: the handler is gone. Settles exactly once.
Timeout
Section titled “Timeout”The handler owns its timeout. The pending effect arms it via
withTimeout; leaving the state aborts it. Answer wins, timer cancelled.
Timeout wins, handler settles timedOut. No dangling timers.
Each request has its own timeoutMs. Independent deadlines, one clock.
Await the result
Section titled “Await the result”waitFor(orderId) returns a promise that resolves when that request settles.
- Reads parent context first. Already settled = resolves immediately.
- Re-dispatching an order clears its settled result, so
waitForwaits for the new attempt.
requester.request("ord_1", 1000);requester.answer("ord_1", "shipped");await requester.waitFor("ord_1"); // { orderId: "ord_1", status: "answered", result: "shipped" }await requester.waitFor("ord_1"); // same, already settledTesting
Section titled “Testing”Deterministic on one VirtualClock:
const clock = new VirtualClock();const requester = createRequester(clock, 1000);
const promise = requester.waitFor("ord_2");requester.request("ord_2", 1000);clock.advance(1001);
await expect(promise).resolves.toEqual({ orderId: "ord_2", status: "timedOut" });expect(requester.requests.size).toBe(0);