@tanstack/query-persist-client-core 5.75.7 → 5.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { matchQuery } from '@tanstack/query-core'
2
2
  import type {
3
3
  Query,
4
+ QueryClient,
4
5
  QueryFilters,
5
6
  QueryFunctionContext,
6
7
  QueryKey,
@@ -20,6 +21,7 @@ export interface AsyncStorage<TStorageValue = string> {
20
21
  getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>
21
22
  setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>
22
23
  removeItem: (key: string) => MaybePromise<void>
24
+ entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>
23
25
  }
24
26
 
25
27
  export interface StoragePersisterOptions<TStorageValue = string> {
@@ -78,7 +80,7 @@ export const PERSISTER_KEY_PREFIX = 'tanstack-query'
78
80
  })
79
81
  ```
80
82
  */
81
- export function experimental_createPersister<TStorageValue = string>({
83
+ export function experimental_createQueryPersister<TStorageValue = string>({
82
84
  storage,
83
85
  buster = '',
84
86
  maxAge = 1000 * 60 * 60 * 24,
@@ -91,45 +93,42 @@ export function experimental_createPersister<TStorageValue = string>({
91
93
  prefix = PERSISTER_KEY_PREFIX,
92
94
  filters,
93
95
  }: 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
101
101
 
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}`
104
118
  try {
105
119
  const storedData = await storage.getItem(storageKey)
106
120
  if (storedData) {
107
121
  const persistedQuery = await deserialize(storedData)
108
122
 
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) {
116
127
  // 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)
130
129
  }
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
133
132
  }
134
133
  }
135
134
  } catch (err) {
@@ -143,24 +142,137 @@ export function experimental_createPersister<TStorageValue = string>({
143
142
  }
144
143
  }
145
144
 
145
+ return
146
+ }
147
+
148
+ async function persistQueryByKey(
149
+ queryKey: QueryKey,
150
+ queryClient: QueryClient,
151
+ ) {
152
+ if (storage != null) {
153
+ const query = queryClient.getQueryCache().find({ queryKey })
154
+ if (query) {
155
+ await persistQuery(query)
156
+ } else {
157
+ if (process.env.NODE_ENV === 'development') {
158
+ console.warn(
159
+ 'Could not find query to be persisted. QueryKey:',
160
+ JSON.stringify(queryKey),
161
+ )
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ async function persistQuery(query: Query) {
168
+ if (storage != null) {
169
+ const storageKey = `${prefix}-${query.queryHash}`
170
+ storage.setItem(
171
+ storageKey,
172
+ await serialize({
173
+ state: query.state,
174
+ queryKey: query.queryKey,
175
+ queryHash: query.queryHash,
176
+ buster: buster,
177
+ }),
178
+ )
179
+ }
180
+ }
181
+
182
+ async function persisterFn<T, TQueryKey extends QueryKey>(
183
+ queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
184
+ ctx: QueryFunctionContext<TQueryKey>,
185
+ query: Query,
186
+ ) {
187
+ const matchesFilter = filters ? matchQuery(filters, query) : true
188
+
189
+ // Try to restore only if we do not have any data in the cache and we have persister defined
190
+ if (matchesFilter && query.state.data === undefined && storage != null) {
191
+ const restoredData = await retrieveQuery(
192
+ query.queryHash,
193
+ (persistedQuery: PersistedQuery) => {
194
+ // Set proper updatedAt, since resolving in the first pass overrides those values
195
+ query.setState({
196
+ dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
197
+ errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
198
+ })
199
+
200
+ if (query.isStale()) {
201
+ query.fetch()
202
+ }
203
+ },
204
+ )
205
+
206
+ if (restoredData != null) {
207
+ return Promise.resolve(restoredData as T)
208
+ }
209
+ }
210
+
146
211
  // If we did not restore, or restoration failed - fetch
147
- const queryFnResult = await queryFn(context)
212
+ const queryFnResult = await queryFn(ctx)
148
213
 
149
214
  if (matchesFilter && storage != null) {
150
215
  // 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
- )
216
+ setTimeout(() => {
217
+ persistQuery(query)
161
218
  }, 0)
162
219
  }
163
220
 
164
221
  return Promise.resolve(queryFnResult)
165
222
  }
223
+
224
+ async function persisterGc() {
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
+ }
234
+ }
235
+ }
236
+ } else if (process.env.NODE_ENV === 'development') {
237
+ throw new Error(
238
+ 'Provided storage does not implement `entries` method. Garbage collection is not possible without ability to iterate over storage items.',
239
+ )
240
+ }
241
+ }
242
+
243
+ async function persisterRestoreAll(queryClient: QueryClient) {
244
+ if (storage?.entries) {
245
+ const entries = await storage.entries()
246
+ for (const [key, value] of entries) {
247
+ if (key.startsWith(prefix)) {
248
+ const persistedQuery = await deserialize(value)
249
+
250
+ if (isExpiredOrBusted(persistedQuery)) {
251
+ await storage.removeItem(key)
252
+ } else {
253
+ queryClient.setQueryData(
254
+ persistedQuery.queryKey,
255
+ persistedQuery.state.data,
256
+ {
257
+ updatedAt: persistedQuery.state.dataUpdatedAt,
258
+ },
259
+ )
260
+ }
261
+ }
262
+ }
263
+ } else if (process.env.NODE_ENV === 'development') {
264
+ throw new Error(
265
+ 'Provided storage does not implement `entries` method. Restoration of all stored entries is not possible without ability to iterate over storage items.',
266
+ )
267
+ }
268
+ }
269
+
270
+ return {
271
+ persisterFn,
272
+ persistQuery,
273
+ persistQueryByKey,
274
+ retrieveQuery,
275
+ persisterGc,
276
+ persisterRestoreAll,
277
+ }
166
278
  }