Skip to content

Commit 3fcbbde

Browse files
committed
feat(prefetch,query-keys): add prefetch helpers and normalize query params
1 parent 19b2289 commit 3fcbbde

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

libs/storefront-data/src/shared/cache-config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type CacheOptions = {
66
refetchOnReconnect?: boolean
77
}
88

9+
export type PrefetchCacheOptions = Pick<CacheOptions, "staleTime" | "gcTime">
10+
911
export type CacheConfig = {
1012
static: CacheOptions
1113
semiStatic: CacheOptions
@@ -56,3 +58,14 @@ export function createCacheConfig(
5658
userData: { ...defaultCacheConfig.userData, ...overrides.userData },
5759
}
5860
}
61+
62+
export function getPrefetchCacheOptions(
63+
cacheConfig: CacheConfig,
64+
strategy: keyof CacheConfig
65+
): PrefetchCacheOptions {
66+
const config = cacheConfig[strategy]
67+
return {
68+
staleTime: config.staleTime,
69+
gcTime: config.gcTime,
70+
}
71+
}

libs/storefront-data/src/shared/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
export { createCacheConfig, defaultCacheConfig } from "./cache-config"
1+
export {
2+
createCacheConfig,
3+
defaultCacheConfig,
4+
getPrefetchCacheOptions,
5+
} from "./cache-config"
26
export type {
37
CacheConfig,
48
CacheConfigOverrides,
59
CacheOptions,
10+
PrefetchCacheOptions,
611
} from "./cache-config"
712
export { createMedusaSdk } from "./medusa-client"
813
export type {
@@ -12,8 +17,17 @@ export type {
1217
} from "./medusa-client"
1318
export { createQueryClientConfig, getQueryClient, makeQueryClient } from "./query-client"
1419
export type { QueryClientConfig } from "./query-client"
15-
export { createQueryKey, createQueryKeyFactory } from "./query-keys"
16-
export type { QueryKey, QueryNamespace } from "./query-keys"
20+
export {
21+
createQueryKey,
22+
createQueryKeyFactory,
23+
normalizeQueryKeyPart,
24+
normalizeQueryKeyParams,
25+
} from "./query-keys"
26+
export type {
27+
NormalizeQueryKeyParamsOptions,
28+
QueryKey,
29+
QueryNamespace,
30+
} from "./query-keys"
1731
export type {
1832
InfiniteQueryOptions,
1933
InfiniteQueryResult,
@@ -27,3 +41,5 @@ export type {
2741
} from "./hook-types"
2842
export type { RegionInfo } from "./region"
2943
export { RegionProvider, useRegionContext } from "./region-context"
44+
export { isQueryFresh, shouldSkipPrefetch } from "./prefetch"
45+
export type { PrefetchSkipMode } from "./prefetch"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { QueryClient } from "@tanstack/react-query"
2+
import type { CacheOptions } from "./cache-config"
3+
import type { QueryKey } from "./query-keys"
4+
5+
export type PrefetchSkipMode = "fresh" | "any"
6+
7+
export const isQueryFresh = (
8+
queryClient: QueryClient,
9+
queryKey: QueryKey,
10+
staleTime: number
11+
) => {
12+
const state = queryClient.getQueryState(queryKey)
13+
if (!state || state.isInvalidated || state.data === undefined) {
14+
return false
15+
}
16+
return Date.now() - state.dataUpdatedAt < staleTime
17+
}
18+
19+
export const shouldSkipPrefetch = (params: {
20+
queryClient: QueryClient
21+
queryKey: QueryKey
22+
cacheOptions: Pick<CacheOptions, "staleTime">
23+
skipIfCached: boolean
24+
skipMode: PrefetchSkipMode
25+
}) => {
26+
if (!params.skipIfCached) {
27+
return false
28+
}
29+
30+
if (params.skipMode === "any") {
31+
return params.queryClient.getQueryData(params.queryKey) !== undefined
32+
}
33+
34+
return isQueryFresh(
35+
params.queryClient,
36+
params.queryKey,
37+
params.cacheOptions.staleTime
38+
)
39+
}

libs/storefront-data/src/shared/query-keys.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export type QueryKey = readonly unknown[]
22

33
export type QueryNamespace = string | readonly string[]
4+
export type NormalizeQueryKeyParamsOptions = {
5+
omitKeys?: readonly string[]
6+
}
47

58
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
69
if (!value || typeof value !== "object") {
@@ -45,6 +48,94 @@ const stableValue = (value: unknown, visited: WeakSet<object>): unknown => {
4548
return value
4649
}
4750

51+
const normalizeValue = (
52+
value: unknown,
53+
visited: WeakSet<object>,
54+
omitKeys: ReadonlySet<string>
55+
): unknown => {
56+
if (Array.isArray(value)) {
57+
if (visited.has(value)) {
58+
throw new Error("QueryKey contains a circular reference")
59+
}
60+
visited.add(value)
61+
const result = value
62+
.map((entry) => normalizeValue(entry, visited, omitKeys))
63+
.filter((entry) => entry !== undefined)
64+
visited.delete(value)
65+
return result
66+
}
67+
68+
if (isPlainObject(value)) {
69+
if (visited.has(value)) {
70+
throw new Error("QueryKey contains a circular reference")
71+
}
72+
visited.add(value)
73+
const entries = Object.entries(value).sort(([a], [b]) =>
74+
a.localeCompare(b)
75+
)
76+
const result: Record<string, unknown> = {}
77+
for (const [key, entryValue] of entries) {
78+
if (omitKeys.has(key) || entryValue === undefined) {
79+
continue
80+
}
81+
const normalizedValue = normalizeValue(entryValue, visited, omitKeys)
82+
if (normalizedValue === undefined) {
83+
continue
84+
}
85+
result[key] = normalizedValue
86+
}
87+
visited.delete(value)
88+
return result
89+
}
90+
91+
return value
92+
}
93+
94+
/**
95+
* Normalizes object-like query params before putting them into query keys.
96+
*
97+
* - Removes `undefined` values recursively
98+
* - Removes keys listed in `omitKeys` (for non-cache-affecting flags like `enabled`)
99+
* - Sorts object keys for stable hashing
100+
*/
101+
export function normalizeQueryKeyParams<TParams extends Record<string, unknown>>(
102+
params: TParams,
103+
options?: NormalizeQueryKeyParamsOptions
104+
): Record<string, unknown> {
105+
if (!isPlainObject(params)) {
106+
throw new Error(
107+
"QueryKey params must be a plain object. Use a serializer before normalizeQueryKeyParams."
108+
)
109+
}
110+
const visited = new WeakSet<object>()
111+
const omitKeys = new Set(options?.omitKeys ?? [])
112+
const normalized = normalizeValue(params, visited, omitKeys)
113+
if (isPlainObject(normalized)) {
114+
return normalized
115+
}
116+
return {}
117+
}
118+
119+
/**
120+
* Safe normalization for query-key parts used by hook factories.
121+
*
122+
* - Plain objects are normalized via `normalizeQueryKeyParams`
123+
* - `undefined` maps to `{}` for stable optional key parts
124+
* - Other values are passed through the stable serializer
125+
*/
126+
export function normalizeQueryKeyPart(
127+
value: unknown,
128+
options?: NormalizeQueryKeyParamsOptions
129+
): unknown {
130+
if (value === undefined) {
131+
return {}
132+
}
133+
if (isPlainObject(value)) {
134+
return normalizeQueryKeyParams(value, options)
135+
}
136+
return stableValue(value, new WeakSet<object>())
137+
}
138+
48139
export function createQueryKey(
49140
namespace: QueryNamespace,
50141
...parts: readonly unknown[]

0 commit comments

Comments
 (0)