Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fifty-zebras-stay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/query-persist-client-core': minor
---

Add removeQueries to experimental_createQueryPersister
15 changes: 15 additions & 0 deletions docs/framework/react/plugins/createPersister.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>`

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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
59 changes: 56 additions & 3 deletions packages/query-persist-client-core/src/createPersister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,13 @@ export function experimental_createQueryPersister<TStorageValue = string>({
const entries = await storage.entries()
for (const [key, value] of entries) {
if (key.startsWith(prefix)) {
const persistedQuery = await deserialize(value)
let persistedQuery: PersistedQuery
try {
persistedQuery = await deserialize(value)
} catch {
await storage.removeItem(key)
continue
}

if (isExpiredOrBusted(persistedQuery)) {
await storage.removeItem(key)
Expand All @@ -266,9 +272,15 @@ export function experimental_createQueryPersister<TStorageValue = string>({

if (storage?.entries) {
const entries = await storage.entries()
const storageKeyPrefix = `${prefix}-`
for (const [key, value] of entries) {
if (key.startsWith(prefix)) {
const persistedQuery = await deserialize(value)
if (key.startsWith(storageKeyPrefix)) {
let persistedQuery: PersistedQuery
try {
persistedQuery = await deserialize(value)
} catch {
continue
}

if (isExpiredOrBusted(persistedQuery)) {
await storage.removeItem(key)
Expand Down Expand Up @@ -301,12 +313,53 @@ export function experimental_createQueryPersister<TStorageValue = string>({
}
}

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 {
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,
}
}
Loading