snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
472
packages/query-persist-client-core/CHANGELOG.md
Normal file
472
packages/query-persist-client-core/CHANGELOG.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# @tanstack/query-persist-client-core
|
||||
|
||||
## 5.102.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.8
|
||||
|
||||
## 5.102.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.7
|
||||
|
||||
## 5.102.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.6
|
||||
|
||||
## 5.102.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`578e5c2`](https://github.com/TanStack/query/commit/578e5c26e8ebd0d7351b4b8e2bafba695e672b8d)]:
|
||||
- @tanstack/query-core@5.102.5
|
||||
|
||||
## 5.102.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a05df6a`](https://github.com/TanStack/query/commit/a05df6aefb0e2489ec2c879ae16e2ee7cb3123ec)]:
|
||||
- @tanstack/query-core@5.102.4
|
||||
|
||||
## 5.102.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.3
|
||||
|
||||
## 5.102.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`80fbf73`](https://github.com/TanStack/query/commit/80fbf73e77892d702c107e14a84c219a8ed825dc)]:
|
||||
- @tanstack/query-core@5.102.2
|
||||
|
||||
## 5.102.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`134890d`](https://github.com/TanStack/query/commit/134890dbdc60e4fb0313e44b512d29284ca82f96)]:
|
||||
- @tanstack/query-core@5.102.1
|
||||
|
||||
## 5.102.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`34f7cee`](https://github.com/TanStack/query/commit/34f7ceed09c10e4a3aa2df31a106ddf02ec4e787), [`b4368c4`](https://github.com/TanStack/query/commit/b4368c43792349f6c29d1fb41f7ee1ef3a8bdd2c), [`5bb089d`](https://github.com/TanStack/query/commit/5bb089d153d33933983b7ea80f8d69b51d423698), [`ba4650c`](https://github.com/TanStack/query/commit/ba4650c05e61e33c609e051b948c9f7d31ce70d1), [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b), [`1f631b3`](https://github.com/TanStack/query/commit/1f631b3604aab6567fb3f6c90646a5a304641546), [`01a02bf`](https://github.com/TanStack/query/commit/01a02bfad32f9efbc17796be31d6399f1197a655), [`18c1c1e`](https://github.com/TanStack/query/commit/18c1c1ef94883e781ad36d36f9ea4a043ce4260b), [`5448063`](https://github.com/TanStack/query/commit/5448063f828d2270dccd41ae375e1afde35e91f2), [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e), [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93), [`5981771`](https://github.com/TanStack/query/commit/5981771abec9330b344a2617543eb05ba7c99e24), [`4a9bef6`](https://github.com/TanStack/query/commit/4a9bef6cf19b1cd6b014032d696129f12a848185), [`bef4bc7`](https://github.com/TanStack/query/commit/bef4bc780ce7cca32b7e3dea85f77d92f82a62a2), [`9656dc4`](https://github.com/TanStack/query/commit/9656dc4e5fef5f8c502579b52459f9e8c72e787c), [`326aaf1`](https://github.com/TanStack/query/commit/326aaf1333e5d9cbc46569c53f218d31684162d2), [`3e83601`](https://github.com/TanStack/query/commit/3e836010032c2586e9f1c66a271fa9114a6401f9), [`c6fc17c`](https://github.com/TanStack/query/commit/c6fc17cfad6e452261c585fabfb8c210e60531ed)]:
|
||||
- @tanstack/query-core@5.102.0
|
||||
|
||||
## 5.101.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.4
|
||||
|
||||
## 5.101.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
|
||||
- @tanstack/query-core@5.101.3
|
||||
|
||||
## 5.101.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.2
|
||||
|
||||
## 5.101.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]:
|
||||
- @tanstack/query-core@5.101.1
|
||||
|
||||
## 5.101.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.0
|
||||
|
||||
## 5.100.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.14
|
||||
|
||||
## 5.100.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d423168`](https://github.com/TanStack/query/commit/d423168f6261a5cb3d353e53b27c8150cc271151)]:
|
||||
- @tanstack/query-core@5.100.13
|
||||
|
||||
## 5.100.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.12
|
||||
|
||||
## 5.100.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.11
|
||||
|
||||
## 5.100.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.10
|
||||
|
||||
## 5.100.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fcee7bd`](https://github.com/TanStack/query/commit/fcee7bdc429385ae8ffa224fa8a7a9ec7b8ee380)]:
|
||||
- @tanstack/query-core@5.100.9
|
||||
|
||||
## 5.100.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.8
|
||||
|
||||
## 5.100.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.7
|
||||
|
||||
## 5.100.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.6
|
||||
|
||||
## 5.100.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a53ef97`](https://github.com/TanStack/query/commit/a53ef97f87decb8ea2431710f5199431d3c94c8d)]:
|
||||
- @tanstack/query-core@5.100.5
|
||||
|
||||
## 5.100.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.4
|
||||
|
||||
## 5.100.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f85d825`](https://github.com/TanStack/query/commit/f85d825e02efbbff02e2081528ed28f5e5382f7a)]:
|
||||
- @tanstack/query-core@5.100.3
|
||||
|
||||
## 5.100.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ea4497e`](https://github.com/TanStack/query/commit/ea4497e8aa00d8c1c3a36fb1e17563a889d6ab31), [`d6a7bf3`](https://github.com/TanStack/query/commit/d6a7bf3e3e024c1a77d0536813238cc8007a5fa7), [`645d5d1`](https://github.com/TanStack/query/commit/645d5d130f5e8017cb1bf1a37987f7b980aed705)]:
|
||||
- @tanstack/query-core@5.100.2
|
||||
|
||||
## 5.100.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1bb0d23`](https://github.com/TanStack/query/commit/1bb0d234280fd4ae1725c439088426a20593a8df)]:
|
||||
- @tanstack/query-core@5.100.1
|
||||
|
||||
## 5.100.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6540a41`](https://github.com/TanStack/query/commit/6540a4126b1c087d86d64525e78f32d9920dcd31)]:
|
||||
- @tanstack/query-core@5.100.0
|
||||
|
||||
## 5.99.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.2
|
||||
|
||||
## 5.99.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.1
|
||||
|
||||
## 5.99.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.0
|
||||
|
||||
## 5.98.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.98.0
|
||||
|
||||
## 5.97.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`2bfb12c`](https://github.com/TanStack/query/commit/2bfb12cc44f1d8495106136e4ddacb817135f8f9)]:
|
||||
- @tanstack/query-core@5.97.0
|
||||
|
||||
## 5.96.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.2
|
||||
|
||||
## 5.96.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.1
|
||||
|
||||
## 5.96.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.0
|
||||
|
||||
## 5.95.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`cd5a35b`](https://github.com/TanStack/query/commit/cd5a35b328837781aa4f9305bb2bd7877ca934e9)]:
|
||||
- @tanstack/query-core@5.95.2
|
||||
|
||||
## 5.95.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f1775c`](https://github.com/TanStack/query/commit/1f1775ca92f2b6c035682947ff3b3424804ff31a)]:
|
||||
- @tanstack/query-core@5.95.1
|
||||
|
||||
## 5.95.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.95.0
|
||||
|
||||
## 5.94.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(\*): resolve issue about excluded build directory ([#10312](https://github.com/TanStack/query/pull/10312))
|
||||
|
||||
- Updated dependencies [[`4b6536d`](https://github.com/TanStack/query/commit/4b6536dfce99036f4e37f52943c6fed3ad0e0a18)]:
|
||||
- @tanstack/query-core@5.94.5
|
||||
|
||||
## 5.94.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: fixed version ([#10064](https://github.com/TanStack/query/pull/10064))
|
||||
|
||||
- Updated dependencies [[`4c75210`](https://github.com/TanStack/query/commit/4c75210ce8235fe3d39b67e1029eff11278927cc)]:
|
||||
- @tanstack/query-core@5.94.4
|
||||
|
||||
## 5.92.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(streamedQuery): maintain error state on reset refetch with initialData defined ([#10287](https://github.com/TanStack/query/pull/10287))
|
||||
|
||||
- Updated dependencies [[`248975e`](https://github.com/TanStack/query/commit/248975e896f585f6eaa505c796e73fcf7bfd1eec)]:
|
||||
- @tanstack/query-core@5.91.2
|
||||
|
||||
## 5.92.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]:
|
||||
- @tanstack/query-core@5.91.1
|
||||
|
||||
## 5.92.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]:
|
||||
- @tanstack/query-core@5.91.0
|
||||
|
||||
## 5.92.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- removeItem if deserialize fails ([#10190](https://github.com/TanStack/query/pull/10190))
|
||||
|
||||
## 5.92.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add removeQueries to experimental_createQueryPersister ([#10186](https://github.com/TanStack/query/pull/10186))
|
||||
|
||||
## 5.91.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]:
|
||||
- @tanstack/query-core@5.90.20
|
||||
|
||||
## 5.91.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]:
|
||||
- @tanstack/query-core@5.90.19
|
||||
|
||||
## 5.91.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]:
|
||||
- @tanstack/query-core@5.90.18
|
||||
|
||||
## 5.91.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]:
|
||||
- @tanstack/query-core@5.90.17
|
||||
|
||||
## 5.91.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]:
|
||||
- @tanstack/query-core@5.90.16
|
||||
|
||||
## 5.91.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]:
|
||||
- @tanstack/query-core@5.90.15
|
||||
|
||||
## 5.91.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]:
|
||||
- @tanstack/query-core@5.90.14
|
||||
|
||||
## 5.91.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]:
|
||||
- @tanstack/query-core@5.90.13
|
||||
|
||||
## 5.91.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]:
|
||||
- @tanstack/query-core@5.90.12
|
||||
|
||||
## 5.91.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]:
|
||||
- @tanstack/query-core@5.90.11
|
||||
|
||||
## 5.91.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8e2e174`](https://github.com/TanStack/query/commit/8e2e174e9fd2e7b94cd232041e49c9d014d74e26), [`eb559a6`](https://github.com/TanStack/query/commit/eb559a66dc0d77dd46435f624fa64fc068bef9ae)]:
|
||||
- @tanstack/query-core@5.90.10
|
||||
|
||||
## 5.91.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]:
|
||||
- @tanstack/query-core@5.90.9
|
||||
|
||||
## 5.91.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]:
|
||||
- @tanstack/query-core@5.90.8
|
||||
|
||||
## 5.91.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]:
|
||||
- @tanstack/query-core@5.90.7
|
||||
|
||||
## 5.91.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]:
|
||||
- @tanstack/query-core@5.90.6
|
||||
|
||||
## 5.91.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]:
|
||||
- @tanstack/query-core@5.90.5
|
||||
|
||||
## 5.91.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]:
|
||||
- @tanstack/query-core@5.90.4
|
||||
|
||||
## 5.91.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]:
|
||||
- @tanstack/query-core@5.90.3
|
||||
|
||||
## 5.91.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- allow restoring null values ([#9747](https://github.com/TanStack/query/pull/9747))
|
||||
|
||||
## 5.91.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Added a refetchOnRestore setting to control refetching after restoring persisted queries ([#9745](https://github.com/TanStack/query/pull/9745))
|
||||
5
packages/query-persist-client-core/eslint.config.js
Normal file
5
packages/query-persist-client-core/eslint.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-check
|
||||
|
||||
import rootConfig from './root.eslint.config.js'
|
||||
|
||||
export default [...rootConfig]
|
||||
65
packages/query-persist-client-core/package.json
Normal file
65
packages/query-persist-client-core/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@tanstack/query-persist-client-core",
|
||||
"version": "5.102.8",
|
||||
"description": "Set of utilities for interacting with persisters, which can save your queryClient for later use",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/query-persist-client-core"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "premove ./build ./coverage ./dist-ts ./.cache/test-types",
|
||||
"compile": "tsc --project tsconfig.json",
|
||||
"test:eslint": "eslint --concurrency=auto ./src",
|
||||
"test:types": "npm-run-all --serial test:types:*",
|
||||
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts56 --tsBuildInfoFile .cache/test-types/ts56.tsbuildinfo",
|
||||
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts57 --tsBuildInfoFile .cache/test-types/ts57.tsbuildinfo",
|
||||
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts58 --tsBuildInfoFile .cache/test-types/ts58.tsbuildinfo",
|
||||
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts59 --tsBuildInfoFile .cache/test-types/ts59.tsbuildinfo",
|
||||
"test:types:tscurrent": "tsc --project tsconfig.json --outDir .cache/test-types/tscurrent --tsBuildInfoFile .cache/test-types/tscurrent.tsbuildinfo",
|
||||
"test:types:ts70": "node ../../node_modules/typescript70/lib/tsc.js --project tsconfig.json --outDir .cache/test-types/ts70 --tsBuildInfoFile .cache/test-types/ts70.tsbuildinfo",
|
||||
"test:lib": "vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "tsdown --tsconfig tsconfig.prod.json"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "build/legacy/index.d.ts",
|
||||
"main": "build/legacy/index.cjs",
|
||||
"module": "build/legacy/index.js",
|
||||
"react-native": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"import": {
|
||||
"types": "./build/modern/index.d.ts",
|
||||
"default": "./build/modern/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./build/modern/index.d.cts",
|
||||
"default": "./build/modern/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/query-test-utils": "workspace:*",
|
||||
"npm-run-all2": "^5.0.0"
|
||||
}
|
||||
}
|
||||
1
packages/query-persist-client-core/root.eslint.config.js
Symbolic link
1
packages/query-persist-client-core/root.eslint.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../eslint.config.js
|
||||
1
packages/query-persist-client-core/root.tsdown.config.js
Symbolic link
1
packages/query-persist-client-core/root.tsdown.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/getTsdownConfig.js
|
||||
@@ -0,0 +1,839 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { Query, QueryClient, hashKey } from '@tanstack/query-core'
|
||||
import {
|
||||
PERSISTER_KEY_PREFIX,
|
||||
experimental_createQueryPersister,
|
||||
} from '../createPersister'
|
||||
import type { QueryFunctionContext, QueryKey } from '@tanstack/query-core'
|
||||
import type { StoragePersisterOptions } from '../createPersister'
|
||||
|
||||
function getFreshStorage() {
|
||||
const storage = new Map()
|
||||
return {
|
||||
getItem: (key: string) => Promise.resolve(storage.get(key)),
|
||||
setItem: (key: string, value: unknown) => {
|
||||
storage.set(key, value)
|
||||
return Promise.resolve()
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
storage.delete(key)
|
||||
return Promise.resolve()
|
||||
},
|
||||
entries: () => {
|
||||
return Promise.resolve(Array.from(storage.entries()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function setupPersister(
|
||||
queryKey: QueryKey,
|
||||
persisterOptions: StoragePersisterOptions,
|
||||
) {
|
||||
const client = new QueryClient()
|
||||
const context = {
|
||||
meta: { foo: 'bar' },
|
||||
client,
|
||||
queryKey,
|
||||
// @ts-expect-error
|
||||
signal: undefined as AbortSignal,
|
||||
} satisfies QueryFunctionContext
|
||||
const queryHash = hashKey(queryKey)
|
||||
const storageKey = `${PERSISTER_KEY_PREFIX}-${queryHash}`
|
||||
|
||||
const queryFn = vi.fn()
|
||||
|
||||
const persister = experimental_createQueryPersister(persisterOptions)
|
||||
|
||||
const query = new Query({
|
||||
client,
|
||||
queryHash,
|
||||
queryKey,
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
context,
|
||||
persister,
|
||||
query,
|
||||
queryFn,
|
||||
queryHash,
|
||||
queryKey,
|
||||
storageKey,
|
||||
}
|
||||
}
|
||||
|
||||
describe('createPersister', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should fetch if storage is not provided', async () => {
|
||||
const { context, persister, query, queryFn } = setupPersister(['foo'], {
|
||||
storage: undefined,
|
||||
})
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should fetch if there is no stored data', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should fetch if query already has data', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
query.state.data = 'baz'
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should fetch if deserialization fails', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(storageKey, '{invalid[item')
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(await storage.getItem(storageKey)).toBeUndefined()
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should remove stored item if `dataUpdatedAt` is empty', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: undefined },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(await storage.getItem(storageKey)).toBeUndefined()
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should remove stored item if its expired', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
maxAge: 100,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: Date.now() - 200 },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(await storage.getItem(storageKey)).toBeUndefined()
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should remove stored item if its busted', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: 'bust',
|
||||
state: { dataUpdatedAt: Date.now() },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
|
||||
expect(await storage.getItem(storageKey)).toBeUndefined()
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
})
|
||||
|
||||
it('should restore item from the storage and set proper `updatedAt` values', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
},
|
||||
)
|
||||
|
||||
const dataUpdatedAt = Date.now()
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt, data: '' },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.state.data = 'data0'
|
||||
query.fetch = vi.fn()
|
||||
expect(query.state.dataUpdatedAt).toEqual(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(0)
|
||||
expect(query.state.dataUpdatedAt).toEqual(dataUpdatedAt)
|
||||
})
|
||||
|
||||
it('should restore item from the storage and refetch when `stale`', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: Date.now(), data: '' },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.state.isInvalidated = true
|
||||
query.fetch = vi.fn()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should restore item from the storage and refetch when `refetchOnRestore` is set to `always`', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
refetchOnRestore: 'always',
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: Date.now() + 1000, data: '' },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.state.isInvalidated = true
|
||||
query.fetch = vi.fn()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should restore item from the storage and NOT refetch when `refetchOnRestore` is set to false', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
refetchOnRestore: false,
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: Date.now(), data: '' },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.state.isInvalidated = true
|
||||
query.fetch = vi.fn()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should store item after successful fetch', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const {
|
||||
context,
|
||||
persister,
|
||||
query,
|
||||
queryFn,
|
||||
queryHash,
|
||||
queryKey,
|
||||
storageKey,
|
||||
} = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.setData('baz')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
|
||||
expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({
|
||||
buster: '',
|
||||
queryHash,
|
||||
queryKey,
|
||||
state: {
|
||||
data: 'baz',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip stored item if not matched by filters', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
filters: {
|
||||
predicate: () => {
|
||||
return false
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const dataUpdatedAt = Date.now()
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.fetch = vi.fn()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(1)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should restore item from the storage with async deserializer', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { context, persister, query, queryFn, storageKey } = setupPersister(
|
||||
['foo'],
|
||||
{
|
||||
storage,
|
||||
deserialize: (cachedString: string) =>
|
||||
new Promise((resolve) => resolve(JSON.parse(cachedString))),
|
||||
},
|
||||
)
|
||||
|
||||
await storage.setItem(
|
||||
storageKey,
|
||||
JSON.stringify({
|
||||
buster: '',
|
||||
state: { dataUpdatedAt: Date.now(), data: '' },
|
||||
}),
|
||||
)
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.state.isInvalidated = true
|
||||
query.fetch = vi.fn()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(0)
|
||||
expect(query.fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should store item after successful fetch with async serializer', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const {
|
||||
context,
|
||||
persister,
|
||||
query,
|
||||
queryFn,
|
||||
queryHash,
|
||||
queryKey,
|
||||
storageKey,
|
||||
} = setupPersister(['foo'], {
|
||||
storage,
|
||||
serialize: (persistedQuery) =>
|
||||
new Promise((resolve) => resolve(JSON.stringify(persistedQuery))),
|
||||
})
|
||||
|
||||
await persister.persisterFn(queryFn, context, query)
|
||||
query.setData('baz')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryFn).toHaveBeenCalledExactlyOnceWith(context)
|
||||
|
||||
expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({
|
||||
buster: '',
|
||||
queryHash,
|
||||
queryKey,
|
||||
state: {
|
||||
data: 'baz',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistQuery', () => {
|
||||
it('should properly persist basic query', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, query, queryHash, queryKey, storageKey } =
|
||||
setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
|
||||
query.setData('baz')
|
||||
await persister.persistQuery(query)
|
||||
|
||||
expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({
|
||||
buster: '',
|
||||
queryHash,
|
||||
queryKey,
|
||||
state: {
|
||||
dataUpdateCount: 1,
|
||||
data: 'baz',
|
||||
status: 'success',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip persistence if storage is not provided', async () => {
|
||||
const serializeMock = vi.fn()
|
||||
const { persister, query } = setupPersister(['foo'], {
|
||||
storage: null,
|
||||
serialize: serializeMock,
|
||||
})
|
||||
|
||||
query.setData('baz')
|
||||
await persister.persistQuery(query)
|
||||
|
||||
expect(serializeMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistQueryByKey', () => {
|
||||
it('should skip persistence if storage is not provided', async () => {
|
||||
const serializeMock = vi.fn()
|
||||
const { persister, client, queryKey } = setupPersister(['foo'], {
|
||||
storage: null,
|
||||
serialize: serializeMock,
|
||||
})
|
||||
|
||||
client.setQueryData(queryKey, 'baz')
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(serializeMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should skip persistence if query was not found', async () => {
|
||||
const serializeMock = vi.fn()
|
||||
const storage = getFreshStorage()
|
||||
const { client, persister, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
serialize: serializeMock,
|
||||
})
|
||||
|
||||
client.setQueryData(queryKey, 'baz')
|
||||
await persister.persistQueryByKey(['foo2'], client)
|
||||
|
||||
expect(serializeMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should properly persist basic query', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryHash, queryKey, storageKey } =
|
||||
setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
|
||||
client.setQueryData(queryKey, 'baz')
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(JSON.parse(await storage.getItem(storageKey))).toMatchObject({
|
||||
buster: '',
|
||||
queryHash,
|
||||
queryKey,
|
||||
state: {
|
||||
dataUpdateCount: 1,
|
||||
data: 'baz',
|
||||
status: 'success',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('retrieveQuery', () => {
|
||||
it('should return the persisted data when called without a restore callback', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryHash, queryKey } = setupPersister(
|
||||
['foo'],
|
||||
{ storage },
|
||||
)
|
||||
|
||||
client.setQueryData(queryKey, 'baz')
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
const restoredData = await persister.retrieveQuery(queryHash)
|
||||
|
||||
expect(restoredData).toBe('baz')
|
||||
})
|
||||
})
|
||||
|
||||
describe('persisterGc', () => {
|
||||
it('should properly clean storage from busted entries', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, query, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
query.setState({
|
||||
dataUpdatedAt: 1,
|
||||
data: 'f',
|
||||
})
|
||||
client.getQueryCache().add(query)
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.persisterGc()
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should remove entries that cannot be deserialized', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister } = setupPersister(['foo'], { storage })
|
||||
|
||||
await storage.setItem(`${PERSISTER_KEY_PREFIX}-["foo"]`, 'not-json{')
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.persisterGc()
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should keep entries that are neither expired nor busted', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, query, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
query.setState({
|
||||
dataUpdatedAt: Date.now(),
|
||||
data: 'foo',
|
||||
})
|
||||
client.getQueryCache().add(query)
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.persisterGc()
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreQueries', () => {
|
||||
it('should properly clean storage from busted entries', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, query, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
query.setState({
|
||||
dataUpdatedAt: 1,
|
||||
data: 'f',
|
||||
})
|
||||
client.getQueryCache().add(query)
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.restoreQueries(client)
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should properly restore queries from cache without filters', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client)
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(1)
|
||||
|
||||
expect(client.getQueryData(queryKey)).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should properly restore queries from cache', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client, { queryKey })
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(1)
|
||||
|
||||
expect(client.getQueryData(queryKey)).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should not restore queries from cache if there is no match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client, { queryKey: ['bar'] })
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should properly restore queries from cache with partial match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client, { queryKey: ['foo'] })
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(1)
|
||||
|
||||
expect(client.getQueryData(queryKey)).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should not restore queries from cache with exact match if there is no match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client, { queryKey: ['foo'], exact: true })
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should restore queries from cache with exact match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
client.clear()
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
|
||||
await persister.restoreQueries(client, {
|
||||
queryKey: queryKey,
|
||||
exact: true,
|
||||
})
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should remove entries that cannot be deserialized', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client } = setupPersister(['foo'], { storage })
|
||||
|
||||
await storage.setItem(`${PERSISTER_KEY_PREFIX}-["foo"]`, 'not-json{')
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.restoreQueries(client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
expect(client.getQueryCache().getAll()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeQueries', () => {
|
||||
it('should remove restore queries from storage without filters', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries()
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should remove queries from storage', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries({ queryKey })
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should not remove queries from storage if there is no match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries({ queryKey: ['bar'] })
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should properly remove queries from storage with partial match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries({ queryKey: ['foo'] })
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should not remove queries from storage with exact match if there is no match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries({ queryKey: ['foo'], exact: true })
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should remove queries from storage with exact match', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister, client, queryKey } = setupPersister(['foo', 'bar'], {
|
||||
storage,
|
||||
})
|
||||
client.setQueryData(queryKey, 'foo')
|
||||
|
||||
await persister.persistQueryByKey(queryKey, client)
|
||||
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
await persister.removeQueries({
|
||||
queryKey: queryKey,
|
||||
exact: true,
|
||||
})
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should remove entries that cannot be deserialized', async () => {
|
||||
const storage = getFreshStorage()
|
||||
const { persister } = setupPersister(['foo'], { storage })
|
||||
|
||||
await storage.setItem(`${PERSISTER_KEY_PREFIX}-["foo"]`, 'not-json{')
|
||||
expect(await storage.entries()).toHaveLength(1)
|
||||
|
||||
await persister.removeQueries({ queryKey: ['foo'] })
|
||||
|
||||
expect(await storage.entries()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
300
packages/query-persist-client-core/src/__tests__/persist.test.ts
Normal file
300
packages/query-persist-client-core/src/__tests__/persist.test.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MutationObserver,
|
||||
QueriesObserver,
|
||||
QueryClient,
|
||||
dehydrate,
|
||||
} from '@tanstack/query-core'
|
||||
import {
|
||||
persistQueryClient,
|
||||
persistQueryClientRestore,
|
||||
persistQueryClientSubscribe,
|
||||
} from '../persist'
|
||||
import { createMockPersister, createSpyPersister } from './utils'
|
||||
|
||||
describe('persist', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
})
|
||||
|
||||
describe('persistQueryClientSubscribe', () => {
|
||||
it('should persist mutations', async () => {
|
||||
const persister = createMockPersister()
|
||||
|
||||
const unsubscribe = persistQueryClientSubscribe({
|
||||
queryClient,
|
||||
persister,
|
||||
dehydrateOptions: { shouldDehydrateMutation: () => true },
|
||||
})
|
||||
|
||||
queryClient.getMutationCache().build(queryClient, {
|
||||
mutationFn: (text: string) => Promise.resolve(text),
|
||||
})
|
||||
|
||||
const result = await persister.restoreClient()
|
||||
|
||||
expect(result?.clientState.mutations).toHaveLength(1)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistQueryClientSave', () => {
|
||||
it('should not be triggered on observer type events', () => {
|
||||
const persister = createSpyPersister()
|
||||
|
||||
const unsubscribe = persistQueryClientSubscribe({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
|
||||
const queryKey = ['test']
|
||||
const queryFn = vi.fn().mockReturnValue(1)
|
||||
const observer = new QueriesObserver(queryClient, [{ queryKey, queryFn }])
|
||||
const unsubscribeObserver = observer.subscribe(vi.fn())
|
||||
observer
|
||||
.getObservers()[0]
|
||||
?.setOptions({ queryKey, refetchOnWindowFocus: false })
|
||||
unsubscribeObserver()
|
||||
|
||||
queryClient.setQueryData(queryKey, 2)
|
||||
|
||||
// persistClient should be called 3 times:
|
||||
// 1. When query is added
|
||||
// 2. When queryFn is resolved
|
||||
// 3. When setQueryData is called
|
||||
// All events fired by manipulating observers are ignored
|
||||
expect(persister.persistClient).toHaveBeenCalledTimes(3)
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should not be triggered on mutation observer type events', () => {
|
||||
const persister = createSpyPersister()
|
||||
|
||||
const unsubscribe = persistQueryClientSubscribe({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
|
||||
const observer = new MutationObserver(queryClient, {
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
})
|
||||
const unsubscribeObserver = observer.subscribe(vi.fn())
|
||||
observer.setOptions({ mutationKey: ['test'] })
|
||||
unsubscribeObserver()
|
||||
|
||||
// Events fired by manipulating the mutation observer are not cache
|
||||
// events, so they must not trigger a persist.
|
||||
expect(persister.persistClient).not.toHaveBeenCalled()
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistQueryClientRestore', () => {
|
||||
let persister: ReturnType<typeof createSpyPersister>
|
||||
|
||||
beforeEach(() => {
|
||||
persister = createSpyPersister()
|
||||
})
|
||||
|
||||
it('should rethrow exceptions in `restoreClient`', async () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const consoleWarn = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const restoreError = new Error('Error restoring client')
|
||||
|
||||
persister.restoreClient = () => Promise.reject(restoreError)
|
||||
|
||||
await expect(
|
||||
persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
}),
|
||||
).rejects.toBe(restoreError)
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledTimes(1)
|
||||
expect(consoleWarn).toHaveBeenCalledTimes(1)
|
||||
expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError)
|
||||
|
||||
consoleMock.mockRestore()
|
||||
consoleWarn.mockRestore()
|
||||
})
|
||||
|
||||
it('should rethrow exceptions in `removeClient` before `restoreClient`', async () => {
|
||||
const consoleMock = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const consoleWarn = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const restoreError = new Error('Error restoring client')
|
||||
const removeError = new Error('Error removing client')
|
||||
|
||||
persister.restoreClient = () => Promise.reject(restoreError)
|
||||
persister.removeClient = () => Promise.reject(removeError)
|
||||
|
||||
await expect(
|
||||
persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
}),
|
||||
).rejects.toBe(removeError)
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledTimes(1)
|
||||
expect(consoleWarn).toHaveBeenCalledTimes(1)
|
||||
expect(consoleMock).toHaveBeenNthCalledWith(1, restoreError)
|
||||
|
||||
consoleMock.mockRestore()
|
||||
consoleWarn.mockRestore()
|
||||
})
|
||||
|
||||
it('should rethrow error in `removeClient`', async () => {
|
||||
const removeError = new Error('Error removing client')
|
||||
|
||||
persister.removeClient = () => Promise.reject(removeError)
|
||||
persister.restoreClient = () => {
|
||||
return Promise.resolve({
|
||||
buster: 'random-buster',
|
||||
clientState: {
|
||||
mutations: [],
|
||||
queries: [],
|
||||
},
|
||||
timestamp: new Date().getTime(),
|
||||
})
|
||||
}
|
||||
|
||||
await expect(
|
||||
persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
}),
|
||||
).rejects.toBe(removeError)
|
||||
})
|
||||
|
||||
it('should hydrate the query client when the persisted cache is valid', async () => {
|
||||
const sourceClient = new QueryClient()
|
||||
sourceClient.setQueryData(['key'], 'data')
|
||||
|
||||
persister.restoreClient = () =>
|
||||
Promise.resolve({
|
||||
buster: '',
|
||||
clientState: dehydrate(sourceClient),
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
await persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
|
||||
expect(persister.removeClient).not.toHaveBeenCalled()
|
||||
expect(queryClient.getQueryData(['key'])).toBe('data')
|
||||
})
|
||||
|
||||
it('should remove the client when the persisted cache is expired', async () => {
|
||||
persister.restoreClient = () =>
|
||||
Promise.resolve({
|
||||
buster: '',
|
||||
clientState: { mutations: [], queries: [] },
|
||||
timestamp: Date.now() - 1000,
|
||||
})
|
||||
|
||||
await persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
maxAge: 100,
|
||||
})
|
||||
|
||||
expect(persister.removeClient).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should remove the client when the buster does not match', async () => {
|
||||
persister.restoreClient = () =>
|
||||
Promise.resolve({
|
||||
buster: 'old-buster',
|
||||
clientState: { mutations: [], queries: [] },
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
await persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
buster: 'new-buster',
|
||||
})
|
||||
|
||||
expect(persister.removeClient).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should remove the client when the persisted cache has no timestamp', async () => {
|
||||
persister.restoreClient = () =>
|
||||
Promise.resolve({
|
||||
buster: '',
|
||||
clientState: { mutations: [], queries: [] },
|
||||
timestamp: 0,
|
||||
})
|
||||
|
||||
await persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
|
||||
expect(persister.removeClient).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistQueryClient', () => {
|
||||
it('should subscribe to the query cache after a successful restore', async () => {
|
||||
const persister = createSpyPersister()
|
||||
persister.restoreClient = () => Promise.resolve(undefined)
|
||||
|
||||
const [unsubscribe, restorePromise] = persistQueryClient({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
await restorePromise
|
||||
|
||||
queryClient.setQueryData(['key'], 'data')
|
||||
|
||||
expect(persister.persistClient).toHaveBeenCalled()
|
||||
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('should not subscribe when unsubscribed before the restore completes', async () => {
|
||||
const persister = createSpyPersister()
|
||||
let resolveRestore: () => void
|
||||
persister.restoreClient = () =>
|
||||
new Promise((resolve) => {
|
||||
resolveRestore = () => resolve(undefined)
|
||||
})
|
||||
|
||||
const [unsubscribe, restorePromise] = persistQueryClient({
|
||||
queryClient,
|
||||
persister,
|
||||
})
|
||||
// Unsubscribe before the restore resolves
|
||||
unsubscribe()
|
||||
resolveRestore!()
|
||||
await restorePromise
|
||||
|
||||
queryClient.setQueryData(['key'], 'data')
|
||||
|
||||
expect(persister.persistClient).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { QueryClient, dehydrate } from '@tanstack/query-core'
|
||||
import { removeOldestQuery } from '../retryStrategies'
|
||||
import type { PersistedClient } from '../persist'
|
||||
|
||||
type PersistedQuery = PersistedClient['clientState']['queries'][number]
|
||||
|
||||
describe('removeOldestQuery', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
})
|
||||
|
||||
function createQuery(
|
||||
queryHash: string,
|
||||
dataUpdatedAt: number,
|
||||
): PersistedQuery {
|
||||
queryClient.setQueryData([queryHash], 'data')
|
||||
const query = dehydrate(queryClient).queries.find(
|
||||
(dehydratedQuery) => dehydratedQuery.queryHash === `["${queryHash}"]`,
|
||||
)!
|
||||
return { ...query, state: { ...query.state, dataUpdatedAt } }
|
||||
}
|
||||
|
||||
function createPersistedClient(
|
||||
queries: Array<PersistedQuery>,
|
||||
mutations: PersistedClient['clientState']['mutations'] = [],
|
||||
): PersistedClient {
|
||||
return {
|
||||
timestamp: 0,
|
||||
buster: '',
|
||||
clientState: { queries, mutations },
|
||||
}
|
||||
}
|
||||
|
||||
it('should remove the query with the oldest dataUpdatedAt', () => {
|
||||
const persistedClient = createPersistedClient([
|
||||
createQuery('a', 30),
|
||||
createQuery('b', 10),
|
||||
createQuery('c', 20),
|
||||
])
|
||||
|
||||
const result = removeOldestQuery({
|
||||
persistedClient,
|
||||
error: new Error('full'),
|
||||
errorCount: 1,
|
||||
})
|
||||
|
||||
expect(result?.clientState.queries.map((query) => query.queryKey)).toEqual([
|
||||
['a'],
|
||||
['c'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should remove only a single query when multiple share the oldest dataUpdatedAt', () => {
|
||||
const persistedClient = createPersistedClient([
|
||||
createQuery('a', 10),
|
||||
createQuery('b', 10),
|
||||
])
|
||||
|
||||
const result = removeOldestQuery({
|
||||
persistedClient,
|
||||
error: new Error('full'),
|
||||
errorCount: 1,
|
||||
})
|
||||
|
||||
expect(result?.clientState.queries.map((query) => query.queryKey)).toEqual([
|
||||
['b'],
|
||||
])
|
||||
})
|
||||
|
||||
it('should return undefined when there are no queries to remove', () => {
|
||||
const persistedClient = createPersistedClient([])
|
||||
|
||||
const result = removeOldestQuery({
|
||||
persistedClient,
|
||||
error: new Error('full'),
|
||||
errorCount: 1,
|
||||
})
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should preserve mutations when removing the oldest query', () => {
|
||||
queryClient.getMutationCache().build(queryClient, {
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
})
|
||||
const { mutations } = dehydrate(queryClient, {
|
||||
shouldDehydrateMutation: () => true,
|
||||
})
|
||||
|
||||
const persistedClient = createPersistedClient(
|
||||
[createQuery('a', 10)],
|
||||
mutations,
|
||||
)
|
||||
|
||||
const result = removeOldestQuery({
|
||||
persistedClient,
|
||||
error: new Error('full'),
|
||||
errorCount: 1,
|
||||
})
|
||||
|
||||
expect(result?.clientState.queries).toEqual([])
|
||||
expect(result?.clientState.mutations).toEqual(mutations)
|
||||
})
|
||||
})
|
||||
28
packages/query-persist-client-core/src/__tests__/utils.ts
Normal file
28
packages/query-persist-client-core/src/__tests__/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { vi } from 'vitest'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
import type { PersistedClient, Persister } from '../persist'
|
||||
|
||||
export function createMockPersister(): Persister {
|
||||
let storedState: PersistedClient | undefined
|
||||
|
||||
return {
|
||||
persistClient(persistClient: PersistedClient) {
|
||||
storedState = persistClient
|
||||
},
|
||||
async restoreClient() {
|
||||
await sleep(10)
|
||||
return storedState
|
||||
},
|
||||
removeClient() {
|
||||
storedState = undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createSpyPersister(): Persister {
|
||||
return {
|
||||
persistClient: vi.fn(),
|
||||
restoreClient: vi.fn(),
|
||||
removeClient: vi.fn(),
|
||||
}
|
||||
}
|
||||
372
packages/query-persist-client-core/src/createPersister.ts
Normal file
372
packages/query-persist-client-core/src/createPersister.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import {
|
||||
hashKey,
|
||||
matchQuery,
|
||||
notifyManager,
|
||||
partialMatchKey,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
Query,
|
||||
QueryClient,
|
||||
QueryFilters,
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
QueryState,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export interface PersistedQuery {
|
||||
buster: string
|
||||
queryHash: string
|
||||
queryKey: QueryKey
|
||||
state: QueryState
|
||||
}
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>
|
||||
|
||||
export interface AsyncStorage<TStorageValue = string> {
|
||||
getItem: (key: string) => MaybePromise<TStorageValue | undefined | null>
|
||||
setItem: (key: string, value: TStorageValue) => MaybePromise<unknown>
|
||||
removeItem: (key: string) => MaybePromise<void>
|
||||
entries?: () => MaybePromise<Array<[key: string, value: TStorageValue]>>
|
||||
}
|
||||
|
||||
export interface StoragePersisterOptions<TStorageValue = string> {
|
||||
/** The storage client used for setting and retrieving items from cache.
|
||||
* For SSR pass in `undefined`.
|
||||
*/
|
||||
storage: AsyncStorage<TStorageValue> | undefined | null
|
||||
/**
|
||||
* How to serialize the data to storage.
|
||||
* @default `JSON.stringify`
|
||||
*/
|
||||
serialize?: (persistedQuery: PersistedQuery) => MaybePromise<TStorageValue>
|
||||
/**
|
||||
* How to deserialize the data from storage.
|
||||
* @default `JSON.parse`
|
||||
*/
|
||||
deserialize?: (cachedString: TStorageValue) => MaybePromise<PersistedQuery>
|
||||
/**
|
||||
* A unique string that can be used to forcefully invalidate existing caches,
|
||||
* if they do not share the same buster string
|
||||
*/
|
||||
buster?: string
|
||||
/**
|
||||
* The max-allowed age of the cache in milliseconds.
|
||||
* If a persisted cache is found that is older than this
|
||||
* time, it will be discarded
|
||||
* @default 24 hours
|
||||
*/
|
||||
maxAge?: number
|
||||
/**
|
||||
* Prefix to be used for storage key.
|
||||
* Storage key is a combination of prefix and query hash in a form of `prefix-queryHash`.
|
||||
* @default 'tanstack-query'
|
||||
*/
|
||||
prefix?: string
|
||||
/**
|
||||
* If set to `true`, the query will refetch on successful query restoration if the data is stale.
|
||||
* If set to `false`, the query will not refetch on successful query restoration.
|
||||
* If set to `'always'`, the query will always refetch on successful query restoration.
|
||||
* Defaults to `true`.
|
||||
*/
|
||||
refetchOnRestore?: boolean | 'always'
|
||||
/**
|
||||
* Filters to narrow down which Queries should be persisted.
|
||||
*/
|
||||
filters?: QueryFilters
|
||||
}
|
||||
|
||||
export const PERSISTER_KEY_PREFIX = 'tanstack-query'
|
||||
|
||||
/**
|
||||
* Warning: experimental feature.
|
||||
* This utility function enables fine-grained query persistence.
|
||||
* Simple add it as a `persister` parameter to `useQuery` or `defaultOptions` on `queryClient`.
|
||||
*
|
||||
* ```
|
||||
* useQuery({
|
||||
queryKey: ['myKey'],
|
||||
queryFn: fetcher,
|
||||
persister: createPersister({
|
||||
storage: localStorage,
|
||||
}),
|
||||
})
|
||||
```
|
||||
*/
|
||||
export function experimental_createQueryPersister<TStorageValue = string>({
|
||||
storage,
|
||||
buster = '',
|
||||
maxAge = 1000 * 60 * 60 * 24,
|
||||
serialize = JSON.stringify as Required<
|
||||
StoragePersisterOptions<TStorageValue>
|
||||
>['serialize'],
|
||||
deserialize = JSON.parse as Required<
|
||||
StoragePersisterOptions<TStorageValue>
|
||||
>['deserialize'],
|
||||
prefix = PERSISTER_KEY_PREFIX,
|
||||
refetchOnRestore = true,
|
||||
filters,
|
||||
}: StoragePersisterOptions<TStorageValue>) {
|
||||
function isExpiredOrBusted(persistedQuery: PersistedQuery) {
|
||||
if (persistedQuery.state.dataUpdatedAt) {
|
||||
const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt
|
||||
const expired = queryAge > maxAge
|
||||
const busted = persistedQuery.buster !== buster
|
||||
|
||||
if (expired || busted) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function retrieveQuery<T>(
|
||||
queryHash: string,
|
||||
afterRestoreMacroTask?: (persistedQuery: PersistedQuery) => void,
|
||||
) {
|
||||
if (storage != null) {
|
||||
const storageKey = `${prefix}-${queryHash}`
|
||||
try {
|
||||
const storedData = await storage.getItem(storageKey)
|
||||
if (storedData) {
|
||||
let persistedQuery: PersistedQuery
|
||||
try {
|
||||
persistedQuery = await deserialize(storedData)
|
||||
} catch {
|
||||
await storage.removeItem(storageKey)
|
||||
return
|
||||
}
|
||||
|
||||
if (isExpiredOrBusted(persistedQuery)) {
|
||||
await storage.removeItem(storageKey)
|
||||
} else {
|
||||
if (afterRestoreMacroTask) {
|
||||
// Just after restoring we want to get fresh data from the server if it's stale
|
||||
notifyManager.schedule(() =>
|
||||
afterRestoreMacroTask(persistedQuery),
|
||||
)
|
||||
}
|
||||
// We must resolve the promise here, as otherwise we will have `loading` state in the app until `queryFn` resolves
|
||||
return persistedQuery.state.data as T
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error(err)
|
||||
console.warn(
|
||||
'Encountered an error attempting to restore query cache from persisted location.',
|
||||
)
|
||||
}
|
||||
await storage.removeItem(storageKey)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
async function persistQueryByKey(
|
||||
queryKey: QueryKey,
|
||||
queryClient: QueryClient,
|
||||
) {
|
||||
if (storage != null) {
|
||||
const query = queryClient.getQueryCache().find({ queryKey })
|
||||
if (query) {
|
||||
await persistQuery(query)
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(
|
||||
'Could not find query to be persisted. QueryKey:',
|
||||
JSON.stringify(queryKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function persistQuery(query: Query) {
|
||||
if (storage != null) {
|
||||
const storageKey = `${prefix}-${query.queryHash}`
|
||||
storage.setItem(
|
||||
storageKey,
|
||||
await serialize({
|
||||
state: query.state,
|
||||
queryKey: query.queryKey,
|
||||
queryHash: query.queryHash,
|
||||
buster: buster,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function persisterFn<T, TQueryKey extends QueryKey>(
|
||||
queryFn: (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>,
|
||||
ctx: QueryFunctionContext<TQueryKey>,
|
||||
query: Query,
|
||||
) {
|
||||
const matchesFilter = filters ? matchQuery(filters, query) : true
|
||||
|
||||
// Try to restore only if we do not have any data in the cache and we have persister defined
|
||||
if (matchesFilter && query.state.data === undefined && storage != null) {
|
||||
const restoredData = await retrieveQuery(
|
||||
query.queryHash,
|
||||
(persistedQuery: PersistedQuery) => {
|
||||
// Set proper updatedAt, since resolving in the first pass overrides those values
|
||||
query.setState({
|
||||
dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
|
||||
errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
|
||||
})
|
||||
|
||||
if (
|
||||
refetchOnRestore === 'always' ||
|
||||
(refetchOnRestore === true && query.isStale())
|
||||
) {
|
||||
query.fetch()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (restoredData !== undefined) {
|
||||
return Promise.resolve(restoredData as T)
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not restore, or restoration failed - fetch
|
||||
const queryFnResult = await queryFn(ctx)
|
||||
|
||||
if (matchesFilter && storage != null) {
|
||||
// Persist if we have storage defined, we use timeout to get proper state to be persisted
|
||||
notifyManager.schedule(() => {
|
||||
persistQuery(query)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(queryFnResult)
|
||||
}
|
||||
|
||||
async function persisterGc() {
|
||||
if (storage?.entries) {
|
||||
const storageKeyPrefix = `${prefix}-`
|
||||
const entries = await storage.entries()
|
||||
for (const [key, value] of entries) {
|
||||
if (key.startsWith(storageKeyPrefix)) {
|
||||
let persistedQuery: PersistedQuery
|
||||
try {
|
||||
persistedQuery = await deserialize(value)
|
||||
} catch {
|
||||
await storage.removeItem(key)
|
||||
continue
|
||||
}
|
||||
if (isExpiredOrBusted(persistedQuery)) {
|
||||
await storage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(
|
||||
'Provided storage does not implement `entries` method. Garbage collection is not possible without ability to iterate over storage items.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreQueries(
|
||||
queryClient: QueryClient,
|
||||
filters: Pick<QueryFilters, 'queryKey' | 'exact'> = {},
|
||||
): Promise<void> {
|
||||
const { exact, queryKey } = filters
|
||||
|
||||
if (storage?.entries) {
|
||||
const storageKeyPrefix = `${prefix}-`
|
||||
const entries = await storage.entries()
|
||||
for (const [key, value] of entries) {
|
||||
if (key.startsWith(storageKeyPrefix)) {
|
||||
let persistedQuery: PersistedQuery
|
||||
try {
|
||||
persistedQuery = await deserialize(value)
|
||||
} catch {
|
||||
await storage.removeItem(key)
|
||||
continue
|
||||
}
|
||||
if (isExpiredOrBusted(persistedQuery)) {
|
||||
await storage.removeItem(key)
|
||||
continue
|
||||
}
|
||||
|
||||
if (queryKey) {
|
||||
if (exact) {
|
||||
if (persistedQuery.queryHash !== hashKey(queryKey)) {
|
||||
continue
|
||||
}
|
||||
} else if (!partialMatchKey(persistedQuery.queryKey, queryKey)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.setQueryData(
|
||||
persistedQuery.queryKey,
|
||||
persistedQuery.state.data,
|
||||
{
|
||||
updatedAt: persistedQuery.state.dataUpdatedAt,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(
|
||||
'Provided storage does not implement `entries` method. Restoration of all stored entries is not possible without ability to iterate over storage items.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeQueries(
|
||||
filters: Pick<QueryFilters, 'queryKey' | 'exact'> = {},
|
||||
): Promise<void> {
|
||||
const { exact, queryKey } = filters
|
||||
|
||||
if (storage?.entries) {
|
||||
const entries = await storage.entries()
|
||||
const storageKeyPrefix = `${prefix}-`
|
||||
for (const [key, value] of entries) {
|
||||
if (key.startsWith(storageKeyPrefix)) {
|
||||
if (!queryKey) {
|
||||
await storage.removeItem(key)
|
||||
continue
|
||||
}
|
||||
|
||||
let persistedQuery: PersistedQuery
|
||||
try {
|
||||
persistedQuery = await deserialize(value)
|
||||
} catch {
|
||||
await storage.removeItem(key)
|
||||
continue
|
||||
}
|
||||
|
||||
if (exact) {
|
||||
if (persistedQuery.queryHash !== hashKey(queryKey)) {
|
||||
continue
|
||||
}
|
||||
} else if (!partialMatchKey(persistedQuery.queryKey, queryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
await storage.removeItem(key)
|
||||
}
|
||||
}
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error(
|
||||
'Provided storage does not implement `entries` method. Removal of stored entries is not possible without ability to iterate over storage items.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
persisterFn,
|
||||
persistQuery,
|
||||
persistQueryByKey,
|
||||
retrieveQuery,
|
||||
persisterGc,
|
||||
restoreQueries,
|
||||
removeQueries,
|
||||
}
|
||||
}
|
||||
5
packages/query-persist-client-core/src/index.ts
Normal file
5
packages/query-persist-client-core/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export * from './persist'
|
||||
export * from './retryStrategies'
|
||||
export * from './createPersister'
|
||||
179
packages/query-persist-client-core/src/persist.ts
Normal file
179
packages/query-persist-client-core/src/persist.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { dehydrate, hydrate } from '@tanstack/query-core'
|
||||
import type {
|
||||
DehydrateOptions,
|
||||
DehydratedState,
|
||||
HydrateOptions,
|
||||
NotifyEventType,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export type Promisable<T> = T | PromiseLike<T>
|
||||
|
||||
export interface Persister {
|
||||
persistClient: (persistClient: PersistedClient) => Promisable<void>
|
||||
restoreClient: () => Promisable<PersistedClient | undefined>
|
||||
removeClient: () => Promisable<void>
|
||||
}
|
||||
|
||||
export interface PersistedClient {
|
||||
timestamp: number
|
||||
buster: string
|
||||
clientState: DehydratedState
|
||||
}
|
||||
|
||||
export interface PersistQueryClientRootOptions {
|
||||
/** The QueryClient to persist */
|
||||
queryClient: QueryClient
|
||||
/** The Persister interface for storing and restoring the cache
|
||||
* to/from a persisted location */
|
||||
persister: Persister
|
||||
/** A unique string that can be used to forcefully
|
||||
* invalidate existing caches if they do not share the same buster string */
|
||||
buster?: string
|
||||
}
|
||||
|
||||
export interface PersistedQueryClientRestoreOptions extends PersistQueryClientRootOptions {
|
||||
/** The max-allowed age of the cache in milliseconds.
|
||||
* If a persisted cache is found that is older than this
|
||||
* time, it will be discarded */
|
||||
maxAge?: number
|
||||
/** The options passed to the hydrate function */
|
||||
hydrateOptions?: HydrateOptions
|
||||
}
|
||||
|
||||
export interface PersistedQueryClientSaveOptions extends PersistQueryClientRootOptions {
|
||||
/** The options passed to the dehydrate function */
|
||||
dehydrateOptions?: DehydrateOptions
|
||||
}
|
||||
|
||||
export interface PersistQueryClientOptions
|
||||
extends
|
||||
PersistedQueryClientRestoreOptions,
|
||||
PersistedQueryClientSaveOptions,
|
||||
PersistQueryClientRootOptions {}
|
||||
|
||||
/**
|
||||
* Checks if emitted event is about cache change and not about observers.
|
||||
* Useful for persist, where we only want to trigger save when cache is changed.
|
||||
*/
|
||||
const cacheEventTypes: Array<NotifyEventType> = ['added', 'removed', 'updated']
|
||||
|
||||
function isCacheEventType(eventType: NotifyEventType) {
|
||||
return cacheEventTypes.includes(eventType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores persisted data to the QueryCache
|
||||
* - data obtained from persister.restoreClient
|
||||
* - data is hydrated using hydrateOptions
|
||||
* If data is expired, busted, empty, or throws, it runs persister.removeClient
|
||||
*/
|
||||
export async function persistQueryClientRestore({
|
||||
queryClient,
|
||||
persister,
|
||||
maxAge = 1000 * 60 * 60 * 24,
|
||||
buster = '',
|
||||
hydrateOptions,
|
||||
}: PersistedQueryClientRestoreOptions) {
|
||||
try {
|
||||
const persistedClient = await persister.restoreClient()
|
||||
|
||||
if (persistedClient) {
|
||||
if (persistedClient.timestamp) {
|
||||
const expired = Date.now() - persistedClient.timestamp > maxAge
|
||||
const busted = persistedClient.buster !== buster
|
||||
if (expired || busted) {
|
||||
return persister.removeClient()
|
||||
} else {
|
||||
hydrate(queryClient, persistedClient.clientState, hydrateOptions)
|
||||
}
|
||||
} else {
|
||||
return persister.removeClient()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.error(err)
|
||||
console.warn(
|
||||
'Encountered an error attempting to restore client cache from persisted location. As a precaution, the persisted cache will be discarded.',
|
||||
)
|
||||
}
|
||||
|
||||
await persister.removeClient()
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists data from the QueryCache
|
||||
* - data dehydrated using dehydrateOptions
|
||||
* - data is persisted using persister.persistClient
|
||||
*/
|
||||
export async function persistQueryClientSave({
|
||||
queryClient,
|
||||
persister,
|
||||
buster = '',
|
||||
dehydrateOptions,
|
||||
}: PersistedQueryClientSaveOptions) {
|
||||
const persistClient: PersistedClient = {
|
||||
buster,
|
||||
timestamp: Date.now(),
|
||||
clientState: dehydrate(queryClient, dehydrateOptions),
|
||||
}
|
||||
|
||||
await persister.persistClient(persistClient)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to QueryCache and MutationCache updates (for persisting)
|
||||
* @returns an unsubscribe function (to discontinue monitoring)
|
||||
*/
|
||||
export function persistQueryClientSubscribe(
|
||||
props: PersistedQueryClientSaveOptions,
|
||||
) {
|
||||
const unsubscribeQueryCache = props.queryClient
|
||||
.getQueryCache()
|
||||
.subscribe((event) => {
|
||||
if (isCacheEventType(event.type)) {
|
||||
persistQueryClientSave(props)
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeMutationCache = props.queryClient
|
||||
.getMutationCache()
|
||||
.subscribe((event) => {
|
||||
if (isCacheEventType(event.type)) {
|
||||
persistQueryClientSave(props)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribeQueryCache()
|
||||
unsubscribeMutationCache()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores persisted data to QueryCache and persists further changes.
|
||||
*/
|
||||
export function persistQueryClient(
|
||||
props: PersistQueryClientOptions,
|
||||
): [() => void, Promise<void>] {
|
||||
let hasUnsubscribed = false
|
||||
let persistQueryClientUnsubscribe: (() => void) | undefined
|
||||
const unsubscribe = () => {
|
||||
hasUnsubscribed = true
|
||||
persistQueryClientUnsubscribe?.()
|
||||
}
|
||||
|
||||
// Attempt restore
|
||||
const restorePromise = persistQueryClientRestore(props).then(() => {
|
||||
if (!hasUnsubscribed) {
|
||||
// Subscribe to changes in the query cache to trigger the save
|
||||
persistQueryClientUnsubscribe = persistQueryClientSubscribe(props)
|
||||
}
|
||||
})
|
||||
|
||||
return [unsubscribe, restorePromise]
|
||||
}
|
||||
30
packages/query-persist-client-core/src/retryStrategies.ts
Normal file
30
packages/query-persist-client-core/src/retryStrategies.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { PersistedClient } from './persist'
|
||||
|
||||
export type PersistRetryer = (props: {
|
||||
persistedClient: PersistedClient
|
||||
error: Error
|
||||
errorCount: number
|
||||
}) => PersistedClient | undefined
|
||||
|
||||
export const removeOldestQuery: PersistRetryer = ({ persistedClient }) => {
|
||||
const mutations = [...persistedClient.clientState.mutations]
|
||||
const queries = [...persistedClient.clientState.queries]
|
||||
const client: PersistedClient = {
|
||||
...persistedClient,
|
||||
clientState: { mutations, queries },
|
||||
}
|
||||
|
||||
// sort queries by dataUpdatedAt (oldest first)
|
||||
const sortedQueries = [...queries].sort(
|
||||
(a, b) => a.state.dataUpdatedAt - b.state.dataUpdatedAt,
|
||||
)
|
||||
|
||||
// clean oldest query
|
||||
if (sortedQueries.length > 0) {
|
||||
const oldestData = sortedQueries.shift()
|
||||
client.clientState.queries = queries.filter((q) => q !== oldestData)
|
||||
return client
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
9
packages/query-persist-client-core/tsconfig.json
Normal file
9
packages/query-persist-client-core/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist-ts",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src", "*.config.ts", "*.config.js", "package.json"],
|
||||
"references": [{ "path": "../query-core" }]
|
||||
}
|
||||
10
packages/query-persist-client-core/tsconfig.prod.json
Normal file
10
packages/query-persist-client-core/tsconfig.prod.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"composite": false,
|
||||
"rootDir": "../../"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__tests__"]
|
||||
}
|
||||
7
packages/query-persist-client-core/tsdown.config.ts
Normal file
7
packages/query-persist-client-core/tsdown.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
import { legacyConfig, modernConfig } from './root.tsdown.config.js'
|
||||
|
||||
export default defineConfig([
|
||||
modernConfig({ entry: ['src/*.ts'] }),
|
||||
legacyConfig({ entry: ['src/*.ts'] }),
|
||||
])
|
||||
30
packages/query-persist-client-core/vite.config.ts
Normal file
30
packages/query-persist-client-core/vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
import packageJson from './package.json'
|
||||
|
||||
export default defineConfig({
|
||||
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
environments: {
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: packageJson.name,
|
||||
dir: './src',
|
||||
watch: false,
|
||||
coverage: {
|
||||
enabled: !!process.env.CI,
|
||||
provider: 'istanbul',
|
||||
include: ['src/**/*'],
|
||||
exclude: ['src/__tests__/**'],
|
||||
},
|
||||
typecheck: { enabled: true },
|
||||
restoreMocks: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user