Skip to content

Generalize record-replay: serialize closures passed to any RPC method#162

Draft
ryanrasti wants to merge 1 commit intocloudflare:mainfrom
ryanrasti:generalize_record_replay
Draft

Generalize record-replay: serialize closures passed to any RPC method#162
ryanrasti wants to merge 1 commit intocloudflare:mainfrom
ryanrasti:generalize_record_replay

Conversation

@ryanrasti
Copy link
Copy Markdown

<stub>.map(fn) already records fn locally, emits a ["remap", …] instruction, and replays it on the receiver. This generalizes the mechanism so any user-defined method on an RpcTarget can accept callbacks that serialize the same way.

Motivation: I'm building typegres which uses capnweb to expose a capability-based query builder:

users
    .where(({ users }) => users.id["="](1n))
    .select(({ users }) => ({ id: users.id, name: users.name.upper() }))
    .execute();

The callbacks should be interpreted server-side (not exported as a stub and sent back to client).

Key design decisions

  1. Backward compatibility: Plain functions passed over RPC today are exported as stubs (pass-by-reference). To keep that unchanged, the draft only routes functions through record-replay when they're passed inside a .map() callback — where creating a new stub is already forbidden. (No new opt-in flags needed).
  2. Sharing between map and general closures: I chose to re-use the most code possible but maintain distinct wire formats for remap and closure.

Potential follow-ons (not in this PR):

  1. Sync replay for closures -- the receiver's replay fn currently returns a Promise because RpcPayload.deliverResolve always wraps in one, even when nothing needs awaiting. Async closures are already rejected at serialize time.
  • Why this matters: Typegres's query builder is synchronous -- without sync replay, every callback-taking method has to become async, infecting the call chain.
  1. Stub identity preservation: independent of closures: round-tripping a stub back to its owner currently yields a proxy, not the raw target.
  • Why this matters: server may need to do things like validate the type of the object, access non-exposed fields, etc. which are impossible to do if its own object is wrapped behind a stub

(Opening as a draft to get your read on the design -- happy to rework anything.)

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 19, 2026

⚠️ No Changeset found

Latest commit: 43dc308

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant