Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/observer-row-container-sharing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/db': patch
---

Preserve live-query observer data arrays and lazy state Maps across status-only and health-only snapshot updates. Keep value and ordering changes visible while retaining point-in-time snapshots.

Keep snapshot invalidation fresh across discarded Collection publications. Publish nested record prototype changes, while preserving equal writes with the same shape.

Keep common equal object-valued Set comparisons linear, with polynomial matching for ambiguous unordered members. Preserve the exported deepEquals record policy while distinguishing prototype changes in Collection rows. Reject observer inputs without valid Collection revision clocks. Keep pooled live-query observers working when layout changes use their ordinary change stream.

Compare every array position so replacing a hole with a defined value publishes the change and refreshes observers.
2 changes: 1 addition & 1 deletion docs/contributing/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ crossing subsystem boundaries; these comparisons are not interchangeable.
| Term | Meaning |
| --- | --- |
| D2 value identity | The value domain compared by `hash` and `equalHashValues`, including their documented reference leaves. A matching hash digest alone does not establish identity. The [identity oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts) defines its bounded value rules. Map/Set insertion-order sensitivity is pinned current behavior, not a universal D2 operator contract. |
| change-event equality | The `deepEquals` comparison used for Collection change-event suppression. Its supported domain ignores Map/Set insertion order, RegExp `lastIndex`, and array holes versus `undefined`. The [change-event equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts) defines its value-class rules. |
| change-event equality | The `sameRowValue` comparison used for Collection change-event suppression. Its supported domain preserves record prototypes and distinguishes signed zero, while ignoring Map/Set insertion order, RegExp `lastIndex`, and array holes versus `undefined`. The [deep-equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts) defines its shared value-class rules; the eager live-source conformance owner also checks row-value fidelity. |
| draft equality | The comparison that recognizes a reverted draft value. Its [revert oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-revert-oracle.property.test.ts) preserves Map/Set insertion order, RegExp `lastIndex`, and array holes, with explicit snapshot rules for class instances. Native mutator methods have separate change-tracking laws. |
| persisted-snapshot equality | The value-shape comparison used before skipping a reconciliation reload. It preserves array holes, enumerable array properties, and exact nested prototypes. Equal keyed class, null-prototype, or cross-realm records with different prototypes require reload; Map and Set insertion order remains significant. |

Expand Down
124 changes: 119 additions & 5 deletions docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

1,338 changes: 1,338 additions & 0 deletions docs/contributing/oracle-reviews/2026-11-01-observer-row-containers.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2639,3 +2639,13 @@ Development builds show this message, with `${...}` replaced by values:
```text
limit must be nonnegative
```

<a id="error-262"></a>

## Error 262: `live-query-observer.ts`

Development builds show this message, with `${...}` replaced by values:

```text
LiveQueryObserver requires finite Collection row and layout revision clocks
```
5 changes: 5 additions & 0 deletions packages/angular-db/tests/inject-live-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ function createMockCollection<T extends object, K extends string | number>(
get _stateRevision() {
return stateRevision
},
// This unordered fixture changes membership only in row publications, so
// its row clock also invalidates every possible layout change.
get _layoutRevision() {
return stateRevision
},
on: (event: string, cb: (e: any) => void) => {
if (event === `status:change`) statusSubs.add(cb)
return () => statusSubs.delete(cb)
Expand Down
7 changes: 2 additions & 5 deletions packages/db/mangle-cache.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"pendingSyncedProjection": "a7",
"orderedLoadFailed": "ba",
"syncStartDeferred": "bb",
"getCollectionLayoutRevision": "bc",
"leaseGeneration": "bd",
"syncEntryActive": "bg",
"builderDependencies": "bh",
Expand Down Expand Up @@ -191,7 +190,6 @@
"handoffScheduled": "dO",
"requestPageCount": "dP",
"createLiveQueryRecord": "dQ",
"getCollectionRevision": "dR",
"materializeCollection": "dS",
"resetSnapshotTracking": "dU",
"sendChangesToPipeline": "dV",
Expand Down Expand Up @@ -247,7 +245,6 @@
"cleanupRetiredEntries": "eR",
"comparisonOpts": "eS",
"createSyncMetadataApi": "eT",
"lastLayoutKeys": "eU",
"pendingSourceRecovery": "eV",
"composeBatchedChange": "eW",
"ensureStandardSchema": "eX",
Expand Down Expand Up @@ -290,7 +287,6 @@
"getSyncConfig": "fE",
"prepareChange": "fF",
"applyChanges": "fG",
"entriesEqual": "fH",
"leaseVersion": "fI",
"loadBoundary": "fJ",
"loadPage": "fK",
Expand Down Expand Up @@ -434,5 +430,6 @@
"awaitsSiblingReplays": "gN",
"grownPrefixRows": "gZ",
"ownPrefixEnd": "he",
"sourceListenerCleanups": "cb"
"sourceListenerCleanups": "cb",
"cachedRows": "dB"
}
15 changes: 9 additions & 6 deletions packages/db/src/collection/changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export class CollectionChangesManager<
| {
depth: number
discard: boolean
stateRevision: number
layoutRevision: number
publications: Array<{
changes: Array<ChangeMessage<TOutput, TKey>>
Expand All @@ -63,16 +62,17 @@ export class CollectionChangesManager<

/**
* Monotonic revision of the collection's visible state, advanced once per
* committed batch of changes and cleanup β€” including while nothing is subscribed.
* committed batch of changes, cleanup, and discarded layout epochs β€” even
* while nothing is subscribed.
* Lets consumers (the live-query observer) cheaply detect "did the data
* change" without subscribing, and stays untouched by subscription
* bootstrap replays, which do not go through emitEvents.
*/
public stateRevision = 0

/**
* Monotonic revision advanced only for explicit layout-only publications.
* Observers use it to detect reordered rows whose values did not change.
* Revision of explicit layout-only publications. A discarded deferral can
* restore it; stateRevision fences that invalidation for detached observers.
*/
public layoutRevision = 0

Expand Down Expand Up @@ -216,7 +216,6 @@ export class CollectionChangesManager<
const deferral = (this.deferral ??= {
depth: 0,
discard: false,
stateRevision: this.stateRevision,
layoutRevision: this.layoutRevision,
publications: [],
})
Expand All @@ -235,7 +234,11 @@ export class CollectionChangesManager<
deferral.publications = []
this.deferral = undefined
if (deferral.discard) {
this.stateRevision = deferral.stateRevision
// State already committed, so its row clock must never rewind. A
// discarded layout clock also needs a fresh invalidation token: a
// detached observer may have read while delivery was deferred.
if (this.layoutRevision !== deferral.layoutRevision)
this.stateRevision++
this.layoutRevision = deferral.layoutRevision
return
}
Expand Down
8 changes: 5 additions & 3 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,18 @@ export class CollectionImpl<

/**
* Monotonic revision of the collection's visible state; advances once per
* committed batch of changes and cleanup, even while nothing is subscribed.
* committed batch of changes, cleanup, and discarded layout epochs, even
* while nothing is subscribed.
* Internal β€” used by the live-query observer's snapshot cache.
*/
public get _stateRevision(): number {
return this._changes.stateRevision
}

/**
* Monotonic revision of explicit layout-only publications.
* Internal β€” used to distinguish them from empty ready events.
* Revision of explicit layout-only publications, restored on discarded
* delivery. _stateRevision stays monotonic across that discard.
* Internal β€” used to distinguish layout publications from empty ready events.
*/
public get _layoutRevision(): number {
return this._changes.layoutRevision
Expand Down
Loading
Loading