Skip to content

fix(store): never wrap platform objects — plain data and user classes only (#2952) - #2954

Merged
ryansolid merged 1 commit into
nextfrom
fix/store-platform-objects-raw
Jul 28, 2026
Merged

fix(store): never wrap platform objects — plain data and user classes only (#2952)#2954
ryansolid merged 1 commit into
nextfrom
fix/store-platform-objects-raw

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes #2952.

Summary

  • Native code brand-checks internal slots and throws when invoked with a store proxy as this: store.map.size crashed on the read path (native accessor called with the proxy receiver) and s.map.set(...) crashed on the draft path (inherited method returned unbound, so this was the draft) — while method reads half-worked. An unmarked Map was wrapped but never honestly supported.
  • Rather than teaching the get trap per-callsite receiver conventions (native/user discrimination per property access), this fixes wrappability: isWrappable now wraps plain data and user class instances only. Platform objects — Map, Set, Date, URL, DOM nodes, host objects — are excluded structurally by the tag check: user classes stringify as [object Object], every native/host object carries its own brand ([object Map], [object Headers], ...), and subclasses inherit the tag.
  • Excluded objects get the markRaw-children contract automatically: served raw, mutations land on the raw object, and the property holding them still tracks, so reassignment notifies. User classes keep everything — prototype getters track through the proxy, draft methods keep the draft this so their field writes stay reactive.
  • The explicit instanceof Node check stays on the cold branch only: real DOM nodes are excluded by tag, but shimmed DOMs (linkedom/happy-dom style) implement nodes as plain user classes that pass it.

Performance

The naive version regressed (megamorphic .constructor loads on the hot path; per-read tag calls for class values). As landed:

  • hot path (plain/null-proto objects) resolves on one Object.getPrototypeOf — intrinsic, no property-lookup IC;
  • the custom-proto verdict is memoized per prototype (one tag call per class, ever).

Local interleaved A/B (Node, min-of-5): plain-object stores within noise, class-instance reads at parity, combined workload at parity. Opening as a PR specifically to get CodSpeed's read on it.

Size: createStore scenario 12.8/12.85 kB brotlied (+70 B vs next).

Test plan

  • 10 new regression tests in tests/store/native-collections.test.ts: Map/Set/Date reads (internal-slot accessors, iteration), draft mutations, raw identity, subclass tags, reassignment tracking, user-class getter/method reactivity on the same store, null-proto objects, and the discriminator case from the issue.
  • Full signals suite (1186) and full monorepo suite pass.

Made with Cursor

… only (#2952)

Native code brand-checks internal slots and throws when invoked with a
store proxy as `this`: `store.map.size` (native accessor, proxy
receiver) and draft `s.map.set(...)` (unbound method, draft `this`)
both crashed with "called on incompatible receiver", while method
READS half-worked (raw-bound) — an unmarked Map was wrapped but never
honestly supported.

Rather than teaching the trap per-callsite receiver conventions, fix
wrappability: isWrappable now wraps plain data and user class
instances only. Platform objects (Map, Set, Date, URL, DOM nodes,
host objects) are excluded structurally by the tag check — user
classes stringify as `[object Object]` while every native carries its
own brand, subclasses included. They get the markRaw-children
contract automatically: served raw, mutations land raw, and the
property holding them still tracks (reassignment notifies).

Perf: hot path (plain/null-proto) resolves on one getPrototypeOf —
intrinsic, no property-lookup IC (the .constructor variant regressed
~50% from megamorphic loads). The custom-proto verdict is memoized
per prototype, so each class pays the tag call once ever: class-read
benchmarks at parity, combined store workload at parity. Size: +70 B
brotlied on the createStore scenario, within budget.

The explicit `instanceof Node` check stays, cold-branch only: real DOM
nodes are excluded by tag, but shimmed DOMs implement nodes as plain
user classes that pass it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@changeset-bot

changeset-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ef01b13

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/signals Patch
test-integration Patch
solid-js Patch
babel-preset-solid Patch
@solidjs/web Patch
@solidjs/html Patch
@solidjs/h Patch
@solidjs/universal Patch
@solidjs/element Patch
hackernews-spa-example Patch
hackernews-example Patch

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

@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
signals: core floor (createSignal/Memo/Effect/Root/flush) 6.84 KB (0%)
signals: + createStore 12.5 KB (+0.58% 🔺)
signals: + isPending/latest 8.43 KB (0%)
app: render + one signal (the simple-app floor) 9.41 KB (0%)
app: CSR with Show/For/Loading/Errored/lazy 11.43 KB (+0.05% 🔺)

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 30380098130

Warning

No base build found for commit 0208ff2 on next.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 75.417%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 785
Covered Lines: 633
Line Coverage: 80.64%
Relevant Branches: 594
Covered Branches: 407
Branch Coverage: 68.52%
Branches in Coverage %: Yes
Coverage Strength: 14.06 hits per line

💛 - Coveralls

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 117 skipped benchmarks1


Comparing fix/store-platform-objects-raw (ef01b13) with next (0208ff2)

Open in CodSpeed

Footnotes

  1. 117 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ryansolid
ryansolid merged commit cce7dcb into next Jul 28, 2026
5 checks passed
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.

2 participants