diff --git a/.changeset/fifty-zebras-stay.md b/.changeset/fifty-zebras-stay.md new file mode 100644 index 00000000000..197f98b390e --- /dev/null +++ b/.changeset/fifty-zebras-stay.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-persist-client-core': minor +--- + +Add removeQueries to experimental_createQueryPersister diff --git a/docs/framework/react/plugins/createPersister.md b/docs/framework/react/plugins/createPersister.md index 413979a7e0f..0b59b216ce1 100644 --- a/docs/framework/react/plugins/createPersister.md +++ b/docs/framework/react/plugins/createPersister.md @@ -126,6 +126,21 @@ The filter object supports the following properties: For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. +### `removeQueries(filters): Promise` + +When using `queryClient.removeQueries`, the data remains in the persister and needs to be removed separately. +This function can be used to remove queries that are currently stored by persister. + +The filter object supports the following properties: + +- `queryKey?: QueryKey` + - Set this property to define a query key to match on. +- `exact?: boolean` + - If you don't want to search queries inclusively by query key, you can pass the `exact: true` option to return only the query with the exact query key you have passed. + +For this function to work, your storage must expose `entries` method that would return a `key-value tuple array`. +For example `Object.entries(localStorage)` for `localStorage` or `entries` from `idb-keyval`. + ## API ### `experimental_createQueryPersister` diff --git a/packages/query-persist-client-core/src/__tests__/createPersister.test.ts b/packages/query-persist-client-core/src/__tests__/createPersister.test.ts index 5434409264f..e9ff7eb8bf3 100644 --- a/packages/query-persist-client-core/src/__tests__/createPersister.test.ts +++ b/packages/query-persist-client-core/src/__tests__/createPersister.test.ts @@ -675,4 +675,93 @@ describe('createPersister', () => { expect(client.getQueryCache().getAll()).toHaveLength(1) }) }) + + describe('removeQueries', () => { + test('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) + }) + + test('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) + }) + + test('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) + }) + + test('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) + }) + + test('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) + }) + + test('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) + }) + }) }) diff --git a/packages/query-persist-client-core/src/createPersister.ts b/packages/query-persist-client-core/src/createPersister.ts index 55fccc5558b..780ae4d6acd 100644 --- a/packages/query-persist-client-core/src/createPersister.ts +++ b/packages/query-persist-client-core/src/createPersister.ts @@ -301,6 +301,47 @@ export function experimental_createQueryPersister({ } } + async function removeQueries( + filters: Pick = {}, + ): Promise { + 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, @@ -308,5 +349,6 @@ export function experimental_createQueryPersister({ retrieveQuery, persisterGc, restoreQueries, + removeQueries, } }