Skip to content

Support Map over the RPC - #232

Open
srikrsna wants to merge 3 commits into
cloudflare:mainfrom
srikrsna:sk/map
Open

srikrsna wants to merge 3 commits into
cloudflare:mainfrom
srikrsna:sk/map

Conversation

@srikrsna

@srikrsna srikrsna commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Adds serialization support for Map. Doesn't special case .map() like we do for arrays, uses the default.

Just like #229, we support reject lazy values in keys.

Closes #230

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/capnweb@232
npm i https://pkg.pr.new/cloudflare/capnweb/capnweb-validate@232

commit: 25c3a7b

@srikrsna

srikrsna commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

/bonk review this

1 similar comment
@teamchong

Copy link
Copy Markdown
Collaborator

/bonk review this

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Map serialization support. The implementation is well thought out and the test coverage is genuinely impressive — the promises inside a Map suite exercises the tricky bits (key/value slots resolving in either order, key collapse under Map semantics, insertion-order preservation, Blob round-trips, and the local deepCopy path). I ran the full node suite (165 passed) and test:types (clean) locally against this branch.

The core design decision — using entries as the source of truth and rebuilding the whole Map on each promise resolution via one-time-use setters — is a clever way to reuse the existing parent[property] = resolved delivery machinery for a structure that has no addressable positions. Nicely mirrors the Set-less-ness reasoning in the comments.

A few non-blocking observations below. None of them are correctness bugs — all tests pass — but the O(n^2) rebuild is worth at least acknowledging.

Notes

  1. O(n^2) rebuild on resolution. Each resolved promise slot calls map.clear() and re-inserts every entry, so a Map with n promise slots does O(n^2) work total. For typical payloads this is fine, but a large Map full of pipelined promises could be pathological. Given the constraint (Map has no stable index to write into), this is a reasonable tradeoff — just flagging it. A future optimization could defer the rebuild until all slots for a given batch have resolved, but that adds bookkeeping complexity that probably isn't worth it now.

  2. Type coverage for serialization. The Map<K,V> types already exist in src/types.d.ts (Stubify/Unstubify/RpcCompatible), so this PR correctly fills the runtime gap. Worth confirming there's a round-trip/type assertion somewhere in __type-tests__ that a Map value survives — roundTripHeaders(new Map(...)) exists but that's Headers-typed, not a Map-typed round trip. Optional.

Overall this looks good to merge. Nice work on the tests.

Comment thread src/core.ts Outdated
Comment thread src/serialize.ts
Comment thread src/core.ts
@ask-bonk

ask-bonk Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review submitted successfully. Here's a summary of what I reviewed and posted on PR #232:

Review Summary

The PR adds runtime serialization for Map over RPC. I read the full diff, traced the delivery/disposal machinery in src/core.ts and src/serialize.ts, and ran the tests locally.

Verification I did:

  • Full node test suite: 165 passed
  • Type tests (test:types): clean
  • Confirmed Map<K,V> types already existed in src/types.d.ts — this PR fills the missing runtime piece.

Assessment: looks good to merge. The design is sound:

  • Uses entries as the source of truth and rebuilds the Map on each promise resolution via one-time-use setters, cleverly reusing the existing parent[property] = resolved delivery path for a structure with no addressable positions.
  • Handles key/value promise slots resolving in either order, key collapse under Map semantics, and insertion-order preservation.
  • Test coverage is thorough (the promises inside a Map suite is excellent).

Non-blocking notes I posted (3 inline + summary):

  1. O(n²) rebuild — map.clear() + full re-insert on every slot resolution; fine for normal payloads but quadratic for large Maps of pipelined promises. Reasonable tradeoff given the constraints, just flagged.
  2. Error precision in the evaluate path — malformed ["map", ...] shapes fall through to the generic "unknown type" error rather than a Map-specific one. Optional.
  3. Disposal invariant — suggested a clarifying comment noting that the disposeImpl map branch only runs for "return" payloads (where slot setters are never installed), so walking the collapsed Map is safe.

No correctness bugs found.

github run

github-actions Bot pushed a commit to srikrsna/capnweb that referenced this pull request Aug 6, 2026
Co-authored-by: teamchong <teamchong@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 25c3a7b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
capnweb Minor

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

@teamchong

Copy link
Copy Markdown
Collaborator

recheck

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

| ------------------------------------------- | --------- | ----------- |
| `Map` and some other built-ins | Not yet | Yes |
| `Set` | Yes | Yes |
| Some other built-ins | Not yet | Yes |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like what?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbortSignal comes to mind but we can remove the line too

Comment thread src/core.ts
// RpcPayload

export type LocatedPromise = {parent: object, property: string | number, promise: RpcPromise};
export type LocatedPromise = {parent: object, property: unknown, promise: RpcPromise};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this necessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map keys can be anything

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably the right general approach, but it also makes me nervous that we might mishandle properties somewhere. I worry about what happens e.g. if someone uses undefined as a map key, or if they somehow manage to encode a value that happens to stringify to __proto__ and then we accidentally do object[propertyName] somewhere and oops.

What if we required arbitrary-typed map keys to be boxed? Like:

type PropertyName = string | number | {mapKey: unknown}

@srikrsna srikrsna Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we required arbitrary-typed map keys to be boxed? Like:

We will allocate for every key but apart from that it will work. I can see that this flips the accidental case to do .set instead of object[propertyName] but the check that prevents this is the same. Instead of an instance check on Map we will check for the key shape to match. I am not sure it is worth the allocations.

Comment thread __tests__/index.test.ts
Comment thread src/core.ts
}

let result = new Map();
for (let [key, val] of entries) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we have to loop over all the entires a second time? can't we just check the key here, as we encounter them, and then just throw immediately if we hit one that's not valid?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid a potential deepCopy of a value

Comment thread src/core.ts
Comment thread src/serialize.ts

case "map": {
let map = <Map<unknown, unknown>>value;
let mapEntries = [...map];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I can tell (I didn't measure it) we should prefer map.entries() here because the destructure creates eagerly allocates memory for a whole new array immediately, while .entries() lazily makes an interable zero-allocation structural pointer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will create the iterator twice, in this case it will just create the array once and loop over the same one twice

Comment thread src/serialize.ts
// rebuilding itself. Values may be promises or Blobs. As with `Set`, validate all keys
// before encoding anything, since encoded streams and Blobs (including in values) create
// pipes that cannot be rolled back.
for (let [key] of mapEntries) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as above, why do we need to loop twice?

Comment thread src/serialize.ts
}
break;
case "map":
if (value.length === 2 && value[1] instanceof Array) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see all the others work this way, but it's strange to me that we silently (right?) drop any error resulting from not hitting this condition. seems like we should protect against malformed stuff here, no? we can handle separately in a new PR for test coverage for this for all types if you think - I'd be happy to do it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to surface a specific error because users are unlikely to hit this case. The only ones that will likely see this could be authors of capnweb ports to other languages.

@dimitropoulos dimitropoulos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some things

This branch has not been deployed

No deployments
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.

support Map serialization type

4 participants