fix(core): keep constructor unbound on withEventMeta proxies - #109
Conversation
The withEventMeta proxy binds every function-valued property to the target so internal-slot methods (Map#get, #private) keep working, but it also bound the inherited `constructor`. Events carrying an id, retry or comments then had `constructor !== Object`, which broke plain-object checks (zod records, is-plain-object) and `toStrictEqual`.
@standard-server/aws-lambda
@standard-server/core
@standard-server/fastify
@standard-server/fetch
@standard-server/node
@standard-server/peer
@standard-server/shared
commit: |
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
constructorstays unbound onwithEventMetaproxies —packages/core/src/event-stream/meta.ts:48now callsgetOrBind(target, prop, { bind: prop !== 'constructor' }), so a metadata-wrapped value reports its realconstructor(Object,Foo,Map) instead ofbound Object, while methods remain bound to the real target.- Core shape test —
packages/core/src/event-stream/meta.test.tspins the visible shape for a plain object (constructor === Object+toStrictEqual), a class instance (constructor, static member,instanceof,#privatemethod) and aMap(get/size). - Receive-path assertions tightened —
packages/fetch/src/event-stream.test.tsandpackages/peer/src/event-stream.test.tsswitch the relevanttoEqualchecks totoStrictEqual, which compares constructors and therefore actually catches this regression.
I ran the core, fetch and peer suites (413 passing), then temporarily reverted the one-line change: the new core assertion and both tightened receive assertions fail as expected (bound Object vs Object), confirming the tests are discriminating rather than passing by construction. The bind option already existed on getOrBind and has no other call sites, and the node/aws-lambda receive paths delegate to the fetch iterator, so the fetch test change covers them. The structuredClone limitation noted in the PR body is a fair scoping call and is left out deliberately.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Events that carry an
id,retryor comments now keep their realconstructor, so they pass plain-object checks again. Before, a received{ a: 2 }with an id reportedconstructorasbound Object, which made zod'sz.record(...),is-plain-objectandtoStrictEqualreject it, while the same event without metadata passed. Class instances wrapped withwithEventMetaalso lostconstructor === Fooand their static members.Fixes
Map#get,Datemethods, class#privatefields) still work through the proxy.Testing
#privatemethod) and aMap.toStrictEqualinstead oftoEqual.toEqualdoesn't compare constructors, which is how this slipped through. These tests fail without the fix.tscare clean.Note for reviewers
Removing the binding entirely would mean dropping the proxy for a WeakMap side table. That fixes more cases (
structuredCloneof an event currently throws), butwithEventMetawould attach metadata to the original object instead of returning a new value, which is a breaking change. It's left out of this PR.