Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(graphcache): track types in the data-structure (#3501)

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Jovi De Croock
Phil Pluckthun
and committed by
GitHub
492af087 969935c6

+103 -11
+5
.changeset/tall-buttons-fetch.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Track list of entity keys for a given type name. This enables enumerating and invalidating all entities of a given type within the normalized cache.
+19
docs/graphcache/cache-updates.md
··· 490 490 mutation by enumerating all `todos` listing fields using `cache.inspectFields` and targetedly 491 491 invalidate only these fields, which causes all queries using these listing fields to be refetched. 492 492 493 + ### Invalidating a type 494 + 495 + We can also invalidate all the entities of a given type, this could be handy in the case of a 496 + list update or when you aren't sure what entity is affected. 497 + 498 + This can be done by only passing the relevant `__typename` to the `invalidate` function. 499 + 500 + ```js 501 + cacheExchange({ 502 + updates: { 503 + Mutation: { 504 + deleteTodo(_result, args, cache, _info) { 505 + cache.invalidate('Todo'); 506 + }, 507 + }, 508 + }, 509 + }); 510 + ``` 511 + 493 512 ## Optimistic updates 494 513 495 514 If we know what result a mutation may return, why wait for the GraphQL API to fulfill our mutations?
+7
exchanges/graphcache/src/operations/invalidate.ts
··· 24 24 } 25 25 } 26 26 }; 27 + 28 + export const invalidateType = (typename: string) => { 29 + const types = InMemoryData.getEntitiesForType(typename); 30 + for (const entity of types) { 31 + invalidateEntity(entity); 32 + } 33 + };
+1
exchanges/graphcache/src/operations/write.ts
··· 232 232 return; 233 233 } else if (!isRoot && entityKey) { 234 234 InMemoryData.writeRecord(entityKey, '__typename', typename); 235 + InMemoryData.writeType(typename, entityKey); 235 236 } 236 237 237 238 const updates = ctx.store.updates[typename];
+19
exchanges/graphcache/src/store/data.test.ts
··· 16 16 InMemoryData.writeRecord('Todo:2', '__typename', 'Todo'); 17 17 InMemoryData.writeRecord('Query', '__typename', 'Query'); 18 18 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 19 + InMemoryData.writeType('Todo', 'Todo:1'); 19 20 20 21 InMemoryData.gc(); 21 22 22 23 expect(InMemoryData.readLink('Query', 'todo')).toBe('Todo:1'); 24 + expect(InMemoryData.getEntitiesForType('Todo')).toEqual( 25 + new Set(['Todo:1']) 26 + ); 23 27 24 28 InMemoryData.writeLink('Query', 'todo', undefined); 25 29 InMemoryData.gc(); 26 30 27 31 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); 28 32 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 33 + expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set()); 29 34 30 35 expect(InMemoryData.getCurrentDependencies()).toEqual( 31 36 new Set(['Todo:1', 'Todo:2', 'Query.todo']) ··· 39 44 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 40 45 InMemoryData.writeLink('Query', 'todo', undefined); 41 46 InMemoryData.writeLink('Query', 'newTodo', 'Todo:1'); 47 + InMemoryData.writeType('Todo', 'Todo:1'); 42 48 43 49 InMemoryData.gc(); 44 50 45 51 expect(InMemoryData.readLink('Query', 'newTodo')).toBe('Todo:1'); 46 52 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); 47 53 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 54 + expect(InMemoryData.getEntitiesForType('Todo')).toEqual( 55 + new Set(['Todo:1']) 56 + ); 48 57 49 58 expect(InMemoryData.getCurrentDependencies()).toEqual( 50 59 new Set(['Todo:1', 'Query.todo', 'Query.newTodo']) ··· 101 110 InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); 102 111 InMemoryData.writeRecord('Todo:1', 'id', '1'); 103 112 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 113 + InMemoryData.writeType('Todo', 'Todo:1'); 114 + InMemoryData.writeType('Author', 'Author:1'); 104 115 105 116 InMemoryData.writeLink('Query', 'todo', undefined); 117 + expect(InMemoryData.getEntitiesForType('Todo')).toEqual( 118 + new Set(['Todo:1']) 119 + ); 120 + expect(InMemoryData.getEntitiesForType('Author')).toEqual( 121 + new Set(['Author:1']) 122 + ); 106 123 InMemoryData.gc(); 107 124 108 125 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 109 126 expect(InMemoryData.readRecord('Author:1', 'id')).toBe(undefined); 127 + expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set()); 128 + expect(InMemoryData.getEntitiesForType('Author')).toEqual(new Set()); 110 129 111 130 expect(InMemoryData.getCurrentDependencies()).toEqual( 112 131 new Set(['Author:1', 'Todo:1', 'Query.todo'])
+26
exchanges/graphcache/src/store/data.ts
··· 47 47 records: NodeMap<EntityField>; 48 48 /** A map of entity links which are connections from one entity to another (key-value entries per entity) */ 49 49 links: NodeMap<Link>; 50 + /** A map of typename to a list of entity-keys belonging to said type */ 51 + types: Map<string, Set<string>>; 50 52 /** A set of Query operation keys that are in-flight and deferred/streamed */ 51 53 deferredKeys: Set<number>; 52 54 /** A set of Query operation keys that are in-flight and awaiting a result */ ··· 234 236 return currentDependencies; 235 237 }; 236 238 239 + const DEFAULT_EMPTY_SET = new Set<string>(); 237 240 export const make = (queryRootKey: string): InMemoryData => ({ 238 241 hydrating: false, 239 242 defer: false, 240 243 gc: new Set(), 244 + types: new Map(), 241 245 persist: new Set(), 242 246 queryRootKey, 243 247 refCount: new Map(), ··· 409 413 const rc = currentData!.refCount.get(entityKey) || 0; 410 414 if (rc > 0) continue; 411 415 416 + const record = currentData!.records.base.get(entityKey); 412 417 // Delete the reference count, and delete the entity from the GC batch 413 418 currentData!.refCount.delete(entityKey); 414 419 currentData!.records.base.delete(entityKey); 420 + 421 + const typename = (record && record.__typename) as string | undefined; 422 + if (typename) { 423 + const type = currentData!.types.get(typename); 424 + if (type) type.delete(entityKey); 425 + } 426 + 415 427 const linkNode = currentData!.links.base.get(entityKey); 416 428 if (linkNode) { 417 429 currentData!.links.base.delete(entityKey); ··· 450 462 ): Link | undefined => { 451 463 updateDependencies(entityKey, fieldKey); 452 464 return getNode(currentData!.links, entityKey, fieldKey); 465 + }; 466 + 467 + export const getEntitiesForType = (typename: string): Set<string> => 468 + currentData!.types.get(typename) || DEFAULT_EMPTY_SET; 469 + 470 + export const writeType = (typename: string, entityKey: string) => { 471 + const existingTypes = currentData!.types.get(typename); 472 + if (!existingTypes) { 473 + const typeSet = new Set<string>(); 474 + typeSet.add(entityKey); 475 + currentData!.types.set(typename, typeSet); 476 + } else { 477 + existingTypes.add(entityKey); 478 + } 453 479 }; 454 480 455 481 /** Writes an entity's field (a "record") to data */
+9
exchanges/graphcache/src/store/store.test.ts
··· 844 844 expect(data).toBe(null); 845 845 }); 846 846 }); 847 + 848 + describe('Invalidating a type', () => { 849 + it('removes an entity from a list.', () => { 850 + InMemoryData.initDataState('write', store.data, null); 851 + store.invalidate('Todo'); 852 + const { data } = query(store, { query: Todos }); 853 + expect(data).toBe(null); 854 + }); 855 + }); 847 856 }); 848 857 849 858 describe('Store with storage', () => {
+17 -11
exchanges/graphcache/src/store/store.ts
··· 24 24 import { contextRef, ensureLink } from '../operations/shared'; 25 25 import { _query, _queryFragment } from '../operations/query'; 26 26 import { _write, _writeFragment } from '../operations/write'; 27 - import { invalidateEntity } from '../operations/invalidate'; 27 + import { invalidateEntity, invalidateType } from '../operations/invalidate'; 28 28 import { keyOfField } from './keys'; 29 29 import * as InMemoryData from './data'; 30 30 ··· 170 170 171 171 invalidate(entity: Entity, field?: string, args?: FieldArgs) { 172 172 const entityKey = this.keyOfEntity(entity); 173 + const shouldInvalidateType = 174 + entity && typeof entity === 'string' && !field && !args; 173 175 174 - invariant( 175 - entityKey, 176 - "Can't generate a key for invalidate(...).\n" + 177 - 'You have to pass an id or _id field or create a custom `keys` field for `' + 178 - (typeof entity === 'object' 179 - ? (entity as Data).__typename 180 - : entity + '`.'), 181 - 19 182 - ); 176 + if (shouldInvalidateType) { 177 + invalidateType(entity); 178 + } else { 179 + invariant( 180 + entityKey, 181 + "Can't generate a key for invalidate(...).\n" + 182 + 'You have to pass an id or _id field or create a custom `keys` field for `' + 183 + (typeof entity === 'object' 184 + ? (entity as Data).__typename 185 + : entity + '`.'), 186 + 19 187 + ); 183 188 184 - invalidateEntity(entityKey, field, args); 189 + invalidateEntity(entityKey, field, args); 190 + } 185 191 } 186 192 187 193 inspectFields(entity: Entity): FieldInfo[] {