11import { matchQuery } from '@tanstack/query-core'
22import 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
2527export 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