From 290d54604d2007904053c47fe6d07b27a09ea5f2 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Thu, 2 Jul 2026 10:11:43 +0100 Subject: [PATCH 1/4] fix: avoid full row origin snapshots --- packages/db/src/collection/state.ts | 33 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index 9cbdebb234..5f4449c3b0 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -226,6 +226,21 @@ export class CollectionStateManager< }) } + private snapshotRowOriginsForKeys( + keys: Iterable, + ): Map { + const rowOrigins = new Map() + + for (const key of keys) { + const origin = this.rowOrigins.get(key) + if (origin !== undefined) { + rowOrigins.set(key, origin) + } + } + + return rowOrigins + } + private enrichWithVirtualPropsSnapshot( row: TOutput, virtualProps: VirtualRowProps, @@ -476,7 +491,7 @@ export class CollectionStateManager< const previousState = new Map(this.optimisticUpserts) const previousDeletes = new Set(this.optimisticDeletes) - const previousRowOrigins = new Map(this.rowOrigins) + const previousRowOrigins = this.rowOrigins // Update pending optimistic state for completed/failed transactions for (const transaction of this.transactions.values()) { @@ -857,10 +872,6 @@ export class CollectionStateManager< // Set flag to prevent redundant optimistic state recalculations this.isCommittingSyncTransactions = true - const previousRowOrigins = new Map(this.rowOrigins) - const previousOptimisticUpserts = new Map(this.optimisticUpserts) - const previousOptimisticDeletes = new Set(this.optimisticDeletes) - // Get the optimistic snapshot from the truncate transaction (captured when truncate() was called) const truncateOptimisticSnapshot = hasTruncateSync ? committedSyncedTransactions.find((t) => t.truncate) @@ -880,6 +891,18 @@ export class CollectionStateManager< } } + const virtualSnapshotKeys = new Set(changedKeys) + for (const key of this.pendingOptimisticDirectUpserts) { + virtualSnapshotKeys.add(key) + } + for (const key of this.pendingOptimisticDirectDeletes) { + virtualSnapshotKeys.add(key) + } + const previousRowOrigins = + this.snapshotRowOriginsForKeys(virtualSnapshotKeys) + const previousOptimisticUpserts = new Map(this.optimisticUpserts) + const previousOptimisticDeletes = new Set(this.optimisticDeletes) + // Use pre-captured state if available (from optimistic scenarios), // otherwise capture current state (for pure sync scenarios) let currentVisibleState = this.preSyncVisibleState From 73d64e4e7c71a7b41b8aab39a0334b42588bab69 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Thu, 2 Jul 2026 12:02:16 +0100 Subject: [PATCH 2/4] chore: add row origin snapshot changeset --- .changeset/quiet-row-origins.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-row-origins.md diff --git a/.changeset/quiet-row-origins.md b/.changeset/quiet-row-origins.md new file mode 100644 index 0000000000..0f10864dc5 --- /dev/null +++ b/.changeset/quiet-row-origins.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Avoid full row origin snapshots during incremental collection updates. From 386006b9b1ebe7da8305c7276d31d0ff8c7033cf Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 2 Jul 2026 09:59:25 +0200 Subject: [PATCH 3/4] =?UTF-8?q?perf(db):=20make=20Transaction.applyMutatio?= =?UTF-8?q?ns=20O(n)=20instead=20of=20O(n=C2=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyMutations did a findIndex scan over the existing mutations array for every incoming mutation, making bulk operations quadratic: a single insert() of 50k rows spent most of its time in this scan. Merge through a globalKey-keyed Map instead, preserving insertion order and rebuilding the array in place to keep its identity for external holders. Co-Authored-By: Claude Fable 5 --- packages/db/src/transactions.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/db/src/transactions.ts b/packages/db/src/transactions.ts index fe2f61c0fd..84cbbcfe58 100644 --- a/packages/db/src/transactions.ts +++ b/packages/db/src/transactions.ts @@ -334,27 +334,39 @@ class Transaction> { * @param mutations - Array of new mutations to apply */ applyMutations(mutations: Array>): void { + // Merge via a globalKey-keyed map rather than a findIndex scan per + // mutation, which is O(n²) for bulk operations (e.g. inserting many rows + // in one call). Map preserves insertion order, matching the previous + // replace-in-place / remove / append semantics. + const merged = new Map>() + for (const mutation of this.mutations) { + merged.set(mutation.globalKey, mutation) + } + for (const newMutation of mutations) { - const existingIndex = this.mutations.findIndex( - (m) => m.globalKey === newMutation.globalKey, - ) + const existingMutation = merged.get(newMutation.globalKey) - if (existingIndex >= 0) { - const existingMutation = this.mutations[existingIndex]! + if (existingMutation) { const mergeResult = mergePendingMutations(existingMutation, newMutation) if (mergeResult === null) { // Remove the mutation (e.g., delete after insert cancels both) - this.mutations.splice(existingIndex, 1) + merged.delete(newMutation.globalKey) } else { // Replace with merged mutation - this.mutations[existingIndex] = mergeResult + merged.set(newMutation.globalKey, mergeResult) } } else { // Insert new mutation - this.mutations.push(newMutation) + merged.set(newMutation.globalKey, newMutation) } } + + // Rebuild in place to preserve the array's identity for external holders + this.mutations.length = 0 + for (const mutation of merged.values()) { + this.mutations.push(mutation) + } } /** From 02889efece664f32d2c4498abc21d9dfd87c382c Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Thu, 2 Jul 2026 14:30:49 +0100 Subject: [PATCH 4/4] chore: update perf changeset --- .changeset/quiet-row-origins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-row-origins.md b/.changeset/quiet-row-origins.md index 0f10864dc5..06b28d6833 100644 --- a/.changeset/quiet-row-origins.md +++ b/.changeset/quiet-row-origins.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Avoid full row origin snapshots during incremental collection updates. +Avoid full row origin snapshots during incremental collection updates and make bulk mutation merging linear.