Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
26934a6
perf(db): serve eq-filtered live queries from shared source partition…
Oct 1, 2026
4a386bb
perf(db): faster local-only direct writes and cheaper drafts (WIP)
Oct 1, 2026
b29f56f
fix(db): keep fields added as undefined when another field reverts
Oct 1, 2026
9ecf0a1
perf(db): track flat-row updates without proxies
Oct 1, 2026
2765c1c
Merge remote-tracking branch 'origin/main' into perf-shared-live-quer…
Oct 1, 2026
a0ea7e4
test(db): witness local-only direct write fallbacks
Oct 1, 2026
39f2454
fix(db): fail pooled live queries when their source starts cleanup
Oct 1, 2026
5d2ca2a
test(db): conformance scenarios for single-source eq filters
Oct 1, 2026
9f0a239
docs(db): document pooled live queries and their coverage
Oct 1, 2026
e4da7e2
docs(db): review the pooled live query and flat tracking oracles
Oct 1, 2026
f8ade57
fix(db): forward every pooled `result.collection` member to its Colle…
Oct 1, 2026
2d36ddb
fix(db): honor gcTime for pooled live queries
Oct 1, 2026
a52ddf4
fix(db): keep PropRef.sourceAlias read-only
Oct 1, 2026
bc24f18
revert(db): let PropRef.sourceAlias be a plain own property
Oct 1, 2026
c8a4861
perf(react-db): attach live query result info only for Suspense
Oct 1, 2026
ff7079a
test(db): compare local-only direct writes with the handler path
Oct 1, 2026
a0f537e
test(db): generate exotic rows and the added-undefined revert in the …
Oct 1, 2026
6543a5a
test(db): check pooled change payloads and pending-write cleanup
Oct 1, 2026
94c22c6
fix(db): make defineProperty in update callbacks act as assignment
Oct 1, 2026
1073a95
docs: record widened pooled, flat, and local-only oracle evidence
Oct 1, 2026
a5d376e
test(db): weight the flat grammar toward the descriptor laws
Oct 1, 2026
035e8a0
test(db): reach arrival order and frozen optimistic rows in pooled ca…
Oct 1, 2026
6d08832
test(db): flip signed zeros in flat campaigns
Oct 1, 2026
edca2bc
fix: run development checks in browser bundles
Oct 1, 2026
2918b23
fix(db): refill a released partition's groups when a view resubscribes
Oct 1, 2026
1d5a816
perf(db): build pooled snapshots in one pass from the group's rows
Oct 1, 2026
f038f56
perf(react-db): keep useLiveQuery's refs in one hook slot
Oct 1, 2026
03dd1e1
perf(db): reuse the tracker's change object and one timestamp per write
Oct 1, 2026
10d0309
perf(db): allocate deepEquals' cycle map only for nested containers
Oct 1, 2026
ecc4046
perf(db): cheaper partition group keys and in-group updates
Oct 1, 2026
2a153fe
perf(db): skip the flat tracker's detach copy without assigned objects
Oct 1, 2026
15b5178
perf(db): convert literals and refs to expressions with fewer traps
Oct 1, 2026
7ab8276
perf(db): build pooled shape keys in one pass
Oct 1, 2026
969d03b
perf(react-db): skip empty deferred-sync work on subscribe
Oct 1, 2026
cb08c96
perf(db): key the virtual props cache by row key
Oct 1, 2026
cd5d493
Merge origin/main into perf-shared-live-query-partitions
Oct 1, 2026
53cd676
test(db): settle local-only direct writes through when('settled')
Oct 1, 2026
25523d0
refactor(react-db): keep useLiveQuery's state in one plain object
Oct 1, 2026
0525325
refactor(db): drop the pooled group's entries cache and listen helper
Oct 1, 2026
2ab1afc
refactor(db): trim the draft delete path, cache entry, and direct-wri…
Oct 1, 2026
54de6d7
feat: pool eq-filtered live queries in Vue, Solid, Svelte, and Angular
Oct 1, 2026
f2e0a0e
refactor(db): serve pooled views through the generic live-query observer
Oct 1, 2026
9c7c966
refactor(db): release pooled partitions on the Collections' cleanup q…
Oct 1, 2026
6a790bf
perf(db): let query builder clones keep their fresh query
Oct 1, 2026
df928a8
docs: describe pooling in every framework adapter
Oct 1, 2026
9d1dd62
docs: record pooling conformance across every adapter
Oct 1, 2026
3b99895
Merge origin/main into perf-shared-live-query-partitions
Oct 1, 2026
adbdd49
perf(db): allocate deepEquals' cycle map only for nested containers
Oct 1, 2026
d354345
test(query-db-collection): assert settled update rows through collect…
Oct 1, 2026
c7225f0
feat(db): pool eq-filtered live queries with other row conditions
Oct 1, 2026
ff262ae
test(db): drop the index-usage helper's dead entriesPassing patch
Oct 2, 2026
eb79acc
docs: describe every-Collection update speedups in the changeset
Oct 2, 2026
9e61558
chore(db): refresh the private-member mangle cache
Oct 2, 2026
0eaca7f
fix(db): draw the mutation id prefix on the first mutation
Oct 2, 2026
83eb853
fix(db): address review findings on pooled live query lifecycle
Oct 2, 2026
693c1eb
fix(db): turn pooled queries terminal when source cleanup starts
Oct 2, 2026
880b105
fix(db): send rows with getter fields to the draft proxy
Oct 2, 2026
2456558
perf(db): pool live query configs that set only their query
Oct 2, 2026
b87aee5
perf(db): identify pooled queries by their source, fields, and literals
Oct 2, 2026
cc20154
perf(db): skip enriching rows an unindexed snapshot rejects
Oct 2, 2026
01a225c
perf(db): pool eq-filtered live queries ordered by their own fields
Oct 2, 2026
be8fa8a
test(db): type the gc test's query helper for any Collection
Oct 2, 2026
0f6bcc6
refactor(db): share eq conjunct parsing between pooling and the prefi…
Oct 2, 2026
a858309
Merge origin/main into perf-shared-live-query-partitions
Oct 2, 2026
699fe3a
fix(db): default pooled queries to the live-query Collection gcTime
Oct 2, 2026
9e4e971
test(query-db-collection): restore the settled update row assertions
Oct 2, 2026
d9f25b2
fix(db): drop pooled partition groups with no rows and no watchers
Oct 2, 2026
e0544f4
Merge origin/main into perf-shared-live-query-partitions
Oct 2, 2026
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
5 changes: 5 additions & 0 deletions .changeset/cheaper-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Make updates and query building cheaper. Updates to rows whose fields are all primitives track changes without a proxy, drafts of other rows allocate less, sorted Collections no longer re-sort when an existing row changes value, published rows reuse one cached copy per key, equality checks on flat rows allocate nothing, and query building copies less. Mutation ids are now a random per-runtime prefix plus a counter instead of a random UUID per mutation; they stay unique across tabs and sessions, but are no longer bare UUIDs.
6 changes: 6 additions & 0 deletions .changeset/fix-browser-development-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/db': patch
'@tanstack/react-db': patch
---

