diff --git a/.changeset/nine-pillows-greet.md b/.changeset/nine-pillows-greet.md new file mode 100644 index 000000000..4ab5e528d --- /dev/null +++ b/.changeset/nine-pillows-greet.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Add method to reset cache state diff --git a/packages/houdini/src/runtime/cache/cache.ts b/packages/houdini/src/runtime/cache/cache.ts index e7ab09fea..eb43b16fd 100644 --- a/packages/houdini/src/runtime/cache/cache.ts +++ b/packages/houdini/src/runtime/cache/cache.ts @@ -274,6 +274,27 @@ export class Cache { this.#notifySubscribers(toNotify) } + // reset the whole cache + reset() { + // Reset Subscriptions + const subSpecs = this._internal_unstable.subscriptions.reset() + + // Reset StaleManager + this._internal_unstable.staleManager.reset() + + // Reset GarbageCollector + this._internal_unstable.lifetimes.reset() + + // Reset Lists + this._internal_unstable.lists.reset() + + // Reset InMemory Storage + this._internal_unstable.storage.reset() + + // Notify Subscribers + this.#notifySubscribers(subSpecs) + } + #notifySubscribers(subs: SubscriptionSpec[]) { // if there's no one to notify, its a no-op if (subs.length === 0) { diff --git a/packages/houdini/src/runtime/cache/gc.ts b/packages/houdini/src/runtime/cache/gc.ts index 6ecd44ece..096af6acb 100644 --- a/packages/houdini/src/runtime/cache/gc.ts +++ b/packages/houdini/src/runtime/cache/gc.ts @@ -14,6 +14,10 @@ export class GarbageCollector { this.cache = cache } + reset() { + this.lifetimes.clear() + } + resetLifetime(id: string, field: string) { // if this is the first time we've seen the id if (!this.lifetimes.get(id)) { diff --git a/packages/houdini/src/runtime/cache/lists.ts b/packages/houdini/src/runtime/cache/lists.ts index 9e4edbf3b..72302e81e 100644 --- a/packages/houdini/src/runtime/cache/lists.ts +++ b/packages/houdini/src/runtime/cache/lists.ts @@ -144,6 +144,11 @@ export class ListManager { // delete the lists by field lookups this.listsByField.get(parentID)!.delete(field) } + + reset() { + this.lists.clear() + this.listsByField.clear() + } } export class List { diff --git a/packages/houdini/src/runtime/cache/staleManager.ts b/packages/houdini/src/runtime/cache/staleManager.ts index e8b93d7c1..ad1cc09e9 100644 --- a/packages/houdini/src/runtime/cache/staleManager.ts +++ b/packages/houdini/src/runtime/cache/staleManager.ts @@ -108,4 +108,8 @@ export class StaleManager { } } } + + reset() { + this.fieldsTime.clear() + } } diff --git a/packages/houdini/src/runtime/cache/storage.ts b/packages/houdini/src/runtime/cache/storage.ts index e7c39beb2..3791d65d6 100644 --- a/packages/houdini/src/runtime/cache/storage.ts +++ b/packages/houdini/src/runtime/cache/storage.ts @@ -289,6 +289,10 @@ export class InMemoryStorage { layer.fields = fields layer.links = links } + + reset() { + this.data = [] + } } export class Layer { diff --git a/packages/houdini/src/runtime/cache/subscription.ts b/packages/houdini/src/runtime/cache/subscription.ts index 57a389d79..8d908e416 100644 --- a/packages/houdini/src/runtime/cache/subscription.ts +++ b/packages/houdini/src/runtime/cache/subscription.ts @@ -6,7 +6,7 @@ import type { SubscriptionSpec, NestedList, } from '../lib/types' -import type { Cache } from './cache' +import { rootID, type Cache } from './cache' import { evaluateKey } from './stuff' export type FieldSelection = [ @@ -344,6 +344,25 @@ export class InMemorySubscriptions { } } + reset() { + // Get all subscriptions that do not start with the rootID + const subscribers = Object.entries(this.subscribers).filter( + ([id]) => !id.startsWith(rootID) + ) + + // Remove those subcribers from this.subscribers + for (const [id, _fields] of subscribers) { + delete this.subscribers[id] + } + + // Get list of all SubscriptionSpecs of subscribers + const subscriptionSpecs = subscribers.flatMap(([_id, fields]) => + Object.values(fields).flatMap((field) => field.map(([spec]) => spec)) + ) + + return subscriptionSpecs + } + private removeSubscribers(id: string, fieldName: string, specs: SubscriptionSpec[]) { // build up a list of the sets we actually need to remove after // checking reference counts diff --git a/packages/houdini/src/runtime/cache/tests/reset.test.ts b/packages/houdini/src/runtime/cache/tests/reset.test.ts new file mode 100644 index 000000000..c44c9dce7 --- /dev/null +++ b/packages/houdini/src/runtime/cache/tests/reset.test.ts @@ -0,0 +1,245 @@ +import { test, expect, vi } from 'vitest' + +import { testConfigFile } from '../../../test' +import { RefetchUpdateMode, type SubscriptionSelection } from '../../lib' +import { Cache } from '../cache' + +const config = testConfigFile() + +test('make sure the cache data was reset', function () { + const cache = new Cache(config) + + // save the data + const data = { + viewer: { + id: '1', + firstName: 'bob', + }, + } + + const selection: SubscriptionSelection = { + fields: { + viewer: { + type: 'User', + visible: true, + keyRaw: 'viewer', + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + firstName: { + type: 'String', + visible: true, + keyRaw: 'firstName', + }, + }, + }, + }, + }, + } + + cache.write({ + selection, + data, + }) + + // reset the cache + cache.reset() + + // make sure the data is gone + expect(cache.read({ selection }).data).toBe(null) +}) + +test('make sure the cache lists were reset', function () { + const cache = new Cache(config) + + const selection: SubscriptionSelection = { + fields: { + viewer: { + type: 'User', + visible: true, + keyRaw: 'viewer', + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + friends: { + type: 'User', + visible: true, + keyRaw: 'friends', + updates: [RefetchUpdateMode.append], + list: { + name: 'All_Users', + connection: false, + type: 'User', + }, + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + firstName: { + type: 'String', + visible: true, + keyRaw: 'firstName', + }, + }, + }, + }, + }, + }, + }, + }, + } + + cache.write({ + selection, + data: { + viewer: { + id: '1', + friends: [ + { + id: '2', + firstName: 'jane', + }, + { + id: '3', + firstName: 'joe', + }, + ], + }, + }, + applyUpdates: [RefetchUpdateMode.append], + }) + const set = vi.fn() + cache.subscribe({ + rootType: 'Query', + set, + selection, + }) + + expect(() => cache.list('All_Users')).toBeDefined() + + // reset the cache + cache.reset() + + // make sure the list doesn't exist + expect(() => cache.list('All_Users')).toThrowError('Cannot find list with name') +}) + +test('make sure the cache subscribers were reset', function () { + const cache = new Cache(config) + + const selection: SubscriptionSelection = { + fields: { + viewer: { + type: 'User', + visible: true, + keyRaw: 'viewer', + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + firstName: { + type: 'String', + visible: true, + keyRaw: 'firstName', + }, + friends: { + type: 'User', + visible: true, + keyRaw: 'friends', + updates: [RefetchUpdateMode.append], + selection: { + fields: { + id: { + type: 'ID', + visible: true, + keyRaw: 'id', + }, + firstName: { + type: 'String', + visible: true, + keyRaw: 'firstName', + }, + }, + }, + }, + }, + }, + }, + }, + } + + // subscribe to the cache + const set = vi.fn() + cache.subscribe({ + rootType: 'Query', + set, + selection, + }) + + // reset the cache + cache.reset() + + // write the data again + cache.write({ + parent: 'User:1', + selection, + data: { + viewer: { + id: '1', + firstName: 'bob', + friends: [ + { + id: '2', + firstName: 'jane', + }, + { + id: '3', + firstName: 'joe', + }, + ], + }, + }, + applyUpdates: [RefetchUpdateMode.append], + }) + + // make sure the subscriber was not called + expect(set).not.toHaveBeenCalled() + + cache.write({ + selection, + data: { + viewer: { + id: '1', + firstName: 'bob', + friends: [ + { + id: '4', + firstName: 'mary', + }, + { + id: '5', + firstName: 'jill', + }, + ], + }, + }, + applyUpdates: [RefetchUpdateMode.append], + }) + + // make sure the root subscriber was called because parent was not defined + expect(set).toHaveBeenCalled() +}) diff --git a/packages/houdini/src/runtime/public/cache.ts b/packages/houdini/src/runtime/public/cache.ts index b608a9b4d..5585e724a 100644 --- a/packages/houdini/src/runtime/public/cache.ts +++ b/packages/houdini/src/runtime/public/cache.ts @@ -124,4 +124,12 @@ Please acknowledge this by setting acceptImperativeInstability to true in your c ): void { return this._internal_unstable.markTypeStale(type ? { ...options, type } : undefined) } + + /** + * Reset the entire cache by clearing all records and lists + */ + + reset(): void { + return this._internal_unstable.reset() + } } diff --git a/site/src/routes/api/cache/+page.svx b/site/src/routes/api/cache/+page.svx index cbaa51a0b..79ccc407b 100644 --- a/site/src/routes/api/cache/+page.svx +++ b/site/src/routes/api/cache/+page.svx @@ -324,3 +324,14 @@ user.markStale('name') const user = cache.get('User', { id: '1' }) user.markStale('name' { when: { pattern: 'capitalize' } }) ``` + +## Resetting the Cache + +In some situations, it is necessary to reset the cache to a totally blank state. To do this, you +should use the `reset` method: + +```typescript +import { cache } from '$houdini' + +cache.reset() +```