From 815bf4fb79f411de75dfd31b3011464d36afa534 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:32:35 +0000 Subject: [PATCH] fix: emit change events for synced data during pending optimistic mutations (issue #1017) When a sync transaction commits while there's a pending optimistic mutation, the sync data was being queued but not visible to derived collections (live queries). This caused synced data to be invisible until the optimistic mutation completed. Root cause: commitPendingTransactions() was designed to delay processing sync transactions while there's a persisting user transaction, to avoid complex reconciliation. However, this also prevented change events from being emitted to subscribers, making the synced data invisible to live query collections that depend on those events. Fix: Add an else branch that emits change events for committed sync operations without modifying syncedData. This allows: 1. The source collection's state (syncedData) to remain unchanged during the persisting transaction (preserving existing behavior) 2. Subscribers (including live queries) to receive events about the new synced data, making it visible immediately The fix skips emitting events for keys that have pending optimistic mutations (optimisticUpserts/optimisticDeletes) to avoid conflicts. When the persisting transaction completes, the normal reconciliation flow will handle those keys. Co-Authored-By: Claude --- packages/db/src/collection/state.ts | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index b873610f6..a3a76a7be 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -825,6 +825,68 @@ export class CollectionStateManager< if (!this.hasReceivedFirstCommit) { this.hasReceivedFirstCommit = true } + } else if (committedSyncedTransactions.length > 0) { + // When there's a persisting transaction, we can't update syncedData yet, but we + // should still notify subscribers about the new synced data. This ensures that + // derived collections (live queries) see the synced data immediately. + // + // We emit events for the committed sync operations without modifying syncedData. + // The syncedData will be updated when the persisting transaction completes. + const events: Array> = [] + + for (const transaction of committedSyncedTransactions) { + for (const operation of transaction.operations) { + const key = operation.key as TKey + // Only emit events for keys that aren't affected by the pending optimistic mutation + // This ensures we don't double-emit or conflict with optimistic state + if ( + !this.optimisticUpserts.has(key) && + !this.optimisticDeletes.has(key) + ) { + // For inserts, check if the key already exists in syncedData to determine event type + if (operation.type === `insert`) { + if (this.syncedData.has(key)) { + // Key exists, this is effectively an update + events.push({ + type: `update`, + key, + value: operation.value, + previousValue: this.syncedData.get(key), + }) + } else { + // New key, emit insert + events.push({ + type: `insert`, + key, + value: operation.value, + }) + } + } else if (operation.type === `update`) { + events.push({ + type: `update`, + key, + value: operation.value, + previousValue: this.syncedData.get(key), + }) + } else { + // operation.type === 'delete' + const previousValue = this.syncedData.get(key) + if (previousValue !== undefined) { + events.push({ + type: `delete`, + key, + value: previousValue, + }) + } + } + } + } + } + + // Emit events to subscribers so derived collections see the synced data + if (events.length > 0) { + this.changes.emitEvents(events, false) + } } }