Uh oh!
There was an error while loading. Please reload this page.
Generalize record-replay: serialize closures passed to any RPC method - #162
Generalize record-replay: serialize closures passed to any RPC method#162ryanrasti wants to merge 1 commit into
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
ryanrasti
commented
Apr 20, 2026
I have read the CLA Document and I hereby sign the CLA |
kentonv
commented
Apr 27, 2026
Hmm. Restricting this to only work inside a |
ryanrasti
commented
Apr 30, 2026
Sure thing. And yes // Table definitions:classPostextendsRpcTarget{getid(){returnthis.column('id')}getuserId(){returnthis.column('user_id')}getcontent(){returnthis.column('content')}}classUserextendsRpcTarget{getid(){returnthis.column('id')}posts(){returnPost.from().where(({posts})=>posts.userId['='](this.id))}}// Main stub:classApiextendsRpcTarget{userForToken(token: string){constuserId=validateToken(token)returnUser.from().where(({users})=>users.id['='](userId))}db(): Database{ ... }}// Actual usage:constgetNumPosts=(api: RpcStub<Api>)=>{consttoken=tokenFromContext(...)constresults=api.map(_=>api.userForToken(token).select(({users})=>({id: users.id,numPosts: users.posts().groupBy().select(({posts})=>posts.id.count())})).execute(api.db()))returnresults[0]?.numPosts??0}The shape I'm envisioning: wrap the entire query once in a |
efebddf to
43dc308Compare- Cap'n Web upstream based on cloudflare/capnweb#162, with extra commits to: - (a) unwrap RpcStubs that point back to local objects (getLocalTarget) - (b) replay record/replay closures synchronously instead of eagerly Promisifying them (so fully-local chains run sync end to end), plus allow zero-arg closures - @expose <-> RpcTarget translation exposing only the members we manually @expose, and crucially unwrapping RpcStubs that point to local objects (the record-replay closures) back to their raw builder objects
- Cap'n Web upstream based on cloudflare/capnweb#162, with extra commits to: - (a) unwrap RpcStubs that point back to local objects (getLocalTarget) - (b) replay record/replay closures synchronously instead of eagerly Promisifying them (so fully-local chains run sync end to end), plus allow zero-arg closures - @expose <-> RpcTarget translation exposing only the members we manually @expose, and crucially unwrapping RpcStubs that point to local objects (the record-replay closures) back to their raw builder objects
ndisidore
commented
Jul 16, 2026
I see you're still pushing here: holler when you want another set of eyes! |
`capnweb` was a runtime dependency with a `file:packages/capnweb` specifier, while `files: ["dist"]` meant `packages/` never shipped. npm resolves that by creating node_modules/capnweb as a symlink to a path that doesn't exist inside the installed package, so `npm install typegres` exits 0 and `import "typegres/capnweb"` then dies with ERR_MODULE_NOT_FOUND. The one feature the README lists as shipped, and the foundation of examples/chat, did not work for any consumer. capnweb is now bundled into the entry point. It can't be an ordinary dependency until cloudflare/capnweb#162 lands: the shim needs five commits that aren't in the published 0.6.1 (closure serialization, synchronous replay, zero-arg closures, getLocalTarget).
`capnweb` was a runtime dependency with a `file:packages/capnweb` specifier, while `files: ["dist"]` meant `packages/` never shipped. npm resolves that by creating node_modules/capnweb as a symlink to a path that doesn't exist inside the installed package, so `npm install typegres` exits 0 and `import "typegres/capnweb"` then dies with ERR_MODULE_NOT_FOUND. The one feature the README lists as shipped, and the foundation of examples/chat, did not work for any consumer. capnweb is now bundled into the entry point. It can't be an ordinary dependency until cloudflare/capnweb#162 lands: the shim needs five commits that aren't in the published 0.6.1 (closure serialization, synchronous replay, zero-arg closures, getLocalTarget).
<stub>.map(fn)already recordsfnlocally, emits a["remap", …]instruction, and replays it on the receiver. This generalizes the mechanism so any user-defined method on anRpcTargetcan accept callbacks that serialize the same way.Motivation: I'm building typegres which uses capnweb to expose a capability-based query builder:
The callbacks should be interpreted server-side (not exported as a stub and sent back to client).
Key design decisions
mapand general closures: I chose to re-use the most code possible but maintain distinct wire formats forremapandclosure.Potential follow-ons (not in this PR):
RpcPayload.deliverResolvealways wraps in one, even when nothing needs awaiting. Async closures are already rejected at serialize time.(Opening as a draft to get your read on the design -- happy to rework anything.)