Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
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
62 changes: 62 additions & 0 deletions packages/query-core/src/__tests__/clientCacheState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest'
import { QueryClient } from '../queryClient'
import { sleep } from '../utils'

describe('Client Cache State', () => {
it('should prefetch query normally when no client state is provided', async () => {
const queryClient = new QueryClient()
const key = ['test']
const serverData = 'server-data'
let fetchCount = 0

const queryFn = async () => {
fetchCount++
await sleep(10)
return serverData
}

await queryClient.prefetchQuery({
queryKey: key,
queryFn,
staleTime: 5000,
})

expect(fetchCount).toBe(1)
})

// This test describes the DESIRED behavior, which is currently NOT implemented.
// It is expected to FAIL until we implement the changes.
it('should SKIP prefetch when client has fresh data (Simulated)', async () => {
// 1. Simulate hypothetical QueryClient with clientCacheState
// We haven't implemented the types yet, so we cast to any or expect it to be ignored for now.
const clientCacheState = {
'["test-optim"]': Date.now(), // Client has fresh data right now
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// @ts-ignore - API not implemented yet
const queryClient = new QueryClient({ clientCacheState })
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const key = ['test-optim']
const serverData = 'server-data'
let fetchCount = 0

const queryFn = async () => {
fetchCount++
await sleep(10)
return serverData
}

await queryClient.prefetchQuery({
queryKey: key,
queryFn,
staleTime: 5000, // 5 seconds stale time
})

// CURRENT BEHAVIOR: fetchCount is 1 (Server fetches anyway)
// DESIRED BEHAVIOR: fetchCount should be 0 (Server skips because client has it)

// We expect this to equal 0 if our feature is working.
// For now, let's assert 0 and see it fail, proving the need for the feature.
expect(fetchCount).toBe(0)
})
})
55 changes: 40 additions & 15 deletions packages/query-core/src/queryClient.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import {
functionalUpdate,
hashKey,
hashQueryKeyByOptions,
noop,
partialMatchKey,
resolveStaleTime,
skipToken,
} from './utils'
import { QueryCache } from './queryCache'
import { MutationCache } from './mutationCache'
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { notifyManager } from './notifyManager'
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
import { MutationCache } from './mutationCache'
import { notifyManager } from './notifyManager'
import { onlineManager } from './onlineManager'
import type { QueryState } from './query'
import { QueryCache } from './queryCache'
import type {
CancelOptions,
ClientCacheState,
DefaultError,
DefaultOptions,
DefaultedQueryObserverOptions,
Expand All @@ -41,8 +34,18 @@ import type {
ResetOptions,
SetDataOptions,
} from './types'
import type { QueryState } from './query'
import type { MutationFilters, QueryFilters, Updater } from './utils'
import {
functionalUpdate,
hashKey,
hashQueryKeyByOptions,
noop,
partialMatchKey,
resolveStaleTime,
skipToken,
timeUntilStale,
} from './utils'


// TYPES

Expand All @@ -65,6 +68,7 @@ export class QueryClient {
#queryDefaults: Map<string, QueryDefaults>
#mutationDefaults: Map<string, MutationDefaults>
#mountCount: number
#clientCacheState?: ClientCacheState
#unsubscribeFocus?: () => void
#unsubscribeOnline?: () => void

Expand All @@ -75,6 +79,7 @@ export class QueryClient {
this.#queryDefaults = new Map()
this.#mutationDefaults = new Map()
this.#mountCount = 0
this.#clientCacheState = config.clientCacheState
}

mount(): void {
Expand Down Expand Up @@ -377,7 +382,27 @@ export class QueryClient {
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<void> {
return this.fetchQuery(options).then(noop).catch(noop)
const defaultedOptions = this.defaultQueryOptions(options)

if (this.#clientCacheState && defaultedOptions.staleTime !== undefined) {
const queryHash = defaultedOptions.queryHash
const clientUpdatedAt = this.#clientCacheState[queryHash]
if (
clientUpdatedAt !== undefined &&
typeof defaultedOptions.staleTime !== 'function'
) {
// If the query is static, it is always fresh
if (defaultedOptions.staleTime === 'static') {
return Promise.resolve()
}
// If the query is fresh, we can skip the prefetch
if (timeUntilStale(clientUpdatedAt, defaultedOptions.staleTime) > 0) {
return Promise.resolve()
}
}
}

return this.fetchQuery(defaultedOptions).then(noop).catch(noop)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fetchInfiniteQuery<
Expand Down
9 changes: 6 additions & 3 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* istanbul ignore file */

import type { QueryClient } from './queryClient'
import type { DehydrateOptions, HydrateOptions } from './hydration'
import type { MutationState } from './mutation'
import type { MutationCache } from './mutationCache'
import type { FetchDirection, Query, QueryBehavior } from './query'
import type { QueryCache } from './queryCache'
import type { QueryClient } from './queryClient'
import type { RetryDelayValue, RetryValue } from './retryer'
import type { QueryFilters, QueryTypeFilter, SkipToken } from './utils'
import type { QueryCache } from './queryCache'
import type { MutationCache } from './mutationCache'

export type NonUndefinedGuard<T> = T extends undefined ? never : T

Expand Down Expand Up @@ -1352,10 +1352,13 @@ export type MutationObserverResult<
| MutationObserverErrorResult<TData, TError, TVariables, TOnMutateResult>
| MutationObserverSuccessResult<TData, TError, TVariables, TOnMutateResult>

export type ClientCacheState = Record<string, number>

export interface QueryClientConfig {
queryCache?: QueryCache
mutationCache?: MutationCache
defaultOptions?: DefaultOptions
clientCacheState?: ClientCacheState
}

export interface DefaultOptions<TError = DefaultError> {
Expand Down