fix(db): clean up optimistic state when server returns a different key#1465
Open
goatrenterguy wants to merge 2 commits intoTanStack:mainfrom
Open
fix(db): clean up optimistic state when server returns a different key#1465goatrenterguy wants to merge 2 commits intoTanStack:mainfrom
goatrenterguy wants to merge 2 commits intoTanStack:mainfrom
Conversation
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>
More templates
@tanstack/angular-db
@tanstack/browser-db-sqlite-persistence
@tanstack/capacitor-db-sqlite-persistence
@tanstack/cloudflare-durable-objects-db-sqlite-persistence
@tanstack/db
@tanstack/db-ivm
@tanstack/db-sqlite-persistence-core
@tanstack/electric-db-collection
@tanstack/electron-db-sqlite-persistence
@tanstack/expo-db-sqlite-persistence
@tanstack/node-db-sqlite-persistence
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/react-native-db-sqlite-persistence
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/tauri-db-sqlite-persistence
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1442. When an
onInsert/onUpdate/onDeletehandler syncs server data back to the collection (viawriteInsert,writeUpdate,writeUpsert,writeDelete, orrefetch), 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). ThependingOptimisticDirectUpsertsset was added to keep optimistic state visible between transaction completion and sync confirmation for correct$syncedtracking. However, inrecomputeOptimisticState(), completed direct transactions unconditionally re-add their mutation keys to this set on every call — even aftercommitPendingTransactionsalready 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 (viawriteInsert/writeUpdate/writeDelete)processedCompletedTransactions: Set<string>ensures keys are added topendingOptimisticDirectUpsertsat most once per transaction (add-once guard)recomputeOptimisticStatefrom re-adding keys after sync already confirmed the dataMechanism B — Orphan cleanup for refetch-with-different-key:
commitPendingTransactionsprocesses sync, keys inpendingOptimisticDirectUpsertsthat belong to completed direct transactions but aren't insyncedDataare removedscheduleTransactionCleanupprocesses queued syncs before deleting the transaction, so the orphan cleanup can find the completed transaction to identify its keysThe
elsebranch 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.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 insertshould mark item as synced when writeInsert uses the same key as the optimistic insertshould not duplicate items when refetch returns a different key than the optimistic insertshould clean up optimistic state when writeUpdate is called in onUpdate handlershould not duplicate items when writeBatch uses different keys than the optimistic insertsFull test suites pass:
@tanstack/db: 94 files, 2275 passed, 0 failed@tanstack/query-db-collection: 2 files, 200 passed, 0 failed