Summary
useCollection / useDocument rebuild the underlying subscription whenever readOnly, the resolved path, or the semantic identity of queryConstraints changes (these are dependencies of the useMemo that creates the subscription). On rebuild, the previous subscription's cleanup calls subscription.stop(), which clears the autosave timer and discards localState. Any un-synced optimistic edit that was still inside the autosave debounce window is silently lost.
Where
Compiled dist/index.mjs (v0.1.2):
useCollection: subscription = useMemo(..., [enabled, store, definition, collectionPath, readOnly, stableConstraints, onPushUndo]) — readOnly and stableConstraints are deps.
subscribe cleanup returns () => { unsub(); subscription.stop(); }.
stop() clears autosaveTimeout and the subscription's local state goes away with the discarded closure.
useDocument has the equivalent shape.
Repro
useCollection, wait for first snapshot.
- Make an edit →
localState pending, autosave scheduled (e.g. 1000ms).
- Before autosave fires, change a prop that the subscription memo depends on — e.g. flip
readOnly true→false, or pass a new (semantically different) queryConstraints.
- The subscription is rebuilt; the old one is
stop()ed.
Expected: a pending un-synced edit is preserved across a subscription rebuild (flushed before teardown, or carried into the new subscription).
Actual: the edit is discarded with the old subscription and never written.
Why it bites in practice
readOnly is commonly derived from async-loaded auth/role state, so it flips false→true→false during app startup, rebuilding every subscription. It can also change at runtime (permission changes, entering/leaving a read-only revision view). Each flip is a window where in-flight optimistic edits on that collection are dropped. A prior hand-rolled hook kept readOnly out of the listener's effect deps for exactly this reason — toggling read-only changed the write guard without tearing down the listener or its pending state.
Suggested fix
- Don't tear the subscription down for changes that don't affect the query/identity.
readOnly is a write-time guard, not part of the subscription identity — handle it like the runtime onError/onNavigate setters (mutate on the existing subscription) instead of including it in the memo deps.
- For changes that genuinely require a new subscription (path/constraints), flush or hand off pending
localState before stop() rather than discarding it (at minimum, attempt a final sync() on teardown when localState is set).
Summary
useCollection/useDocumentrebuild the underlying subscription wheneverreadOnly, the resolved path, or the semantic identity ofqueryConstraintschanges (these are dependencies of theuseMemothat creates the subscription). On rebuild, the previous subscription's cleanup callssubscription.stop(), which clears the autosave timer and discardslocalState. Any un-synced optimistic edit that was still inside the autosave debounce window is silently lost.Where
Compiled
dist/index.mjs(v0.1.2):useCollection:subscription = useMemo(..., [enabled, store, definition, collectionPath, readOnly, stableConstraints, onPushUndo])—readOnlyandstableConstraintsare deps.subscribecleanup returns() => { unsub(); subscription.stop(); }.stop()clearsautosaveTimeoutand the subscription's local state goes away with the discarded closure.useDocumenthas the equivalent shape.Repro
useCollection, wait for first snapshot.localStatepending, autosave scheduled (e.g. 1000ms).readOnlytrue→false, or pass a new (semantically different)queryConstraints.stop()ed.Expected: a pending un-synced edit is preserved across a subscription rebuild (flushed before teardown, or carried into the new subscription).
Actual: the edit is discarded with the old subscription and never written.
Why it bites in practice
readOnlyis commonly derived from async-loaded auth/role state, so it flipsfalse→true→falseduring app startup, rebuilding every subscription. It can also change at runtime (permission changes, entering/leaving a read-only revision view). Each flip is a window where in-flight optimistic edits on that collection are dropped. A prior hand-rolled hook keptreadOnlyout of the listener's effect deps for exactly this reason — toggling read-only changed the write guard without tearing down the listener or its pending state.Suggested fix
readOnlyis a write-time guard, not part of the subscription identity — handle it like the runtimeonError/onNavigatesetters (mutate on the existing subscription) instead of including it in the memo deps.localStatebeforestop()rather than discarding it (at minimum, attempt a finalsync()on teardown whenlocalStateis set).