Skip to content

Commit d611b94

Browse files
authored
fix: disable GC on child collections created by includes system (#1430)
* 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. * 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
1 parent caf5166 commit d611b94

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

packages/db/src/query/live/collection-config-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,7 @@ function createChildCollectionEntry(
15151515
},
15161516
},
15171517
startSync: true,
1518+
gcTime: 0,
15181519
})
15191520

15201521
const entry: ChildCollectionEntry = {

packages/db/tests/query/includes.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, describe, expect, it, vi } from 'vitest'
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
22
import {
33
and,
44
concat,
@@ -8,6 +8,7 @@ import {
88
toArray,
99
} from '../../src/query/index.js'
1010
import { createCollection } from '../../src/collection/index.js'
11+
import { CleanupQueue } from '../../src/collection/cleanup-queue.js'
1112
import { mockSyncCollectionOptions, stripVirtualProps } from '../utils.js'
1213

1314
type Project = {
@@ -4012,4 +4013,52 @@ describe(`includes subqueries`, () => {
40124013
})
40134014
})
40144015
})
4016+
4017+
describe(`child collection garbage collection`, () => {
4018+
beforeEach(() => {
4019+
vi.useFakeTimers()
4020+
CleanupQueue.resetInstance()
4021+
})
4022+
4023+
afterEach(() => {
4024+
vi.useRealTimers()
4025+
CleanupQueue.resetInstance()
4026+
})
4027+
4028+
it(`child collections should not be garbage collected when external subscribers unmount`, async () => {
4029+
const collection = buildIncludesQuery()
4030+
await collection.preload()
4031+
4032+
// Verify child data exists
4033+
const alpha = collection.get(1) as any
4034+
expect(childItems(alpha.issues)).toEqual([
4035+
{ id: 10, title: `Bug in Alpha` },
4036+
{ id: 11, title: `Feature for Alpha` },
4037+
])
4038+
4039+
const beta = collection.get(2) as any
4040+
expect(childItems(beta.issues)).toEqual([
4041+
{ id: 20, title: `Bug in Beta` },
4042+
])
4043+
4044+
// Simulate what useLiveQuery does in React: subscribe to child collection,
4045+
// then unsubscribe when the component unmounts (e.g., virtual table scroll)
4046+
const childSub = alpha.issues.subscribeChanges(() => {})
4047+
childSub.unsubscribe()
4048+
4049+
// Advance well past the default gcTime (5 minutes = 300,000ms)
4050+
await vi.advanceTimersByTimeAsync(600_000)
4051+
4052+
// Child collection data should still be intact — the includes system
4053+
// owns these collections and manages their lifecycle via flushIncludesState.
4054+
// External GC must not destroy them.
4055+
expect(childItems(alpha.issues)).toEqual([
4056+
{ id: 10, title: `Bug in Alpha` },
4057+
{ id: 11, title: `Feature for Alpha` },
4058+
])
4059+
expect(childItems(beta.issues)).toEqual([
4060+
{ id: 20, title: `Bug in Beta` },
4061+
])
4062+
})
4063+
})
40154064
})

0 commit comments

Comments
 (0)