Skip to content

fix(db): clean up optimistic state when server returns a different key#1465

Open
goatrenterguy wants to merge 2 commits intoTanStack:mainfrom
goatrenterguy:fix/optimistic-insert-different-key-cleanup
Open

fix(db): clean up optimistic state when server returns a different key#1465
goatrenterguy wants to merge 2 commits intoTanStack:mainfrom
goatrenterguy:fix/optimistic-insert-different-key-cleanup

Conversation

@goatrenterguy
Copy link
Copy Markdown
Contributor

@goatrenterguy goatrenterguy commented Apr 8, 2026

Summary

Fixes #1442. When an onInsert/onUpdate/onDelete handler syncs server data back to the collection (via writeInsert, writeUpdate, writeUpsert, writeDelete, or refetch), the optimistic state under the original client key is now correctly removed if the server returns a different key.

Root Cause

Introduced in 9952921e (Virtual props implementation #1213). The pendingOptimisticDirectUpserts set was added to keep optimistic state visible between transaction completion and sync confirmation for correct $synced tracking. However, in recomputeOptimisticState(), completed direct transactions unconditionally re-add their mutation keys to this set on every call — even after commitPendingTransactions already cleared the key. The set is additive and never fully cleared, so entries persist forever when the server confirms under a different key.

Approach

Two coordinated mechanisms in packages/db/src/collection/state.ts:

Mechanism A — Prevent re-adding keys for transactions with sync writes:

  • directTransactionsWithSyncWrites: Set<string> tracks transaction IDs whose handlers committed immediate sync writes (via writeInsert/writeUpdate/writeDelete)
  • processedCompletedTransactions: Set<string> ensures keys are added to pendingOptimisticDirectUpserts at most once per transaction (add-once guard)
  • Together, these prevent recomputeOptimisticState from re-adding keys after sync already confirmed the data

Mechanism B — Orphan cleanup for refetch-with-different-key:

  • After commitPendingTransactions processes sync, keys in pendingOptimisticDirectUpserts that belong to completed direct transactions but aren't in syncedData are removed
  • scheduleTransactionCleanup processes queued syncs before deleting the transaction, so the orphan cleanup can find the completed transaction to identify its keys

The else branch in the completed-transaction loop is split into three cases (direct+first-time, non-direct, direct+already-processed) to prevent concurrent direct transactions from clobbering each other's entries.

Flow Result
writeInsert + different key fixed — Mechanism A prevents re-adding
writeInsert + same key fixed — add-once guard prevents re-adding after line 990 clears it
writeUpdate in onUpdate handler fixed — same mechanism as writeInsert
writeBatch with different keys fixed — same mechanism
refetch + different key fixed — Mechanism B removes orphaned key
No writeInsert + default refetch safe — unchanged behavior
No writeInsert + refetch: false safe — key survives until external sync

Test plan

Five new tests in packages/query-db-collection/tests/query.test.ts:

  • should not duplicate items when writeInsert uses a different key than the optimistic insert
  • should mark item as synced when writeInsert uses the same key as the optimistic insert
  • should not duplicate items when refetch returns a different key than the optimistic insert
  • should clean up optimistic state when writeUpdate is called in onUpdate handler
  • should not duplicate items when writeBatch uses different keys than the optimistic inserts

Full test suites pass:

  • @tanstack/db: 94 files, 2275 passed, 0 failed
  • @tanstack/query-db-collection: 2 files, 200 passed, 0 failed

goatrenterguy and others added 2 commits April 8, 2026 17:04
When an onInsert/onUpdate/onDelete handler syncs server data back to
the collection (via writeInsert, writeUpdate, writeUpsert, writeDelete,
or refetch), the optimistic state under the original client key is now
correctly removed if the server returns a different key.

Previously, pendingOptimisticDirectUpserts unconditionally re-added
client keys on every recomputeOptimisticState call, causing permanent
duplication when the server used a different key, and stale $synced:
false when using the same key.

The fix introduces two tracking sets:
- directTransactionsWithSyncWrites: prevents re-adding keys for
  transactions whose handlers committed immediate sync writes
- processedCompletedTransactions: add-once guard ensuring keys are
  only added on first transaction completion, never re-added after
  commitPendingTransactions clears them

Also adds orphan cleanup in commitPendingTransactions for the refetch-
with-different-key case, and fixes ordering in scheduleTransactionCleanup
to process queued syncs before deleting the transaction.

Closes TanStack#1442

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1465

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1465

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1465

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1465

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1465

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1465

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1465

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1465

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1465

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1465

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1465

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1465

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1465

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1465

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1465

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1465

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1465

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1465

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1465

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1465

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1465

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1465

commit: 3b32e48

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.

Optimistic insert not removed when server returns a different key

1 participant