Run development-only checks in browser development builds. The duplicate `@tanstack/db` instance check and React's development warnings (deprecated dependency arrays, unhashable query identity) skipped themselves whenever there was no `process` global, which is the case in Vite and other browser bundles even though they inline `process.env.NODE_ENV`. They now read `process.env.NODE_ENV` as bundlers expect, so an app that loads two copies of `@tanstack/db` in development throws `DuplicateDbInstanceError` as documented. Set `process.env.TANSTACK_DB_DISABLE_DUP_CHECK` to `'1'` through your bundler's `define` to turn the check off.
5 changes: 5 additions & 0 deletions .changeset/fix-draft-added-undefined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Fix an update that drops a field added as `undefined`. When a callback added a field with the value `undefined` and set another field back to its original value, the draft treated every change as reverted and reported nothing.
5 changes: 5 additions & 0 deletions .changeset/fix-draft-define-property.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Make `Object.defineProperty` inside an update callback report what assignment would. Defining a field back to its original value is no longer a change, an enumerable getter reports its value, and assigning a field the callback gave only a getter throws as it would on a plain object. Deleting a non-enumerable field the callback had written is no longer reported as a deletion, matching a plain delete.
5 changes: 5 additions & 0 deletions .changeset/local-only-direct-writes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Local-only Collections apply direct `insert`, `update`, and `delete` calls without an optimistic stage when no user handler is configured for that operation and no other transaction on the Collection is pending or persisting. The write is published once, and the returned transaction is already `completed` with `isPersisted.promise` resolved. Writes inside an ambient transaction, with a handler, or beside another unsettled transaction behave as before.
12 changes: 12 additions & 0 deletions .changeset/perf-pooled-live-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@tanstack/db': patch
'@tanstack/react-db': patch
'@tanstack/vue-db': patch
'@tanstack/solid-db': patch
'@tanstack/svelte-db': patch
'@tanstack/angular-db': patch
---

