Skip to content

[HSF-448] feat!: keep unchanged state identity across edits and snapshots - #51

Merged
TrystonPerry merged 2 commits into
masterfrom
tryston/hsf-448-structural-sharing
Oct 9, 2026
Merged

TrystonPerry merged 2 commits into
masterfrom
tryston/hsf-448-structural-sharing

Conversation

@TrystonPerry

@TrystonPerry TrystonPerry commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Every edit deep-cloned the whole collection, and every snapshot rebuilt every document. One field change gave every document a new object, so React.memo and useMemo in consumers could never skip work.

  • Edits: applyDiff copies only the objects on the changed path. Siblings and unchanged nested objects keep their identity.
  • Snapshots: each document and nested object that equals the current view is reused (replaceEqualDeep). This covers the confirmation of our own write and server write-backs during a pending edit.
  • No in-place mutation: id injection, the rebase, remote deletes, add, remove, and serverTimestamp() display overrides copy on write.
  • Dev freeze: outside production, notify() deep-freezes syncState and localState. An in-place mutation throws a TypeError instead of silently corrupting the diff baseline.
  • Docs: README.md, docs/, and AGENTS.md describe the immutability contract.
  • Version: 0.2.0.

Breaking change

State is immutable. Non-production builds throw a TypeError when code mutates data read from Firestate. applyDiff results share unchanged subtrees with their input.

Tests

  • New src/__tests__/structural-sharing.test.ts: edits, one-document snapshots, write confirmations, write-back during a pending edit, add and remove, undo, and the freeze.
  • New src/utils/diff.test.ts for the diff helpers.
  • 284 tests pass. Typecheck and build are clean.
  • The HVAKR app suite (2,583 tests) passes against this build. Only 4 of 368 app test files use a real Firestate store, so the dev app is the real check for in-place mutations.

Release

After merge, publish a GitHub release for v0.2.0 to trigger the npm publish workflow.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Every edit deep-cloned the whole collection, and every snapshot rebuilt
every document, so one field change gave every document a new object.
Reference-based memoization in consumers (React.memo, useMemo) could
never skip work.

- applyDiff copies only the objects on the changed path.
- Snapshots reuse each document and nested object that equals the
  current view (replaceEqualDeep), including our own confirmed write.
- The rebase, id injection, remote deletes, add, remove, and display
  overrides copy on write instead of mutating shared state.
- Outside production, notify() deep-freezes syncState and localState,
  so an in-place mutation throws instead of corrupting the diff baseline.

BREAKING CHANGE: state is immutable. Non-production builds throw a
TypeError when code mutates data read from Firestate. applyDiff results
share unchanged subtrees with their input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@TrystonPerry TrystonPerry self-assigned this Oct 9, 2026
add() now deep-clones its data like set() does, and applyDiff copies
arrays from a diff (reusing the current array when equal), so objects a
caller passes to add() or update() are never frozen by notify().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@TrystonPerry
TrystonPerry merged commit aac3910 into master Oct 9, 2026
1 check passed
@TrystonPerry
TrystonPerry deleted the tryston/hsf-448-structural-sharing branch October 9, 2026 23:13
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