Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/seven-facts-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/query-core': patch
---

fix: use !== undefined instead of truthy check for timer IDs to correctly handle falsy value 0 in clearGcTimeout,

57 changes: 57 additions & 0 deletions packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from 'vitest'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryClient, QueryObserver, focusManager } from '..'
import { defaultTimeoutProvider, timeoutManager } from '../timeoutManager'
import type { QueryObserverResult } from '..'

describe('queryObserver', () => {
Expand Down Expand Up @@ -1626,4 +1627,60 @@ describe('queryObserver', () => {
unsubscribe2()
})
})

describe('falsy timer ID (0) handling', () => {
test('should call clearTimeout when stale timer ID is 0', async () => {
const provider = {
setTimeout: vi.fn(() => 0),
clearTimeout: vi.fn(),
setInterval: vi.fn(() => 0),
clearInterval: vi.fn(),
}
timeoutManager.setTimeoutProvider(provider)

const key = queryKey()
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => 'data',
staleTime: 100,
})

const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)

// setOptions triggers #clearStaleTimeout → must call clearTimeout(0)
observer.setOptions({ queryKey: key, queryFn: () => 'data', staleTime: 200 })

expect(provider.clearTimeout).toHaveBeenCalledWith(0)

unsubscribe()
timeoutManager.setTimeoutProvider(defaultTimeoutProvider)
})

test('should call clearInterval when refetch interval timer ID is 0', async () => {
const provider = {
setTimeout: vi.fn(() => 0),
clearTimeout: vi.fn(),
setInterval: vi.fn(() => 0),
clearInterval: vi.fn(),
}
timeoutManager.setTimeoutProvider(provider)

const key = queryKey()
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => 'data',
refetchInterval: 100,
})

const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)

unsubscribe() // destroy → #clearRefetchInterval → must call clearInterval(0)

expect(provider.clearInterval).toHaveBeenCalledWith(0)

timeoutManager.setTimeoutProvider(defaultTimeoutProvider)
})
})
})
51 changes: 51 additions & 0 deletions packages/query-core/src/__tests__/removable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { queryKey } from '@tanstack/query-test-utils'
import { QueryClient, QueryObserver } from '..'
import { defaultTimeoutProvider, timeoutManager } from '../timeoutManager'

describe('removable', () => {
let queryClient: QueryClient

beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
queryClient.mount()
})

afterEach(() => {
queryClient.clear()
vi.useRealTimers()
timeoutManager.setTimeoutProvider(defaultTimeoutProvider)
})

describe('falsy timer ID (0) handling', () => {
test('should call clearTimeout for gcTimeout when timer ID is 0', async () => {
const provider = {
setTimeout: vi.fn(() => 0),
clearTimeout: vi.fn(),
setInterval: vi.fn(() => 0),
clearInterval: vi.fn(),
}
timeoutManager.setTimeoutProvider(provider)

const key = queryKey()
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => 'data',
gcTime: 100,
})

// Subscribe then unsubscribe: no observers left → scheduleGc() sets #gcTimeout = 0
const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)
unsubscribe()

// Subscribe again: addObserver calls clearGcTimeout() → must call clearTimeout(0)
const unsubscribe2 = observer.subscribe(() => undefined)

expect(provider.clearTimeout).toHaveBeenCalledWith(0)

unsubscribe2()
})
})
})
4 changes: 2 additions & 2 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,14 @@ export class QueryObserver<
}

#clearStaleTimeout(): void {
if (this.#staleTimeoutId) {
if (this.#staleTimeoutId !== undefined) {
timeoutManager.clearTimeout(this.#staleTimeoutId)
this.#staleTimeoutId = undefined
}
}

#clearRefetchInterval(): void {
if (this.#refetchIntervalId) {
if (this.#refetchIntervalId !== undefined) {
timeoutManager.clearInterval(this.#refetchIntervalId)
this.#refetchIntervalId = undefined
}
Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/removable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export abstract class Removable {
}

protected clearGcTimeout() {
if (this.#gcTimeout) {
if (this.#gcTimeout !== undefined) {
timeoutManager.clearTimeout(this.#gcTimeout)
this.#gcTimeout = undefined
}
Expand Down
Loading