Mount and update many small filtered live queries at Redux-level cost. A live query that reads one eager source Collection, filters it by at least one `eq(field, literal)`, and has no clause besides `where` and an `orderBy` on its own fields is served from an equality partition shared by every query on those fields, in React, Vue, Solid, Svelte, and Angular. Its other `where` conditions on the row, such as `not`, `gt`, or `like`, are evaluated per query over its group. This applies to a query function, a query builder, and a `{ query }` config that sets no other option besides `queryKey` or `gcTime`. Queries with a `DbClient` (React, Svelte) or React Suspense keep a live-query Collection. Each query reads its group of rows instead of compiling a live query and subscribing to the source. With 240 such queries in React, mounting takes about 2.3 ms instead of 8.2 ms, and is the same with or without an index.

Results are unchanged: the same rows in the same order with the same values and status, including a terminal error when the source is cleaned up. Two things can differ. Rows are the source Collection's row objects rather than copies. The returned `collection` is built only when your code reads it, so its automatic id may differ, and tools that list live Collections do not see a pooled query until then.
3 changes: 3 additions & 0 deletions docs/contributing/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ production queues, caches, or semantic helpers merely to share their names.
| Collection | The public keyed data container. Capitalize it when referring to the TanStack DB type. | Relation, table, or query result. |
| source Collection | A Collection read by a query or adapter. | Source relation when the value is a public Collection. |
| live-query Collection | A Collection whose rows are produced by a live query. | Query, observer, or result set. |
| pooled live query | A live query on one source Collection whose `where` has at least one `eq(field, literal)` conjunct and otherwise reads only the row, optionally ordered by the row's own fields without a limit, served from an equality partition; its live-query Collection is built only when read. | Cached query or shared live-query Collection. |
| equality partition | Source rows grouped by the `eq`-normalized values of one set of fields, shared by every pooled live query that filters on those fields. | Index or bucket relation. |
| partition group | The rows of an equality partition whose fields equal one tuple of literals. | Bucket or active bucket. |
| relation | An internal weighted multiset maintained by D2. | Collection. |
| row | One keyed public Collection value or one relation value. Qualify source row, relation row, or public row when more than one kind appears. | Event or transaction. |
| change message | One insert, update, or delete delivered through the Collection sync boundary. | Transaction or publication. |
Expand Down
8 changes: 7 additions & 1 deletion docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

169 changes: 169 additions & 0 deletions docs/contributing/oracle-reviews/2026-10-01-pooled-live-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Pooled live query and flat change tracking oracle review

## Reviewed state and claim

Base: `84b828c7e`, merged with `origin/main` at `18abceee4`. This record reviews
the Git tree that contains it; the final commit or pull request identifies that
tree.

