diff --git a/.changeset/fix-gc-infinity.md b/.changeset/fix-gc-infinity.md new file mode 100644 index 000000000..1421e425e --- /dev/null +++ b/.changeset/fix-gc-infinity.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Fix `gcTime: Infinity` causing immediate garbage collection instead of disabling GC. JavaScript's `setTimeout` coerces `Infinity` to `0` via ToInt32, so we now explicitly check for non-finite values. diff --git a/packages/db/src/collection/lifecycle.ts b/packages/db/src/collection/lifecycle.ts index ee5fed5e7..097d138c1 100644 --- a/packages/db/src/collection/lifecycle.ts +++ b/packages/db/src/collection/lifecycle.ts @@ -180,8 +180,10 @@ export class CollectionLifecycleManager< const gcTime = this.config.gcTime ?? 300000 // 5 minutes default - // If gcTime is 0, GC is disabled - if (gcTime === 0) { + // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled. + // Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC, + // so we must explicitly check for non-finite values here. + if (gcTime <= 0 || !Number.isFinite(gcTime)) { return } diff --git a/packages/db/tests/collection-lifecycle.test.ts b/packages/db/tests/collection-lifecycle.test.ts index ef5a04a98..8b70b3ddc 100644 --- a/packages/db/tests/collection-lifecycle.test.ts +++ b/packages/db/tests/collection-lifecycle.test.ts @@ -360,6 +360,26 @@ describe(`Collection Lifecycle Management`, () => { expect(mockSetTimeout).not.toHaveBeenCalled() expect(collection.status).not.toBe(`cleaned-up`) }) + + it(`should disable GC when gcTime is Infinity`, () => { + const collection = createCollection<{ id: string; name: string }>({ + id: `infinity-gc-test`, + getKey: (item) => item.id, + gcTime: Infinity, // Disabled GC via Infinity + sync: { + sync: () => {}, + }, + }) + + const subscription = collection.subscribeChanges(() => {}) + subscription.unsubscribe() + + // Should not start any timer when gcTime is Infinity + // Note: Without this fix, setTimeout(fn, Infinity) would coerce to 0, + // causing immediate GC instead of never collecting + expect(mockSetTimeout).not.toHaveBeenCalled() + expect(collection.status).not.toBe(`cleaned-up`) + }) }) describe(`Manual Preload and Cleanup`, () => {