diff --git a/.changeset/evil-maps-shine.md b/.changeset/evil-maps-shine.md new file mode 100644 index 0000000000..607568dfe8 --- /dev/null +++ b/.changeset/evil-maps-shine.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-db-collection': minor +--- + +fix: move setPersistedOwners after write to prevent row deletion when sibling live query GCs diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 5012036127..d9e37fa903 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1318,15 +1318,20 @@ export function queryCollectionOptions( }) newItemsMap.forEach((newItem, key) => { + addRow(key, hashedQueryKey) + const existingItem = currentSyncedItems.get(key) + if (!existingItem) { + write({ type: `insert`, value: newItem }) + } else if (!previouslyOwnedRows.has(key)) { + write({ type: `update`, value: newItem }) + } + // Read owners AFTER write: insert clears pending metadata, so reading here + // ensures we see the post-insert baseline and correctly re-apply ownership. const owners = getPersistedOwners(key) if (!owners.has(hashedQueryKey)) { owners.add(hashedQueryKey) setPersistedOwners(key, owners) } - addRow(key, hashedQueryKey) - if (!currentSyncedItems.has(key)) { - write({ type: `insert`, value: newItem }) - } }) commit() diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index fbb48d882c..a95d9d1a0b 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -2604,4 +2604,47 @@ describe(`Query Collections`, () => { ) }) }) + + describe(`unmounting narrow live query does not drop record from broad live query`, () => { + it(`should retain item in broad useLiveQuery when narrow eq-filter useLiveQuery unmounts`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-unmount-sibling`, + getKey: (p: Person) => p.id, + initialData: initialPersons, + }), + ) + + // List view: all items + const listResult = renderHook(() => + useLiveQuery((q) => q.from({ p: collection })), + ) + + // Wait for list to load + await waitFor(() => { + expect(listResult.result.current.data).toHaveLength(3) + }) + + // Detail view: single item with eq filter + const detailResult = renderHook(() => + useLiveQuery((q) => + q.from({ p: collection }).where(({ p }) => eq(p.id, `1`)), + ), + ) + + await waitFor(() => { + expect(detailResult.result.current.data).toHaveLength(1) + }) + + // Unmount the detail view + detailResult.unmount() + + // Wait a tick for GC to fire (gcTime: 1ms) + await new Promise((r) => setTimeout(r, 10)) + + // List should still have all 3 items + expect(listResult.result.current.data).toHaveLength(3) + expect(listResult.result.current.state.get(`1`)).toBeDefined() + }) + }) })