Skip to content

fix(store): snapshot() and store materialization keep symbol-keyed props#2895

Open
yumemi-thomas wants to merge 1 commit into
solidjs:nextfrom
yumemi-thomas:fix/snapshot-symbol-keys
Open

fix(store): snapshot() and store materialization keep symbol-keyed props#2895
yumemi-thomas wants to merge 1 commit into
solidjs:nextfrom
yumemi-thomas:fix/snapshot-symbol-keys

Conversation

@yumemi-thomas

Copy link
Copy Markdown

Closes #2894

Summary

After any write to a store subtree, snapshot() of it drops symbol-keyed properties — and writing that subtree into another store drops them too. Before the first write they survive by accident (the identity-return fast path hands back the original object), so the loss only appears once the object has an override, and initial tests against untouched records pass.

This is the remaining materialization variant of #2769: that fix covered symbol keys in the set trap, storeSetter, storePath, and merge/omit via ownEnumerableKeys, and #2851 covered reconcile()'s diff loops — but the internal materialization paths still enumerated with Object.keys.

Root cause

getKeys() seeds its base key list from Object.keys(source) (strings only) and drives:

  • snapshotImplsnapshot()/deep() of any written record iterate string keys into the eagerly-created copy, so symbol props never land; and on the no-override path, symbol-keyed store-in-store values are never visited, so they leak wrapped instead of unwrapping.
  • unwrapStoreValue — the setter's store-to-store materialization, so copying a previously-written subtree into another store drops its symbol props.
  • walkAffectsScope — a keyless affects(store) never descends into symbol-keyed subtrees, so their nodes miss pending coverage.

Note the boundary: symbols living in the override layer (added after creation) were already enumerated (Reflect.ownKeys(override) in the merge loop) — the gap was specifically base-object symbols, which is why the failure needs a prior write to appear.

Fix

New symbol-aware enumeration helpers, applied at exactly those three sites; the public getKeys is unchanged, so reconcile's getAllKeys and the ownKeys trap keep their existing paths:

  • getStoreKeys — enumerable strings + enumerable symbols, honoring override adds and $DELETED removals. Plain objects stay on the Object.keys fast path and only pay one getOwnPropertySymbols call (ownEnumerableKeysPlain); wrapped store-in-store sources keep the single-pass ownEnumerableKeys so their ownKeys trap fires once, untracked.
  • getStoreSymbols — symbol-only variant for the array branches, so large index lists aren't allocated and scanned a second time; both array walks (snapshot and affects) get a separate symbol pass after the numeric loop.
  • Gating: symbol enumeration only happens inside a store backing tree (a lookup is set), so snapshot() of a plain non-store object keeps its old string-only behavior. Non-enumerable symbols are filtered, so internal slots can never leak into copies (test-pinned). Symbol-keyed accessors are skipped, matching the string-key branches.

Tests

Seventeen new tests (ten confirmed failing red-first on the unfixed source; seven are boundary-semantics controls):

  • tests/snapshot.test.ts — symbol keys survive after a write; a write inside a symbol-keyed subtree is captured; unread and written-parent symbol-keyed store-in-store values unwrap; array symbol metadata survives an index write; store-to-store materialization keeps symbols; shared references between symbol and string keys stay aliased in the copy; cycles through symbol keys preserve identity; deleted symbol keys (object and array) stay deleted; non-enumerable symbols stay excluded from copies; a symbol key added after a prior snapshot appears; untouched nested values keep symbols
  • tests/store/utilities.test.tsdeep() tracks updates in a symbol-keyed subtree, and notifies on symbol key addition and deletion
  • tests/question-scoped-pending.test.ts — keyless affects(store) covers an untouched symbol-keyed grandchild, and symbol-keyed array metadata

Full solid-signals suite: the diff versus pristine is exactly the intended flips, nothing else — including the #2893 affects-audit tests, which exercise the same subsystem as the walkAffectsScope change. solid-js and @solidjs/web rebuilt against the fixed package and re-run at their baselines; source passes the typecheck. Verified at next @ d823a806 (2.0.0-beta.19).

Performance

Micro-benched with symbol-free fixtures (the common case): no measurable change for written arrays, store-to-store writes, wide objects (~3%), or the existing reconcile/deep() benches. Snapshotting a deep tree of many small objects pays ~14% — one getOwnPropertySymbols call per visited object, which is the minimum price of seeing symbol keys at all. snapshot() is a serialization-boundary operation, not a per-read hot path. If this matters, a follow-up could gate enumeration behind a WeakSet marker like #2851's symbolKeyedRecords.

Does this exist in Solid 1.x?

Regression. Verified against 1.9.14: unwrap after a write keeps symbol keys, and copying a written subtree into another store keeps them.

Full disclosure: I wrote this with the help of an AI assistant (Claude Fable 5) and reviewed every line before pushing. All red-first tests were confirmed failing on the unfixed code.

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 85be00c

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

This PR includes changesets to release 9 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

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

@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 118 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
merge 267.7 µs 369.5 µs -27.55%
merge 278 µs 190.1 µs +46.26%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing yumemi-thomas:fix/snapshot-symbol-keys (85be00c) with next (d823a80)

Open in CodSpeed

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.

1 participant