Skip to content

Commit 4d20004

Browse files
authored
fix disabling of gc by settings gcTime=0 (#463)
1 parent 1c5e206 commit 4d20004

4 files changed

Lines changed: 154 additions & 1 deletion

File tree

.changeset/neat-queens-smile.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tanstack/electric-db-collection": patch
3+
"@tanstack/db": patch
4+
---
5+
6+
fix disabling of gc by setting `gcTime: 0` on the collection options

packages/db/src/collection.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,12 @@ export class CollectionImpl<
746746
}
747747

748748
const gcTime = this.config.gcTime ?? 300000 // 5 minutes default
749+
750+
// If gcTime is 0, GC is disabled
751+
if (gcTime === 0) {
752+
return
753+
}
754+
749755
this.gcTimeoutId = setTimeout(() => {
750756
if (this.activeSubscribersCount === 0) {
751757
this.cleanup()
@@ -784,7 +790,6 @@ export class CollectionImpl<
784790
this.activeSubscribersCount--
785791

786792
if (this.activeSubscribersCount === 0) {
787-
this.activeSubscribersCount = 0
788793
this.startGCTimer()
789794
} else if (this.activeSubscribersCount < 0) {
790795
throw new NegativeActiveSubscribersError()

packages/db/tests/collection-lifecycle.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,30 @@ describe(`Collection Lifecycle Management`, () => {
239239
unsubscribe3()
240240
expect((collection as any).activeSubscribersCount).toBe(0)
241241
})
242+
243+
it(`should handle rapid subscribe/unsubscribe correctly`, () => {
244+
const collection = createCollection<{ id: string; name: string }>({
245+
id: `rapid-sub-test`,
246+
getKey: (item) => item.id,
247+
gcTime: 1000, // Short GC time for testing
248+
sync: {
249+
sync: () => {},
250+
},
251+
})
252+
253+
// Subscribe and immediately unsubscribe multiple times
254+
for (let i = 0; i < 5; i++) {
255+
const unsubscribe = collection.subscribeChanges(() => {})
256+
expect((collection as any).activeSubscribersCount).toBe(1)
257+
unsubscribe()
258+
expect((collection as any).activeSubscribersCount).toBe(0)
259+
260+
// Should start GC timer each time
261+
expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 1000)
262+
}
263+
264+
expect(mockSetTimeout).toHaveBeenCalledTimes(5)
265+
})
242266
})
243267

244268
describe(`Garbage Collection`, () => {
@@ -325,6 +349,24 @@ describe(`Collection Lifecycle Management`, () => {
325349
// Should use default 5 minutes (300000ms)
326350
expect(mockSetTimeout).toHaveBeenCalledWith(expect.any(Function), 300000)
327351
})
352+
353+
it(`should disable GC when gcTime is 0`, () => {
354+
const collection = createCollection<{ id: string; name: string }>({
355+
id: `disabled-gc-test`,
356+
getKey: (item) => item.id,
357+
gcTime: 0, // Disabled GC
358+
sync: {
359+
sync: () => {},
360+
},
361+
})
362+
363+
const unsubscribe = collection.subscribeChanges(() => {})
364+
unsubscribe()
365+
366+
// Should not start any timer when GC is disabled
367+
expect(mockSetTimeout).not.toHaveBeenCalled()
368+
expect(collection.status).not.toBe(`cleaned-up`)
369+
})
328370
})
329371

330372
describe(`Manual Preload and Cleanup`, () => {

packages/electric-db-collection/tests/electric.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,5 +1030,105 @@ describe(`Electric Integration`, () => {
10301030
await expect(testCollection.utils.awaitTxId(300)).resolves.toBe(true)
10311031
await expect(testCollection.utils.awaitTxId(400)).resolves.toBe(true)
10321032
})
1033+
1034+
it(`should resync after garbage collection and new subscription`, () => {
1035+
// Use fake timers for this test
1036+
vi.useFakeTimers()
1037+
1038+
const config = {
1039+
id: `gc-resync-test`,
1040+
shapeOptions: {
1041+
url: `http://test-url`,
1042+
params: {
1043+
table: `test_table`,
1044+
},
1045+
},
1046+
getKey: (item: Row) => item.id as number,
1047+
startSync: true,
1048+
gcTime: 100, // Short GC time for testing
1049+
}
1050+
1051+
const testCollection = createCollection(electricCollectionOptions(config))
1052+
1053+
// Populate collection with initial data
1054+
subscriber([
1055+
{
1056+
key: `1`,
1057+
value: { id: 1, name: `Initial User` },
1058+
headers: { operation: `insert` },
1059+
},
1060+
{
1061+
key: `2`,
1062+
value: { id: 2, name: `Another User` },
1063+
headers: { operation: `insert` },
1064+
},
1065+
{
1066+
headers: { control: `up-to-date` },
1067+
},
1068+
])
1069+
1070+
// Verify initial data is present
1071+
expect(testCollection.has(1)).toBe(true)
1072+
expect(testCollection.has(2)).toBe(true)
1073+
expect(testCollection.size).toBe(2)
1074+
1075+
// Subscribe and then unsubscribe to trigger GC timer
1076+
const unsubscribe = testCollection.subscribeChanges(() => {})
1077+
unsubscribe()
1078+
1079+
// Collection should still be ready before GC timer fires
1080+
expect(testCollection.status).toBe(`ready`)
1081+
expect(testCollection.size).toBe(2)
1082+
1083+
// Fast-forward time to trigger GC (past the 100ms gcTime)
1084+
vi.advanceTimersByTime(150)
1085+
1086+
// Collection should be cleaned up
1087+
expect(testCollection.status).toBe(`cleaned-up`)
1088+
expect(testCollection.size).toBe(0)
1089+
1090+
// Reset mock call count for new subscription
1091+
const initialMockCallCount = mockSubscribe.mock.calls.length
1092+
1093+
// Subscribe again - this should restart the sync
1094+
const newUnsubscribe = testCollection.subscribeChanges(() => {})
1095+
1096+
// Should have created a new stream
1097+
expect(mockSubscribe.mock.calls.length).toBe(initialMockCallCount + 1)
1098+
expect(testCollection.status).toBe(`loading`)
1099+
1100+
// Send new data to simulate resync
1101+
subscriber([
1102+
{
1103+
key: `3`,
1104+
value: { id: 3, name: `Resynced User` },
1105+
headers: { operation: `insert` },
1106+
},
1107+
{
1108+
key: `1`,
1109+
value: { id: 1, name: `Updated User` },
1110+
headers: { operation: `insert` },
1111+
},
1112+
{
1113+
headers: { control: `up-to-date` },
1114+
},
1115+
])
1116+
1117+
// Verify the collection has resynced with new data
1118+
expect(testCollection.status).toBe(`ready`)
1119+
expect(testCollection.has(1)).toBe(true)
1120+
expect(testCollection.has(3)).toBe(true)
1121+
expect(testCollection.get(1)).toEqual({ id: 1, name: `Updated User` })
1122+
expect(testCollection.get(3)).toEqual({ id: 3, name: `Resynced User` })
1123+
expect(testCollection.size).toBe(2)
1124+
1125+
// Old data should not be present (collection was cleaned)
1126+
expect(testCollection.has(2)).toBe(false)
1127+
1128+
newUnsubscribe()
1129+
1130+
// Restore real timers
1131+
vi.useRealTimers()
1132+
})
10331133
})
10341134
})

0 commit comments

Comments
 (0)