Skip to content

fix: preserve Firestore opaque values through the diff pipeline#15

Merged
TrystonPerry merged 1 commit into
masterfrom
fix/server-timestamp-corruption
Jun 9, 2026
Merged

fix: preserve Firestore opaque values through the diff pipeline#15
TrystonPerry merged 1 commit into
masterfrom
fix/server-timestamp-corruption

Conversation

@TrystonPerry

Copy link
Copy Markdown
Collaborator

Summary

  • Critical fix: applyDiffMutable was substituting serverTimestamp() with Timestamp.now() before the write reached Firestore — shipping client clock time instead of the real server timestamp. Every other Firestore opaque value (DocumentReference, GeoPoint, Bytes, VectorValue, arrayUnion/arrayRemove, increment, deleteField) was also at risk of being walked or stripped of its prototype by the diff/clone pipeline.
  • Introduces an isFirestoreOpaque predicate and routes computeDiff, applyDiffMutable, deepClone, and flattenDiff through it — opaque values are preserved by reference so they reach Firestore in their original form.
  • Adds per-subscription displayOverrides maps (document + collection) that capture a frozen Timestamp.now() for each serverTimestamp() path in localState, so optimistic UI keeps rendering a real Timestamp while the write is in flight. Entries are reconciled on every notify and dropped when the sentinel leaves localState.

Test plan

  • pnpm test — 132/132 passing (includes 51 new diff unit tests and 6 new integration tests pinning the C1 regression end-to-end)
  • Verify in a downstream app that serverTimestamp() writes land with server time (not client time) and that the UI shows a renderable Timestamp during the in-flight window
  • Spot-check DocumentReference, GeoPoint, Bytes, VectorValue, arrayUnion/arrayRemove, increment round-trips

`applyDiffMutable` was substituting `serverTimestamp()` with
`Timestamp.now()`, shipping client clock time to Firestore instead of
the real server timestamp. The diff/clone pipeline also mishandled
other opaque values (`DocumentReference`, `GeoPoint`, `Bytes`,
`VectorValue`, `arrayUnion`/`arrayRemove`, `increment`), either
walking their keys or stripping their prototypes.

Introduce `isFirestoreOpaque` and treat every sentinel and value type
as opaque across `computeDiff`, `applyDiffMutable`, `deepClone`, and
`flattenDiff` — preserved by reference so writes reach Firestore in
their original form.

To keep optimistic UI working now that the sentinel survives in
`localState`, document and collection subscriptions maintain a
`displayOverrides` map of frozen `Timestamp.now()` values keyed by
dotted path. `getMergedData` overlays the overrides so consumers see a
renderable Timestamp while the write is in flight; entries are dropped
when the sentinel leaves `localState` on sync ack or overwrite.
@TrystonPerry TrystonPerry merged commit 00058e1 into master Jun 9, 2026
1 check failed
@TrystonPerry TrystonPerry deleted the fix/server-timestamp-corruption branch June 9, 2026 15:02
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