Skip to content

Commit 95dad65

Browse files
committed
fix(experimental_createQueryPersister): return more utilities, rename persister
1 parent acb5d37 commit 95dad65

File tree

2 files changed

+139
-47
lines changed

2 files changed

+139
-47
lines changed

packages/query-persist-client-core/src/__tests__/createPersister.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function setupPersister(
3636

3737
const queryFn = vi.fn()
3838

39-
const persisterFn = experimental_createPersister(persisterOptions)
39+
const { persisterFn } = experimental_createPersister(persisterOptions)
4040

4141
const query = new Query({
4242
cache: new QueryCache(),
@@ -202,7 +202,7 @@ describe('createPersister', () => {
202202
storageKey,
203203
JSON.stringify({
204204
buster: '',
205-
state: { dataUpdatedAt },
205+
state: { dataUpdatedAt, data: '' },
206206
}),
207207
)
208208

@@ -231,7 +231,7 @@ describe('createPersister', () => {
231231
storageKey,
232232
JSON.stringify({
233233
buster: '',
234-
state: { dataUpdatedAt: Date.now() },
234+
state: { dataUpdatedAt: Date.now(), data: '' },
235235
}),
236236
)
237237

@@ -325,7 +325,7 @@ describe('createPersister', () => {
325325
storageKey,
326326
JSON.stringify({
327327
buster: '',
328-
state: { dataUpdatedAt: Date.now() },
328+
state: { dataUpdatedAt: Date.now(), data: '' },
329329
}),
330330
)
331331

packages/query-persist-client-core/src/createPersister.ts

Lines changed: 135 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { matchQuery } from '@tanstack/query-core'
22
import type {
33
Query,
4+
QueryClient,
45
QueryFilters,
56
QueryFunctionContext,
67
QueryKey,
@@ -20,6 +21,7 @@ export interface AsyncStorage<TStorageValue = string> {
2021
getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>
2122
setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>
2223
removeItem: (key: string) => MaybePromise<void>
24+
entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>
2325
}
2426

2527
export interface StoragePersisterOptions<TStorageValue = string> {
@@ -78,7 +80,7 @@ export const PERSISTER_KEY_PREFIX = 'tanstack-query'
7880
})
7981
```
8082
*/
81-
export function experimental_createPersister<TStorageValue = string>({
83+
export function experimental_createQueryPersister<TStorageValue = string>({
8284
storage,
8385
buster = '',
8486
maxAge = 1000 * 60 * 60 * 24,
@@ -91,45 +93,42 @@ export function experimental_createPersister<TStorageValue = string>({
9193
prefix = PERSISTER_KEY_PREFIX,
9294
filters,
9395
}: StoragePersisterOptions<TStorageValue>) {
94-
return async function persisterFn<T, TQueryKey extends QueryKey>(
95-
queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
96-
context: QueryFunctionContext<TQueryKey>,
97-
query: Query,
98-
) {
99-
const storageKey = `${prefix}-${query.queryHash}`
100-
const matchesFilter = filters ? matchQuery(filters, query) : true
96+
function isExpiredOrBusted(persistedQuery: PersistedQuery) {
97+
if (persistedQuery.state.dataUpdatedAt) {
98+
const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt
99+
const expired = queryAge > maxAge
100+
const busted = persistedQuery.buster !== buster
101101

102-
// Try to restore only if we do not have any data in the cache and we have persister defined
103-
if (matchesFilter && query.state.data === undefined && storage != null) {
102+
if (expired || busted) {
103+
return true
104+
}
105+
106+
return false
107+
}
108+
109+
return true
110+
}
111+
112+
async function retrieveQuery<T>(
113+
queryHash: string,
114+
afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void,
115+
) {
116+
if (storage != null) {
117+
const storageKey = `${prefix}-${queryHash}`
104118
try {
105119
const storedData = await storage.getItem(storageKey)
106120
if (storedData) {
107121
const persistedQuery = await deserialize(storedData)
108122

109-
if (persistedQuery.state.dataUpdatedAt) {
110-
const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt
111-
const expired = queryAge > maxAge
112-
const busted = persistedQuery.buster !== buster
113-
if (expired || busted) {
114-
await storage.removeItem(storageKey)
115-
} else {
123+
if (isExpiredOrBusted(persistedQuery)) {
124+
await storage.removeItem(storageKey)
125+
} else {
126+
if (afterRestoreMacroTask) {
116127
// Just after restoring we want to get fresh data from the server if it's stale
117-
setTimeout(() => {
118-
// Set proper updatedAt, since resolving in the first pass overrides those values
119-
query.setState({
120-
dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
121-
errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
122-
})
123-
124-
if (query.isStale()) {
125-
query.fetch()
126-
}
127-
}, 0)
128-
// We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves
129-
return Promise.resolve(persistedQuery.state.data as T)
128+
setTimeout(() => afterRestoreMacroTask(persistedQuery), 0)
130129
}
131-
} else {
132-
await storage.removeItem(storageKey)
130+
// We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves
131+
return persistedQuery.state.data as T
133132
}
134133
}
135134
} catch (err) {
@@ -143,24 +142,117 @@ export function experimental_createPersister<TStorageValue = string>({
143142
}
144143
}
145144

145+
return
146+
}
147+
148+
async function persistQuery(query: Query) {
149+
if (storage != null) {
150+
const storageKey = `${prefix}-${query.queryHash}`
151+
storage.setItem(
152+
storageKey,
153+
await serialize({
154+
state: query.state,
155+
queryKey: query.queryKey,
156+
queryHash: query.queryHash,
157+
buster: buster,
158+
}),
159+
)
160+
}
161+
}
162+
163+
async function persisterFn<T, TQueryKey extends QueryKey>(
164+
queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
165+
ctx: QueryFunctionContext<TQueryKey>,
166+
query: Query,
167+
) {
168+
const matchesFilter = filters ? matchQuery(filters, query) : true
169+
170+
// Try to restore only if we do not have any data in the cache and we have persister defined
171+
if (matchesFilter && query.state.data === undefined && storage != null) {
172+
const restoredData = await retrieveQuery(
173+
query.queryHash,
174+
(persistedQuery: PersistedQuery) => {
175+
// Set proper updatedAt, since resolving in the first pass overrides those values
176+
query.setState({
177+
dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
178+
errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
179+
})
180+
181+
if (query.isStale()) {
182+
query.fetch()
183+
}
184+
},
185+
)
186+
187+
if (restoredData != null) {
188+
return Promise.resolve(restoredData as T)
189+
}
190+
}
191+
146192
// If we did not restore, or restoration failed - fetch
147-
const queryFnResult = await queryFn(context)
193+
const queryFnResult = await queryFn(ctx)
148194

149195
if (matchesFilter && storage != null) {
150196
// Persist if we have storage defined, we use timeout to get proper state to be persisted
151-
setTimeout(async () => {
152-
storage.setItem(
153-
storageKey,
154-
await serialize({
155-
state: query.state,
156-
queryKey: query.queryKey,
157-
queryHash: query.queryHash,
158-
buster: buster,
159-
}),
160-
)
197+
setTimeout(() => {
198+
persistQuery(query)
161199
}, 0)
162200
}
163201

164202
return Promise.resolve(queryFnResult)
165203
}
204+
205+
async function persisterGc() {
206+
if (storage?.entries) {
207+
const entries = await storage.entries()
208+
for (const [key, value] of entries) {
209+
if (key.startsWith(prefix)) {
210+
const persistedQuery = await deserialize(value)
211+
212+
if (isExpiredOrBusted(persistedQuery)) {
213+
await storage.removeItem(key)
214+
}
215+
}
216+
}
217+
} else if (process.env.NODE_ENV === 'development') {
218+
throw new Error(
219+
'Provided storage does not implement `entries` method. Garbage collection is not possible without ability to iterate over storage items.',
220+
)
221+
}
222+
}
223+
224+
async function persisterRestoreAll(queryClient: QueryClient) {
225+
if (storage?.entries) {
226+
const entries = await storage.entries()
227+
for (const [key, value] of entries) {
228+
if (key.startsWith(prefix)) {
229+
const persistedQuery = await deserialize(value)
230+
231+
if (isExpiredOrBusted(persistedQuery)) {
232+
await storage.removeItem(key)
233+
} else {
234+
queryClient.setQueryData(
235+
persistedQuery.queryKey,
236+
persistedQuery.state.data,
237+
{
238+
updatedAt: persistedQuery.state.dataUpdatedAt,
239+
},
240+
)
241+
}
242+
}
243+
}
244+
} else if (process.env.NODE_ENV === 'development') {
245+
throw new Error(
246+
'Provided storage does not implement `entries` method. Restoration of all stored entries is not possible without ability to iterate over storage items.',
247+
)
248+
}
249+
}
250+
251+
return {
252+
persisterFn,
253+
persistQuery,
254+
retrieveQuery,
255+
persisterGc,
256+
persisterRestoreAll,
257+
}
166258
}

0 commit comments

Comments
 (0)