The pooled live query oracle claims that a live query filtered only by
`eq(field, literal)` on one eager, non-persisted source, served from an
equality partition, publishes what its live-query Collection would: the same
rows in key order, values, and status. That holds through sync transactions,
optimistic writes confirmed or rolled back, peer mounts and unmounts, and
source cleanup and restart. The claim excludes on-demand and persisted
sources, `DbClient` hydration, Suspense, and every clause beyond `eq`
conjuncts, which keep the live-query Collection.

The flat change tracking oracle claims that, for a row whose own fields are
all primitives or functions with a plain or null prototype and no symbol
keys, the flat tracker reports the same change set as the draft proxy and an
independent model, with `0` and `-0` equal. Nested values, Dates, Maps, Sets,
class instances, and symbol keys are outside it, except that they must fall
back to the proxy.

Both oracles use `mockSyncCollectionOptions` or plain rows; neither claims a
real sync adapter's behavior.

## RED and GREEN evidence

Mutants ran through each oracle file alone on the reviewed tree. Each file was
restored after each run. Every outcome below is an assertion failure.

| Mutant | Oracle | Tests failed |
| --- | --- | --- |
| Partition ignores a row's previous group | Pooled | 6 of 9 |
| Groups keep rows in arrival order | Pooled | 3 of 9 |
| Literals and fields compared without `eq` normalization | Pooled | 4 of 9 |
| Partition skips the source's initial state | Pooled | 8 of 9 |
| View reports the source's status after cleanup | Pooled | 4 of 9 |
| Partition never terminates on cleanup | Pooled | 4 of 9 |
| Partition keys groups by the first field only | Pooled | 5 of 9 |
| Update published as delete then insert | Pooled | 3 of 9 (0 of 7 before payload checks) |
| Update carries a stale `previousValue` | Pooled | 3 of 9 (0 of 7 before) |
| Update carries the old row as its value | Pooled | 3 of 9 (0 of 7 before) |
| Within-group updates dropped | Pooled | 3 of 9 |
| Partition built on a cleaned-up source starts terminal | Pooled | 3 of 9 (0 of 7 before) |
| Frozen peers drop pending optimistic rows | Pooled | 3 of 9 (0 of 7 before) |
| Flat diff with `!==` | Flat | 3 of 17 |
| Flat diff with `Object.is` alone | Flat | 3 of 17 |
| Flat diff without deletions | Flat | 5 of 17 |
| Draft proxy without the added-field revert fix | Flat | pinned history and both campaigns |
| Flat diff ignores whether the row owns a field | Flat | 4 of 17 |
| Flat drafts edit the rows in place | Flat | 9 of 17 |
| Assigned objects not detached | Flat | 1 of 17 (detach witness) |
| Frozen rows sent to the proxy | Flat | 3 of 17 |
| Proxy ignores accessors defined in the callback | Flat | 3 of 17 |
| Proxy reports a field defined back to its own value | Flat | 4 of 17 |
| Proxy accepts a getter-only field's own value | Flat | 3 of 17 |
| Proxy reports a written hidden field's delete | Flat | 3 of 17 |
| Flat compares a hidden object field by identity | Flat | 3 of 17 |

Every pooled and flat mutant above fails its pinned history and both
campaigns. The pooled grammar delivers initial rows in key order or in
reverse and weights a pending insert most peers see before cleanup; the flat
grammar weights runs that write and delete a hidden field, write an equal
object over a hidden object field, assign a getter-only field its own value, and write the opposite-signed zero
over each zero field. Before that weighting, arrival order and frozen optimistic rows
escaped about one random campaign in three, and the three descriptor
mutants escaped both campaigns. The `Object.is`-alone flat mutant escaped about one random campaign in two
until the zero-flip run.

The pooled oracle found that a pooled view followed its source's status after
cleanup instead of entering the live query's terminal error; the repair makes
the partition terminate. The flat oracle found that the draft proxy dropped a
field added as `undefined` when another field reverted, on `main` as well; the
repair treats a field the original lacks as changed. The grammar now weights
that run, so both campaigns also kill the unrepaired proxy.

