From 2774f87cd03b75c80da31cdc55c325d4566e9859 Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:07:20 +0300 Subject: [PATCH 1/2] fix(db): skip deletes for unsent keys in filterDuplicateInserts --- .changeset/fix-ghost-delete-sentkeys.md | 5 ++ packages/db/src/query/live/utils.ts | 4 +- ...ction-subscriber-duplicate-inserts.test.ts | 54 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-ghost-delete-sentkeys.md diff --git a/.changeset/fix-ghost-delete-sentkeys.md b/.changeset/fix-ghost-delete-sentkeys.md new file mode 100644 index 0000000000..7bd2420f26 --- /dev/null +++ b/.changeset/fix-ghost-delete-sentkeys.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +fix(db): skip deletes for items never sent to D2 in filterDuplicateInserts diff --git a/packages/db/src/query/live/utils.ts b/packages/db/src/query/live/utils.ts index c7f701124f..dc0f0d72f2 100644 --- a/packages/db/src/query/live/utils.ts +++ b/packages/db/src/query/live/utils.ts @@ -339,7 +339,9 @@ export function filterDuplicateInserts( } sentKeys.add(change.key) } else if (change.type === `delete`) { - sentKeys.delete(change.key) + if (!sentKeys.delete(change.key)) { + continue + } } filtered.push(change) } diff --git a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts index 8b9d6be57c..63fc7e00f0 100644 --- a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts +++ b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { createCollection } from '../src/collection/index.js' import { BTreeIndex } from '../src/indexes/btree-index.js' import { createLiveQueryCollection, eq } from '../src/query/index.js' +import { filterDuplicateInserts } from '../src/query/live/utils.js' import { mockSyncCollectionOptions } from './utils.js' import type { ChangeMessage } from '../src/types.js' @@ -452,4 +453,57 @@ describe(`CollectionSubscriber duplicate insert prevention`, () => { subscription.unsubscribe() }) + + describe(`filterDuplicateInserts`, () => { + it(`should skip deletes for keys never sent to D2`, () => { + const sentKeys = new Set([`1`, `2`]) + + const changes: Array> = [ + { type: `delete`, key: `3`, value: { id: `3`, value: 50 } }, + ] + + const result = filterDuplicateInserts(changes, sentKeys) + + expect(result).toHaveLength(0) + expect(sentKeys.has(`1`)).toBe(true) + expect(sentKeys.has(`2`)).toBe(true) + expect(sentKeys.has(`3`)).toBe(false) + }) + + it(`should forward deletes for keys that were sent to D2`, () => { + const sentKeys = new Set([`1`, `2`]) + + const changes: Array> = [ + { type: `delete`, key: `2`, value: { id: `2`, value: 90 } }, + ] + + const result = filterDuplicateInserts(changes, sentKeys) + + expect(result).toHaveLength(1) + expect(result[0]!.type).toBe(`delete`) + expect(result[0]!.key).toBe(`2`) + expect(sentKeys.has(`2`)).toBe(false) + }) + + it(`should handle mixed inserts and ghost deletes correctly`, () => { + const sentKeys = new Set([`1`]) + + const changes: Array> = [ + { type: `insert`, key: `2`, value: { id: `2`, value: 90 } }, + { type: `delete`, key: `3`, value: { id: `3`, value: 50 } }, + { type: `delete`, key: `1`, value: { id: `1`, value: 100 } }, + { type: `insert`, key: `4`, value: { id: `4`, value: 70 } }, + ] + + const result = filterDuplicateInserts(changes, sentKeys) + + expect(result).toHaveLength(3) + expect(result.map((c) => ({ type: c.type, key: c.key }))).toEqual([ + { type: `insert`, key: `2` }, + { type: `delete`, key: `1` }, + { type: `insert`, key: `4` }, + ]) + expect(sentKeys).toEqual(new Set([`2`, `4`])) + }) + }) }) From 2a36fbf3b0625361b60acc63d5308dd8912cc941 Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:38:39 +0300 Subject: [PATCH 2/2] test: add empty-batch corner case for filterDuplicateInserts --- .../collection-subscriber-duplicate-inserts.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts index 63fc7e00f0..db38ec6479 100644 --- a/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts +++ b/packages/db/tests/collection-subscriber-duplicate-inserts.test.ts @@ -455,6 +455,16 @@ describe(`CollectionSubscriber duplicate insert prevention`, () => { }) describe(`filterDuplicateInserts`, () => { + it(`should return empty output for empty changes and empty sentKeys`, () => { + const sentKeys = new Set() + const changes: Array> = [] + + const result = filterDuplicateInserts(changes, sentKeys) + + expect(result).toEqual([]) + expect(sentKeys.size).toBe(0) + }) + it(`should skip deletes for keys never sent to D2`, () => { const sentKeys = new Set([`1`, `2`])