Skip to content

Commit b52b277

Browse files
authored
feat(persistQueryClient): try to clean the old persistented data when storage full (TanStack#2851)
1 parent 80f236d commit b52b277

File tree

3 files changed

+214
-2
lines changed

3 files changed

+214
-2
lines changed

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module.exports = {
22
collectCoverage: true,
33
coverageReporters: ['json', 'lcov', 'text', 'clover', 'text-summary'],
44
setupFilesAfterEnv: ['./jest.setup.js'],
5-
testMatch: ['<rootDir>/src/**/*.test.tsx'],
5+
testMatch: ['<rootDir>/src/**/*.test.{tsx,ts}'],
66
testPathIgnorePatterns: ['<rootDir>/types/'],
77
moduleNameMapper: {
88
'react-query': '<rootDir>/src/index.ts',

src/createWebStoragePersistor-experimental/index.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,47 @@ export function createWebStoragePersistor({
2828
serialize = JSON.stringify,
2929
deserialize = JSON.parse,
3030
}: CreateWebStoragePersistorOptions): Persistor {
31+
//try to save data to storage
32+
function trySave(persistedClient: PersistedClient) {
33+
try {
34+
storage.setItem(key, serialize(persistedClient))
35+
} catch {
36+
return false
37+
}
38+
return true
39+
}
40+
3141
if (typeof storage !== 'undefined') {
3242
return {
3343
persistClient: throttle(persistedClient => {
34-
storage.setItem(key, serialize(persistedClient))
44+
if (trySave(persistedClient) !== true) {
45+
const mutations = [...persistedClient.clientState.mutations]
46+
const queries = [...persistedClient.clientState.queries]
47+
const client: PersistedClient = {
48+
...persistedClient,
49+
clientState: { mutations, queries },
50+
}
51+
52+
// sort queries by dataUpdatedAt (oldest first)
53+
const sortedQueries = [...queries].sort(
54+
(a, b) => a.state.dataUpdatedAt - b.state.dataUpdatedAt
55+
)
56+
// clean old queries and try to save
57+
while (sortedQueries.length > 0) {
58+
const oldestData = sortedQueries.shift()
59+
client.clientState.queries = queries.filter(q => q !== oldestData)
60+
if (trySave(client)) {
61+
return // save success
62+
}
63+
}
64+
65+
// clean mutations and try to save
66+
while (mutations.shift()) {
67+
if (trySave(client)) {
68+
return // save success
69+
}
70+
}
71+
}
3572
}, throttleTime),
3673
restoreClient: () => {
3774
const cacheString = storage.getItem(key)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { dehydrate, MutationCache, QueryCache, QueryClient } from '../../core'
2+
import { sleep } from '../../react/tests/utils'
3+
import { createWebStoragePersistor } from '../index'
4+
5+
function getMockStorage(limitSize?: number) {
6+
const dataSet = new Map<string, string>()
7+
return ({
8+
getItem(key: string): string | null {
9+
const value = dataSet.get(key)
10+
return value === undefined ? null : value
11+
},
12+
13+
setItem(key: string, value: string) {
14+
if (limitSize) {
15+
const currentSize = Array.from(dataSet.entries()).reduce(
16+
(n, d) => d[0].length + d[1].length + n,
17+
0
18+
)
19+
if (
20+
currentSize - (dataSet.get(key)?.length || 0) + value.length >
21+
limitSize
22+
) {
23+
throw Error(
24+
` Failed to execute 'setItem' on 'Storage': Setting the value of '${key}' exceeded the quota.`
25+
)
26+
}
27+
}
28+
return dataSet.set(key, value)
29+
},
30+
removeItem(key: string) {
31+
return dataSet.delete(key)
32+
},
33+
} as any) as Storage
34+
}
35+
36+
describe('createWebStoragePersistor ', () => {
37+
test('basic store and recover', async () => {
38+
const queryCache = new QueryCache()
39+
const mutationCache = new MutationCache()
40+
const queryClient = new QueryClient({ queryCache, mutationCache })
41+
42+
const storage = getMockStorage()
43+
const webStoragePersistor = createWebStoragePersistor({
44+
throttleTime: 0,
45+
storage,
46+
})
47+
48+
await queryClient.prefetchQuery('string', () => Promise.resolve('string'))
49+
await queryClient.prefetchQuery('number', () => Promise.resolve(1))
50+
await queryClient.prefetchQuery('boolean', () => Promise.resolve(true))
51+
await queryClient.prefetchQuery('null', () => Promise.resolve(null))
52+
await queryClient.prefetchQuery('array', () =>
53+
Promise.resolve(['string', 0])
54+
)
55+
56+
const persistClient = {
57+
buster: 'test-buster',
58+
timestamp: Date.now(),
59+
clientState: dehydrate(queryClient),
60+
}
61+
webStoragePersistor.persistClient(persistClient)
62+
await sleep(1)
63+
const restoredClient = await webStoragePersistor.restoreClient()
64+
expect(restoredClient).toEqual(persistClient)
65+
})
66+
67+
test('should clean the old queries when storage full', async () => {
68+
const queryCache = new QueryCache()
69+
const mutationCache = new MutationCache()
70+
const queryClient = new QueryClient({ queryCache, mutationCache })
71+
72+
const N = 2000
73+
const storage = getMockStorage(N * 5) // can save 4 items;
74+
const webStoragePersistor = createWebStoragePersistor({
75+
throttleTime: 0,
76+
storage,
77+
})
78+
79+
await queryClient.prefetchQuery('A', () => Promise.resolve('A'.repeat(N)))
80+
await sleep(1)
81+
await queryClient.prefetchQuery('B', () => Promise.resolve('B'.repeat(N)))
82+
await sleep(1)
83+
await queryClient.prefetchQuery('C', () => Promise.resolve('C'.repeat(N)))
84+
await sleep(1)
85+
await queryClient.prefetchQuery('D', () => Promise.resolve('D'.repeat(N)))
86+
87+
await sleep(1)
88+
await queryClient.prefetchQuery('E', () => Promise.resolve('E'.repeat(N)))
89+
90+
const persistClient = {
91+
buster: 'test-limit',
92+
timestamp: Date.now(),
93+
clientState: dehydrate(queryClient),
94+
}
95+
webStoragePersistor.persistClient(persistClient)
96+
await sleep(10)
97+
const restoredClient = await webStoragePersistor.restoreClient()
98+
expect(restoredClient?.clientState.queries.length).toEqual(4)
99+
expect(
100+
restoredClient?.clientState.queries.find(q => q.queryKey === 'A')
101+
).toBeUndefined()
102+
expect(
103+
restoredClient?.clientState.queries.find(q => q.queryKey === 'B')
104+
).not.toBeUndefined()
105+
106+
// update query Data
107+
await queryClient.prefetchQuery('A', () => Promise.resolve('a'.repeat(N)))
108+
const updatedPersistClient = {
109+
buster: 'test-limit',
110+
timestamp: Date.now(),
111+
clientState: dehydrate(queryClient),
112+
}
113+
webStoragePersistor.persistClient(updatedPersistClient)
114+
await sleep(10)
115+
const restoredClient2 = await webStoragePersistor.restoreClient()
116+
expect(restoredClient2?.clientState.queries.length).toEqual(4)
117+
expect(
118+
restoredClient2?.clientState.queries.find(q => q.queryKey === 'A')
119+
).toHaveProperty('state.data', 'a'.repeat(N))
120+
expect(
121+
restoredClient2?.clientState.queries.find(q => q.queryKey === 'B')
122+
).toBeUndefined()
123+
})
124+
125+
test('should clean queries before mutations when storage full', async () => {
126+
const queryCache = new QueryCache()
127+
const mutationCache = new MutationCache()
128+
const queryClient = new QueryClient({ queryCache, mutationCache })
129+
130+
const N = 2000
131+
const storage = getMockStorage(N * 5) // can save 4 items;
132+
const webStoragePersistor = createWebStoragePersistor({
133+
throttleTime: 0,
134+
storage,
135+
})
136+
137+
mutationCache.build(
138+
queryClient,
139+
{
140+
mutationKey: 'MUTATIONS',
141+
mutationFn: () => Promise.resolve('M'.repeat(N)),
142+
},
143+
{
144+
error: null,
145+
context: '',
146+
failureCount: 1,
147+
isPaused: true,
148+
status: 'success',
149+
variables: '',
150+
data: 'M'.repeat(N),
151+
}
152+
)
153+
await sleep(1)
154+
await queryClient.prefetchQuery('A', () => Promise.resolve('A'.repeat(N)))
155+
await sleep(1)
156+
await queryClient.prefetchQuery('B', () => Promise.resolve('B'.repeat(N)))
157+
await queryClient.prefetchQuery('C', () => Promise.resolve('C'.repeat(N)))
158+
await sleep(1)
159+
await queryClient.prefetchQuery('D', () => Promise.resolve('D'.repeat(N)))
160+
161+
const persistClient = {
162+
buster: 'test-limit-mutations',
163+
timestamp: Date.now(),
164+
clientState: dehydrate(queryClient),
165+
}
166+
webStoragePersistor.persistClient(persistClient)
167+
await sleep(10)
168+
const restoredClient = await webStoragePersistor.restoreClient()
169+
expect(restoredClient?.clientState.mutations.length).toEqual(1)
170+
expect(restoredClient?.clientState.queries.length).toEqual(3)
171+
expect(
172+
restoredClient?.clientState.queries.find(q => q.queryKey === 'A')
173+
).toBeUndefined()
174+
})
175+
})

0 commit comments

Comments
 (0)