The loss audit then widened both oracles. The pooled granular observer had
been checked by key membership only, so the payload and lifecycle mutants
marked "0 of 7 before" survived it. The flat grammar gained frozen rows,
non-enumerable fields, accessor and data `defineProperty`, stored drafts, and
throwing callbacks. Four shapes split the trackers, three of them on `main`'s
proxy too. The adopted rule is that defining a field acts as assigning it,
and that a non-enumerable field is row data only once the callback writes
it. The proxy now records accessors and data defines through the assignment
path, rejects a getter-only write of its own value, and ignores the delete of
a hidden field it wrote. The flat tracker now compares a hidden object field
by contents.

Local-only direct writes are compared with a local-only Collection whose
handlers resolve, across mixed multi-key batches, failing batches, schema
rejection, and every fallback for insert, update, and delete. Mutants that
ignore handler types, drop the pending or persisting check, drop the whole
transaction check, run before the ambient branch, write only the first
mutation, or never complete the transaction fail 4, 4, 4, 8, 12, 1, and 9
tests.

Two shared conformance scenarios, `eq-filter-rows` and `eq-filter-peers`, run
the pooled path under React, Vue, Solid, Svelte, and Angular. A partition that
ignores a row's previous group fails both under every adapter. `eq-filter-peers` also checks the
source's public `subscriberCount`: an adapter that declares `pooledEqFilters`
must share one subscription for two queries on the same fields. Disabling pooling or using one partition per query fails it under React;
before this check the first passed. Each adapter's driver declares
`pooledEqFilters`.

The loss audit found that a partition released its source one second after
its last listener, whatever `gcTime` its views had. A focused release-timing
test, `pooled-live-query-gc.test.ts`, now compares pooled and live-query
Collection release times. A fixed delay failed 5 of 10 cases, a missing 50 ms
floor for unsubscribed queries 1, a last-`gcTime`-wins rule 1, and releasing
at `gcTime` 0 2. Release timing is resource lifetime, so the publication
oracle does not observe it.

Pooling later admitted residual conjuncts: any `where` conjunct that reads
only the query's own row, beside at least one `eq`, is evaluated per view
with the compiler's evaluator. Peers may add `not(eq(g, literal))`, which the
model evaluates itself, and a pinned history moves rows in and out of a view
within one group. A view that ignores the residual, reports unfiltered
entries, or sends a row entering or leaving it as an update fails the
pinned history and both campaigns. Hiding part of a group makes some
earlier histories rarer, so in one full run the arrival-order,
normalization, delete-plus-insert, and dropped in-group update mutants
escaped the random campaign; the fixed campaign and their pinned histories
still kill each. Peers' literals were weighted toward the normalized values
to keep the normalization mutant in the fixed campaign.

## Pooled live query oracle

