PATTERN Cited by 1 source
Record-replay DSL¶
Problem¶
You want to apply a user-supplied JavaScript transformation to
each element of a promised remote array, and you want it to
happen server-side in one round trip — not as N client→server
round trips where the client fetches the array, iterates in a
for loop, and re-dispatches a dependent RPC per element.
The obvious solution — "just pass the callback by reference" — works but defeats the purpose. Cap'n Web can pass a function by reference, but then the server has to call the callback back at the client per element, which is exactly the N round trips you wanted to avoid.
The other obvious solution — ship a serialized chunk of JavaScript
to the server and eval it — opens a huge security and
sandboxing problem. It also requires the server to understand the
full language.
What you actually need: ship a restricted, non-Turing-complete, domain-specific description of the transformation that the server can faithfully replay per element, without trusting the client with arbitrary code execution.
Pattern¶
Run the callback once on the client against a Proxy
placeholder, intercept the speculative pipelined calls it makes,
and transmit the recorded call graph as the DSL. The server
replays the recorded instructions per element in the array.
Works because the recording fits a shape already in the RPC protocol — promise pipelining. The DSL is literally the pipelining protocol itself.
Mechanism (from Cap'n Web)¶
let user = api.authenticate(token)
let friendsPromise = user.listFriends()
// Apply server-side transformation to each friend:
let friendsWithPhotos = friendsPromise.map(friend => {
return { friend, photo: api.getUserPhoto(friend.id) }
})
let results = await friendsWithPhotos // ONE round trip
What happens under the hood:
.map(callback)is invoked on a promise (not a resolved array). The implementation does not await the promise.- The implementation runs
callbackonce, client-side, passing a specialProxyas thefriendargument. The callback must be synchronous — anyawaitinside would force resolution, defeating the record. - The only thing the callback can do synchronously is call
methods on the
friendproxy (which interprets them as pipelined calls on the future element) and call other RPC stubs in the surrounding scope (which generate their own pipelinedpushmessages). - Those pipelined calls are recorded — intercepted by the
Cap'n Web machinery and assembled into a list of RPC
expressions: "invoke api.getUserPhoto(
.id); return {friend: ., photo: }" - The recording is shipped to the server as part of the
.map()RPC message. The server iterates the array, substitutes each element for the placeholder, and replays the expressions. - The assembled result array is returned — one round trip.
The "DSL" here is just the Cap'n Web RPC protocol itself,
interpreted per element. There is no separate language; no
JavaScript sandbox on the server; no second serialization format.
The same push / pipeline / resolve vocabulary that the
protocol uses for cross-network calls is reused as the
transformation instructions.
Why it works¶
- JavaScript
Proxyobjects as call recorders. JS Proxies can intercept every property access and method invocation, which is exactly what's needed to capture the speculative pipelined calls a callback tries to make. - Capability RPC already expresses "use the result of X as the
input to Y." Cap'n Web's
pushwith a["pipeline", …]argument is exactly this. No new wire format is needed. - Synchronous-only constraint is naturally enforced. The
record runs on the client; any real
awaitwould actually wait on the network, which is wrong. Forcing the callback synchronous means the only observable thing it can do is call methods on proxies — which is what we want to record. - Server-side replay is a tight loop. The server has all the necessary state (the array + the recorded expressions) and does not need to talk to the client during iteration. It substitutes the element, evaluates, and collects results.
Known uses¶
- Cap'n Web
.map()(Cloudflare, 2025) — the canonical instance. Cap'n Proto never supported this per Varda's own statement; Cap'n Web is the first implementation of the technique. "But the application code just specified a JavaScript method. How on Earth could we convert this into the narrow DSL? The answer is record-replay." (Source: sources/2025-09-22-cloudflare-capn-web-rpc-for-browsers-and-web-servers) - Effect systems / algebraic effects in FP languages — conceptually adjacent: run a computation against a placeholder, capture the operations it performs, reinterpret them elsewhere. Not wiki-native; listed for lineage.
- Mocking libraries (Jest spies, sinon) use the same proxy- interception idea — to a different end (test assertions rather than DSL capture).
Trade-offs¶
- Callback must be synchronous. Any
awaitbreaks the recording. Users will hit this in practice — the ergonomic sharp edge Cap'n Web's announcement underplays. - Callback restricted to pipelined RPC operations. The
callback can only do what the RPC protocol already
represents: method calls, property access, object literals.
Local computation (string manipulation, arithmetic,
ifstatements on non-RPC values) may not record cleanly. - DSL expressiveness bounded by the RPC protocol. The protocol is non-Turing-complete on purpose; transformations needing branching / looping / complex logic don't fit.
- Debuggability. A crash mid-replay happens on the server
with recorded instructions that are one step removed from
the user's callback — stack traces point at the recording
machinery, not at the author's
.map()lambda. - Silent correctness hazard if the callback has side effects. The client runs the callback once against a proxy; those calls are recorded. If the callback mutates local variables or prints to console, that happens once on the client, not per element on the server. Pure callbacks only.
- Small blast radius. The technique generalizes beyond
.map()in principle (to.filter(),.reduce(), etc.) but the Cap'n Web post only ships.map()as of 2025-09-22.
Related¶
- concepts/promise-pipelining — the underlying mechanism.
- concepts/object-capability-rpc — the protocol family.
- concepts/json-serializable-dsl — philosophical cousin (the recorded expressions are JSON).
- concepts/bidirectional-rpc — the symmetric protocol Cap'n Web rides on top of.
- patterns/capability-returning-authenticate — the other load-bearing Cap'n Web idiom from the same post.
- systems/capnweb — implementation.
- GraphQL — the previous-generation technique for the same round-trip-collapse problem, achieved via a declarative query language rather than record-replay.