@tanstack/query-persist-client-core 5.0.0-beta.9 → 5.0.0-rc.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.
@@ -0,0 +1,160 @@
1
+ import { matchQuery } from '@tanstack/query-core'
2
+ import type { QueryFilters } from '@tanstack/query-core'
3
+ import type {
4
+ Query,
5
+ QueryFunctionContext,
6
+ QueryKey,
7
+ QueryState,
8
+ } from '@tanstack/query-core'
9
+
10
+ export interface PersistedQuery {
11
+ buster: string
12
+ queryHash: string
13
+ queryKey: QueryKey
14
+ state: QueryState
15
+ }
16
+
17
+ export interface AsyncStorage {
18
+ getItem: (key: string) => Promise<string | undefined | null>
19
+ setItem: (key: string, value: string) => Promise<unknown>
20
+ removeItem: (key: string) => Promise<void>
21
+ }
22
+
23
+ export interface StoragePersisterOptions {
24
+ /** The storage client used for setting and retrieving items from cache.
25
+ * For SSR pass in `undefined`.
26
+ */
27
+ storage: AsyncStorage | Storage | undefined | null
28
+ /**
29
+ * How to serialize the data to storage.
30
+ * @default `JSON.stringify`
31
+ */
32
+ serialize?: (persistedQuery: PersistedQuery) => string
33
+ /**
34
+ * How to deserialize the data from storage.
35
+ * @default `JSON.parse`
36
+ */
37
+ deserialize?: (cachedString: string) => PersistedQuery
38
+ /**
39
+ * A unique string that can be used to forcefully invalidate existing caches,
40
+ * if they do not share the same buster string
41
+ */
42
+ buster?: string
43
+ /**
44
+ * The max-allowed age of the cache in milliseconds.
45
+ * If a persisted cache is found that is older than this
46
+ * time, it will be discarded
47
+ * @default 24 hours
48
+ */
49
+ maxAge?: number
50
+ /**
51
+ * Prefix to be used for storage key.
52
+ * Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`.
53
+ * @default 'tanstack-query'
54
+ */
55
+ prefix?: string
56
+ /**
57
+ * Filters to narrow down which Queries should be persisted.
58
+ */
59
+ filters?: QueryFilters
60
+ }
61
+
62
+ export const PERSISTER_KEY_PREFIX = 'tanstack-query'
63
+
64
+ /**
65
+ * Warning: experimental feature.
66
+ * This utility function enables fine-grained query persistance.
67
+ * Simple add it as a `persister` parameter to `useQuery` or `defaultOptions` on `queryClient`.
68
+ *
69
+ * ```
70
+ * useQuery({
71
+ queryKey: ['myKey'],
72
+ queryFn: fetcher,
73
+ persister: createPersister({
74
+ storage: localStorage,
75
+ }),
76
+ })
77
+ ```
78
+ */
79
+ export function experimental_createPersister({
80
+ storage,
81
+ buster = '',
82
+ maxAge = 1000 * 60 * 60 * 24,
83
+ serialize = JSON.stringify,
84
+ deserialize = JSON.parse,
85
+ prefix = PERSISTER_KEY_PREFIX,
86
+ filters,
87
+ }: StoragePersisterOptions) {
88
+ return async function persisterFn<T, TQueryKey extends QueryKey>(
89
+ queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
90
+ context: QueryFunctionContext<TQueryKey>,
91
+ query: Query,
92
+ ) {
93
+ const storageKey = `${prefix}-${query.queryHash}`
94
+ const matchesFilter = filters ? matchQuery(filters, query) : true
95
+
96
+ // Try to restore only if we do not have any data in the cache and we have persister defined
97
+ if (matchesFilter && query.state.data === undefined && storage != null) {
98
+ try {
99
+ const storedData = await storage.getItem(storageKey)
100
+ if (storedData) {
101
+ const persistedQuery = deserialize(storedData)
102
+
103
+ if (persistedQuery.state.dataUpdatedAt) {
104
+ const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt
105
+ const expired = queryAge > maxAge
106
+ const busted = persistedQuery.buster !== buster
107
+ if (expired || busted) {
108
+ await storage.removeItem(storageKey)
109
+ } else {
110
+ // Just after restoring we want to get fresh data from the server if it's stale
111
+ setTimeout(() => {
112
+ // Set proper updatedAt, since resolving in the first pass overrides those values
113
+ query.setState({
114
+ dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
115
+ errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
116
+ })
117
+
118
+ if (query.isStale()) {
119
+ query.fetch()
120
+ }
121
+ }, 0)
122
+ // We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves
123
+ return Promise.resolve(persistedQuery.state.data as T)
124
+ }
125
+ } else {
126
+ await storage.removeItem(storageKey)
127
+ }
128
+ }
129
+ } catch (err) {
130
+ if (process.env.NODE_ENV === 'development') {
131
+ console.error(err)
132
+ console.warn(
133
+ 'Encountered an error attempting to restore query cache from persisted location.',
134
+ )
135
+ }
136
+ await storage.removeItem(storageKey)
137
+ }
138
+ }
139
+
140
+ // If we did not restore, or restoration failed - fetch
141
+ const queryFnResult = await queryFn(context)
142
+
143
+ if (matchesFilter && storage != null) {
144
+ // Persist if we have storage defined, we use timeout to get proper state to be persisted
145
+ setTimeout(() => {
146
+ storage.setItem(
147
+ storageKey,
148
+ serialize({
149
+ state: query.state,
150
+ queryKey: query.queryKey,
151
+ queryHash: query.queryHash,
152
+ buster: buster,
153
+ }),
154
+ )
155
+ }, 0)
156
+ }
157
+
158
+ return Promise.resolve(queryFnResult)
159
+ }
160
+ }
package/src/index.ts CHANGED
@@ -2,3 +2,4 @@
2
2
 
3
3
  export * from './persist'
4
4
  export * from './retryStrategies'
5
+ export * from './createPersister'