Skip to content

fix(store): reconcile() notifies symbol-keyed properties (closes #2851)#2852

Open
yumemi-thomas wants to merge 2 commits into
solidjs:nextfrom
yumemi-thomas:fix/reconcile-symbol-keys
Open

fix(store): reconcile() notifies symbol-keyed properties (closes #2851)#2852
yumemi-thomas wants to merge 2 commits into
solidjs:nextfrom
yumemi-thomas:fix/reconcile-symbol-keys

Conversation

@yumemi-thomas

Copy link
Copy Markdown

Closes #2851

Summary

reconcile() never notifies subscribers of symbol-keyed store properties. An effect or binding tracking state[SYM] doesn't re-run when reconcile() changes that property, and because the stale node then shadows the value, even a plain state[SYM] read keeps returning the old value — the reconciled value is unreachable. String-keyed properties on the same store update fine through the identical path, so the only difference is the key type.

https://stackblitz.com/edit/solidjs-templates-krnhumcg?file=src%2FApp.tsx

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

Root cause

Every key enumeration in reconcile's diff dropped symbols:

  • getAllKeys (the $TRACK-tracked path) built from getKeys (string-only) + Object.keys(next).
  • The untracked value loops and the STORE_HAS (in-dependency) loops iterated Object.keys(nodes).

So setSignal was never called for symbol-keyed nodes, while applyState* had already swapped STORE_VALUE to the new value — leaving the stale node shadowing it for direct reads too.

Fix

Enumerate symbol keys everywhere reconcile enumerates string keys, while keeping the common symbol-free path on the exact Object.keys fast path:

  • getAllKeys builds string keys via Object.keys (unchanged) and appends enumerable symbol keys via getOwnPropertySymbols — symbol-free objects pay only an empty call, no per-key predicate.
  • nodeKeys (new) enumerates a node record's keys: Object.keys always, plus symbol nodes only for records flagged in a symbolKeyedRecords WeakSet. getNode marks a record when it creates a user (non-$TRACK) symbol node, and its unobserved cleanup drops the mark once the last such node goes — so the flag means exactly "this record currently holds a user symbol node", and the reconcile hot path stays on Object.keys gated by a free WeakSet.has.
  • $TRACK (the internal self-node, the only internal symbol a node record can hold — the get/has traps early-return the others) is filtered out of symbol enumeration; callers handle it separately.

Semantics are identical to the old code for string keys, and match ownEnumerableKeys (enumerable strings + enumerable symbols) for the extended set; key order is irrelevant since each key is diffed independently.

Tests

Seven tests in tests/store/reconcile.test.ts (reconcile with symbol-keyed properties), the symbol cases confirmed failing red-first on the unfixed source:

  • effect tracking a symbol-keyed property is notified; the value is reachable, not shadowed
  • string control updates through the identical path
  • string and symbol keys on one store both update
  • a symbol key removed by reconcile notifies undefined
  • a symbol in check updates when reconcile adds the key
  • a nested symbol-keyed value reconciles
  • perf invariant: the symbolKeyedRecords mark is set while tracked and cleared once unobserved (guards the fast-path optimization against a monotonic leak)

Full solid-signals suite: the diff versus pristine is exactly the intended flips, nothing else; solid and @solidjs/web rebuilt against the fixed package and re-run at their baselines; source and test files pass the tests-inclusive typecheck.

Solid 1.x note

Exists in 1.x too, in a worse form: 1.x reconcile doesn't even merge symbol-keyed values (the value stays stale; no notification question arises). 2.0 half-fixed this — the value merges — but subscribers of the symbol-keyed node are still never notified. Not a regression, but 2.0 already handles symbols in its other store paths (set trap, getAllKeys, merge/omit after #2769), so finishing the job in reconcile's diff loops is consistent.

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

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 023a932

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 7, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 7.61%

⚠️ 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
✅ 119 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
reconcile: deep tree, single deep() effect 80.1 ms 74.4 ms +7.61%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing yumemi-thomas:fix/reconcile-symbol-keys (023a932) with next (070d853)

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