From 8f1b8b3e14deb4ab15b5839cb2c66c0e817ad15e Mon Sep 17 00:00:00 2001 From: Balaji Raghavan Date: Sat, 28 Mar 2026 15:17:19 -0500 Subject: [PATCH 1/2] test: add regression test for child collection GC data loss Child collections created by the includes system (nested subqueries in .select()) inherit the default gcTime of 5 minutes. When the only React subscriber unmounts (e.g., virtual table scrolling), the collection gets garbage collected and data is permanently lost. This test subscribes to a child collection, unsubscribes (simulating component unmount), advances past the GC timeout, and asserts the data is still intact. It currently fails, proving the bug. --- packages/db/tests/query/includes.test.ts | 51 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/db/tests/query/includes.test.ts b/packages/db/tests/query/includes.test.ts index 727050647..9d923bdfa 100644 --- a/packages/db/tests/query/includes.test.ts +++ b/packages/db/tests/query/includes.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { and, concat, @@ -8,6 +8,7 @@ import { toArray, } from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' +import { CleanupQueue } from '../../src/collection/cleanup-queue.js' import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js' type Project = { @@ -4012,4 +4013,52 @@ describe(`includes subqueries`, () => { }) }) }) + + describe(`child collection garbage collection`, () => { + beforeEach(() => { + vi.useFakeTimers() + CleanupQueue.resetInstance() + }) + + afterEach(() => { + vi.useRealTimers() + CleanupQueue.resetInstance() + }) + + it(`child collections should not be garbage collected when external subscribers unmount`, async () => { + const collection = buildIncludesQuery() + await collection.preload() + + // Verify child data exists + const alpha = collection.get(1) as any + expect(childItems(alpha.issues)).toEqual([ + { id: 10, title: `Bug in Alpha` }, + { id: 11, title: `Feature for Alpha` }, + ]) + + const beta = collection.get(2) as any + expect(childItems(beta.issues)).toEqual([ + { id: 20, title: `Bug in Beta` }, + ]) + + // Simulate what useLiveQuery does in React: subscribe to child collection, + // then unsubscribe when the component unmounts (e.g., virtual table scroll) + const childSub = alpha.issues.subscribeChanges(() => {}) + childSub.unsubscribe() + + // Advance well past the default gcTime (5 minutes = 300,000ms) + await vi.advanceTimersByTimeAsync(600_000) + + // Child collection data should still be intact — the includes system + // owns these collections and manages their lifecycle via flushIncludesState. + // External GC must not destroy them. + expect(childItems(alpha.issues)).toEqual([ + { id: 10, title: `Bug in Alpha` }, + { id: 11, title: `Feature for Alpha` }, + ]) + expect(childItems(beta.issues)).toEqual([ + { id: 20, title: `Bug in Beta` }, + ]) + }) + }) }) From 2a13acf83378dba58a057a5da99801f2c478a6cf Mon Sep 17 00:00:00 2001 From: Balaji Raghavan Date: Sat, 28 Mar 2026 15:17:36 -0500 Subject: [PATCH 2/2] fix: disable GC on child collections created by includes system Add gcTime: 0 to createChildCollectionEntry so child collections are not subject to time-based garbage collection. Child collection lifecycle is already managed by flushIncludesState Phase 5, which removes them from childRegistry when parent rows are deleted. External GC is unnecessary and causes permanent data loss when React subscribers unmount (e.g., virtual table scrolling, tab switching, conditional rendering). Fixes #1429 --- packages/db/src/query/live/collection-config-builder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index 79e6f2bd0..20fb26f79 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -1515,6 +1515,7 @@ function createChildCollectionEntry( }, }, startSync: true, + gcTime: 0, }) const entry: ChildCollectionEntry = {