| Requirement | Outcome |
| --- | --- |
| ORC-001 Contract authority and limits | Pass. The `eq` operand rules come from `src/query/compiler/evaluators.ts`; the pooled boundary and the terminal-error rule come from the live-query architecture document's pooled section and cleanup law. The opening prose lists the omissions. |
| ORC-002 Independent judgment | Pass. `expectedKeys` uses a local `eq` over plain values. Order, values, and status come from a live-query Collection, which compiles a D2 pipeline and does not use the partition. |
| ORC-003 Distinguishable responsibilities | Pass. Contract, model, grammar, driver, and refinement check are separate marked sections. |
| ORC-004 Generated-history controls | Pass. Reconstruction: every pinned history uses only domain values and step kinds. Ablation, run per axis on the campaigns with pinned cases skipped: without Dates, `NaN`, and `-0` the normalization mutant survives; without optimistic steps, frozen peers dropping optimistic rows survives; without mounts and unmounts, the terminal-at-creation mutant survives; without cleanup-restart, the status, termination, and both pending-cleanup mutants survive; without the second conjunct, first-field grouping survives. Range: at most four rows, three peers, eight steps, and ids 0 through 3; field values weight toward the literal so rows update within their group. Exclusion: an optimistic update to an equal value, an insert of an existing id, and an update or delete of a missing id are dropped. |
| ORC-005 Production path and observation | Pass. `createPooledLiveQuery` and `createLiveQueryObserver`, the adapter seam, run in wholesale and granular mode; the conformance scenarios run through React's `useLiveQuery`. Rows, keyed state, status, layout revision, and non-materialization are observed after every step, and granular changes are compared with a reference observer by type, key, value, and previous value, then replayed against the model. |
| ORC-006 Checker calibration | Pass. Thirteen mutants, classified above. |
| ORC-007 Fixed/random replay | Pass. Fixed seed `44_502_001`, an unseeded campaign, and a replay entry share one property and budget; the file is in `test:oracles`. |
| ORC-008 Stateful-model minimality | Pass. The model keeps source rows and, per mounted peer, the frozen keys at cleanup. The pinned cleanup history distinguishes a frozen peer from one mounted after the restart. |
| ORC-009 Vocabulary mapping | Pass. Equality partition, partition group, and pooled live query are glossary terms; a peer is one mounted pooled live query. |
| ORC-010 Failure fidelity and cleanup | Pass. `withOracleCleanup` releases observers, references, and the source and keeps the check failure. |
| ORC-011 Independent second formulation | Pass. The live-query Collection is the second formulation for order, values, and status. |
| ORC-012 Review evidence | This record; the coverage map links it. |
| ORC-013 Reusable boundary law | Pass. Normalization is rejected by the Date history, arrival order by the key-order history, and following the source's status by the cleanup history. |
| ORC-014 Controlled-premise handoff | Not triggered. The claim is limited to the mock source's sync transactions and lifecycle. |

## Flat change tracking oracle

| Requirement | Outcome |
| --- | --- |
| ORC-001 Contract authority and limits | Pass. The change-set rules come from `src/proxy.ts`'s `getChanges` contract: changed fields with their final value and deleted fields as `undefined`. |
| ORC-002 Independent judgment | Pass. `expectedChanges` folds the operations over a plain copy and does not import either tracker. |
| ORC-003 Distinguishable responsibilities | Pass. Contract, model, grammar, driver, and refinement check are separate marked sections. |
| ORC-004 Generated-history controls | Pass. Reconstruction: every pinned history uses domain values and operations. Ablation: removing `NaN`, `-0`, reverts, or deletions each loses a mutant. Range: one to three rows, three fields plus one added and one non-enumerable field, frozen or not, up to six operations per row including data and accessor defines, stored drafts, and a throw. Exclusion: none in generation; non-flat rows are rejected by the fallback witness. |
| ORC-005 Production path and observation | Pass. `withFlatChangeTracking`, `withArrayChangeTracking`, and `withChangeTracking` run the same callbacks; the result change sets are observed. `collection.update` selects between them. |
| ORC-006 Checker calibration | Pass. Thirteen mutants, classified above. |
| ORC-007 Fixed/random replay | Pass. Fixed seed `44_502_101`, an unseeded campaign, and a replay entry; the file is in `test:oracles`. |
| ORC-008 Stateful-model minimality | Not triggered. The model recomputes from the operations. |
| ORC-009 Vocabulary mapping | Pass. Draft, change set, and revert follow the proxy's terms. |
| ORC-010 Failure fidelity and cleanup | Not triggered. The oracle holds no resources. |
| ORC-011 Independent second formulation | Pass. The draft proxy is the second formulation. |
| ORC-012 Review evidence | This record; the coverage map links it. |
| ORC-013 Reusable boundary law | Pass. `!==` is rejected by the `NaN` history, `Object.is` by the `-0` history, and missing deletions by the deleted-field history. |
| ORC-014 Controlled-premise handoff | Not triggered. No provider is involved. |

## Open work

- Svelte's suite reads `@tanstack/db` from its built `dist`, so a mutant
must type-check and be rebuilt before Svelte can observe it.
Loading
Loading