Repository navigation
Add support for sending Set over RPC - #229
Conversation
🦋 Changeset detectedLatest commit: 4fbcbe0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
/bonk review this |
commit: |
|
/bonk review this |
There was a problem hiding this comment.
Review: Add support for sending Set over RPC
Overall this is a solid, well-tested implementation. I traced the promise-substitution path on both the wire (Evaluator → RpcPayload.forEvaluate → deliverTo → deliverRpcPromiseTo → parent[property] = value) and the local (deepCopy) side, and the one-time setter approach in defineSetPromiseSlot correctly:
- substitutes each promise at its own position (verified with multiple promises),
- re-applies Set semantics so a resolution equal to an existing element collapses,
- uses reference identity via
indexOf(placeholder), which is robust even when resolved values coincide, - covers
Blob-in-Seton the return path since blobs are delivered asRpcPromises.
The test suite is thorough and the strayProps assertions are a nice way to pin down that no leftover setter properties leak. build succeeds. (I couldn't run the vitest suite in CI here because it uses the browser pool and Playwright isn't installed in this environment, but I validated the setter logic with a standalone reproduction.)
Comments below are minor — mostly whitespace/style, plus one unrelated diff line and a complexity note. None are blocking.
Notes not tied to a single line
- O(n²) resolution cost: each promise resolution does
[...set]+indexOf+clear()+ full rebuild. For a Set with k promises this is O(k·n). Fine for the expected small-cardinality case, but worth a comment ondefineSetPromiseSlotso a future reader doesn't assume it's cheap. - Changeset file
.changeset/serialize-set.mdis missing a trailing newline (\ No newline at end of file).
|
Review posted successfully. Here's a summary of what I reviewed and posted to PR #229. Review summaryI traced the full Correctness — verified sound:
Feedback posted (all minor, non-blocking):
The review was submitted as a single |
|
/bonk review this |
1 similar comment
|
/bonk review this |
There was a problem hiding this comment.
Review: Add support for sending Set over RPC
I re-reviewed at the latest commit (0942d18). The style nits from the earlier round (trailing whitespace, the unrelated array-case diff) have been addressed — src/ typechecks cleanly (tsc --noEmit), and the remaining tsc errors are pre-existing and confined to __tests__/bun.test.ts / workerd.test.ts, unrelated to this change.
Correctness — traced and looks sound:
- Wire path:
Devaluatoremits["set", elements];Evaluatorrebuilds withevaluateImpl(element, set, key, depth + 1)and installs a per-element setter viadefineSetPromiseSlot. Depth is threaded correctly so the recursion limit still applies to set elements. - Local path:
deepCopybuilds the newSetfirst, then fills it, passingresultas the parent so promise records point at the copy (not the caller's set). Thesubstitutes a promise in a Set passed to a local stubtest confirms the caller's set is left untouched. - Delivery:
deliverTodoesparent[property] = value, which fires the one-time setter.indexOf(placeholder)uses reference identity, so multiple promises and resolutions that coincide with existing elements both behave correctly (dedup re-applied on rebuild). Verified against the multi-promise and collapse tests. followPathtreatssetas having no remotely-addressable properties, consistent with skippingmap().disposeImpl/ignoreUnhandledRejectionsImplrecurse over set elements; disposal is driven by thepromisesrecord list, so replacing promises in-place during delivery doesn't break it.- Empty set (
["set", []]) passes thevalue.length === 2 && value[1] instanceof Arrayguard.
Non-blocking notes:
defineSetPromiseSlotis O(k·n) for a set with k promises (each resolution does[...set]+indexOf+clear()+ full rebuild). Fine for expected small cardinalities, but worth a one-line comment so a future reader doesn't assume it's cheap.- If a set element promise never resolves (rejection/abort before delivery), the non-enumerable setter property persists on the copied set. It's on an ephemeral copy that gets GC'd, so this is harmless — noting it for completeness.
- A couple of missing semicolons in the setter body (
delete .../set.clear()), inconsistent with the surrounding file style. Minor.
Nice work — the test coverage (strayProps assertions in particular) is thorough and pins down the tricky substitution behavior well.
|
Review submitted successfully to PR #229. SummaryI re-reviewed the Key findings:
The implementation is solid and the test coverage (especially the |
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
|
/bonk review this |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
/bonk review this |
There was a problem hiding this comment.
Review: Add support for sending Set over RPC
The implementation has evolved substantially since the earlier rounds and now follows @kentonv's guidance: instead of the property-accessor/setter hack, promises (and, on the wire, Blobs) are simply banned as direct Set elements, with clear TypeErrors on both the send and receive sides. This is a much simpler and more defensible design.
Verification performed in this review:
npm run buildsucceeds.vitest run --project node __tests__/index.test.ts-> 202 passed (browser/workerd pools can't run in this sandbox, but the Set logic is pure and fully covered by the node run).- Traced the full path on both sides:
- Wire send (
Devaluator,src/serialize.ts:322): validates all elements before encoding any, so a rejected element can't leave a half-created blob pipe behind. Good, and the testrejects a Blob sent inside a Setpins this down. - Wire receive (
Evaluator,src/serialize.ts:891): rebuilds the set and rejects any element that evaluates to anRpcPromise. Since streamedBlobs arrive as pipelined imports (RpcPromise), this also blocks Blob-in-Set from a hostile/buggy peer. The forged-message test (rejects a promise arriving inside a Set from a peer) exercises this guard directly. - Local path (
RpcPayload.deepCopy,src/core.ts:1077): builds the copy first, bans promises, but intentionally allowsBlobs (no wire stream involved). Consistent with theaccepts a Blob in a Set passed to a local stubtest. followPath,disposeImpl, andignoreUnhandledRejectionsImplall handle thesetcase consistently.
- Wire send (
The one change worth calling out for a maintainer's eyes: src/serialize.ts:1152 changes the non-promise import branch of addStub from new RpcPromise(hook, []) to new RpcStub(hook). This is needed so the Evaluator's copy instanceof RpcPromise check doesn't reject a legitimate bounced-back stub inside a Set (RpcPromise extends RpcStub). It also looks like a genuine correctness fix — on main this branch registered the hook in this.hooks (the stub list) yet returned an RpcPromise, which contradicts both the documented "imports pass immediately as stubs" intent and the parallel logic in deepCopy (src/core.ts:1123-1125). The behavioral effect is that root-level import arguments are no longer eagerly pulled/awaited before the target runs, which is the intended behavior. Worth a maintainer confirming there's no code relying on the old (arguably buggy) behavior. The bounce tests cover the Set-specific case.
Everything below is minor / non-blocking. Nice work — the test suite (especially the strayProps assertions and the forged-peer-message test) is thorough.
|
fatal: could not read Username for 'https://github.com': No such device or address |
Adds serialization support for
Set. Doesn't special case.map()like we do for arrays, uses the default. I think we don't need it is becauseSetitself doesn't have one, so it is not expected as much as it is on anArray.This does handle lazy values that are part of a set. Again, can't think of a use case but using a
Blobas an element ofSetis technically valid and also using anRpcPromisethat you get back as part of theSet, so this implementation handles such cases. It handles it by not moving away fromLocatedPromisewhich expects a parent and property accessor to replace the placeholder promise with the resolved value.We could refactor the code to avoid the temporary setter but that felt like a bigger change, so once this and
Mapimplementation merges we can revisit that.If we want to revisit
.mapover set elements, I think we should block.mapsupport forSetCloses #221