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 ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
Hmm. Restricting this to only work inside a |
Sure thing. And yes // Table definitions:
class Post extends RpcTarget {
get id() { return this.column('id') }
get userId() { return this.column('user_id') }
get content() { return this.column('content') }
}
class User extends RpcTarget {
get id() { return this.column('id') }
posts() { return Post.from().where(({posts}) => posts.userId['='](this.id)) }
}
// Main stub:
class Api extends RpcTarget {
userForToken(token: string) {
const userId = validateToken(token)
return User.from().where(({users}) => users.id['='](userId))
}
db(): Database { ... }
}
// Actual usage:
const getNumPosts = (api: RpcStub<Api>) => {
const token = tokenFromContext(...)
const results = api.map(_ =>
api
.userForToken(token)
.select(({users}) => ({
id: users.id,
numPosts: users.posts().groupBy().select(({posts}) => posts.id.count())
}))
.execute(api.db())
)
return results[0]?.numPosts ?? 0
}The shape I'm envisioning: wrap the entire query once in a |
efebddf to
43dc308
Compare
- 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
|
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.)