Skip to content

fix(#26): preserve un-synced localState across subscription rebuild#27

Open
TrystonPerry wants to merge 2 commits into
masterfrom
claude/sweet-darwin-g3fhl9
Open

fix(#26): preserve un-synced localState across subscription rebuild#27
TrystonPerry wants to merge 2 commits into
masterfrom
claude/sweet-darwin-g3fhl9

Conversation

@TrystonPerry

Copy link
Copy Markdown
Collaborator

Summary

  • Removes readOnly from the useMemo dependency array in both useDocument and useCollection. readOnly is now a write-time guard applied via a stable ref-backed getter (getReadOnly), using the same pattern already used for undoable/onPushUndo. Toggling readOnly at runtime no longer tears down the subscription or discards pending local state.
  • Adds getReadOnly?: () => boolean | undefined to DocumentOptions and CollectionOptions (backward-compatible — the static readOnly option still works for direct API callers).
  • Adds a fire-and-forget sync() call in both hook cleanup functions, so genuine subscription rebuilds (path/query changes) attempt to flush pending edits before teardown rather than silently dropping them.
  • Adds a stopped flag to both subscription factories to prevent an async sync() completing after teardown from re-registering a stale sync key and corrupting useIsSynced.

Changes

File Change
src/document.ts Add getReadOnly option; resolveReadOnly() replaces isReadOnly const; stopped flag guards reportSyncState in notify() and is set in stop()
src/collection.ts Same changes as document.ts
src/hooks.ts readOnlyRef + stable getReadOnly callback in both hooks; readOnly removed from memo deps; fire-and-forget sync() added to subscription cleanup
src/firestate.integration.test.ts 5 new tests covering: getReadOnly toggle preserves document/collection localState; sync-before-stop flushes document/collection writes; stale sync key is not re-registered after stop

How to verify

  1. Mount a component with useCollection({ ..., readOnly: derivedFromAuthState }) where readOnly flips false → true → false during startup.
  2. While readOnly is briefly true, make an optimistic edit.
  3. Before fix: edit is silently lost when readOnly flips back.
  4. After fix: edit survives and syncs normally.

Repro for path-change flush: edit a document, immediately navigate (changing the path params) before autosave fires — the edit now reaches Firestore.

Follow-ups / risks

  • Undo actions captured before a path-change still reference the old subscription's closure. This is pre-existing behaviour and out of scope for this fix.
  • The fire-and-forget sync is best-effort; network failures after teardown are not surfaced anywhere. A more robust solution would hand off localState to the new subscription, but that requires a subscription handshake API.

Closes #26


Generated by Claude Code

readOnly was in the useMemo deps for both useDocument and useCollection,
so any runtime toggle (e.g. auth/role state loading) tore down the
subscription and discarded pending local edits in the autosave window.

Fix:
- Move readOnly out of the subscription memo deps for both hooks. A
  stable ref-backed getter (getReadOnly) is passed to the subscription
  factories instead, evaluated at mutation time — the same pattern used
  for `undoable`/`onPushUndo`. Toggling readOnly now changes the write
  guard in place without rebuilding the subscription.
- Add getReadOnly option to DocumentOptions and CollectionOptions (the
  static readOnly option continues to work for direct API callers).
- For genuine rebuilds (path/query change), attempt a fire-and-forget
  sync() in the hook cleanup before stop() so pending edits reach
  Firestore rather than being silently dropped.
- Add a stopped flag to both subscription factories so an async sync()
  completing after teardown cannot re-register a stale sync key in the
  global store and corrupt useIsSynced.

Closes #26

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJWMe1UYgBbCjjM618GxKT

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a2ecfbca1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/document.ts
…teardown

stop() now sets stopped=true to prevent a fire-and-forget sync() from
re-registering a stale sync key. But the retryOnError path calls
stop() followed immediately by load() (or startListener()), which
re-activates the subscription — leaving stopped=true forever and
causing notify() to skip reportSyncState permanently after a retry.

Reset stopped=false at the start of load() (document.ts) and
startListener() (collection.ts) so sync-state reporting resumes after
a successful retry, and useIsSynced correctly reflects pending edits.

Fixes review finding on PR #27 (Codex P1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJWMe1UYgBbCjjM618GxKT
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.

Subscription rebuild (readOnly / queryConstraints / path change) discards un-synced localState

2 participants