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.

(graphcache) - Implement RFC for optimistic behavior enhancements (#750)

- Convert dependencies data structure from `Set<string>` to `type Dependencies = Record<string, true>`
- Keep track of optimistically updated dependencies (of type `Dependencies`)
- Block refetching of operations that have active optimistic updates and keep track of them (see above)
- Allow `executePendingOperations` to trigger refetch for previously blocked `cache-and-network` operations
- Keep track of optimistic mutation that are pending at the same time, and flush their results simultaneously, also erasing the optimistically updated (i.e. blocked) dependencies at the same time

authored by

Phil Plückthun and committed by
GitHub
6871e9b9 e02392ab

+443 -122
+5
.changeset/little-chicken-ring.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Implement refetch blocking for queries that are affected by optimistic update. When a query would normally be refetched, either because it was partial or a cache-and-network operation, we now wait if it touched optimistic data for that optimistic mutation to complete. This prevents optimistic update data from unexpectedly disappearing
+5
.changeset/seven-onions-check.md
··· 1 + --- 2 + '@urql/exchange-graphcache': minor 3 + --- 4 + 5 + Implement optimistic mutation result flushing. Mutation results for mutation that have had optimistic updates will now wait for all optimistic mutations to complete at the same time before being applied to the cache. This sometimes does delay cache updates to until after multiple mutations have completed, but it does prevent optimistic data from being accidentally committed permanently, which is more intuitive.
+200 -2
exchanges/graphcache/src/cacheExchange.test.ts
··· 696 696 expect(result).toHaveBeenCalledTimes(4); 697 697 }); 698 698 699 + it('batches optimistic mutation result application', () => { 700 + jest.useFakeTimers(); 701 + 702 + const mutation = gql` 703 + mutation { 704 + concealAuthor { 705 + id 706 + name 707 + } 708 + } 709 + `; 710 + 711 + const optimisticMutationData = { 712 + __typename: 'Mutation', 713 + concealAuthor: { 714 + __typename: 'Author', 715 + id: '123', 716 + name: '[REDACTED OFFLINE]', 717 + }, 718 + }; 719 + 720 + const mutationData = { 721 + __typename: 'Mutation', 722 + concealAuthor: { 723 + __typename: 'Author', 724 + id: '123', 725 + name: '[REDACTED ONLINE]', 726 + }, 727 + }; 728 + 729 + const client = createClient({ url: 'http://0.0.0.0' }); 730 + const { source: ops$, next } = makeSubject<Operation>(); 731 + 732 + const reexec = jest 733 + .spyOn(client, 'reexecuteOperation') 734 + .mockImplementation(next); 735 + 736 + const opOne = client.createRequestOperation('query', { 737 + key: 1, 738 + query: queryOne, 739 + }); 740 + 741 + const opMutationOne = client.createRequestOperation('mutation', { 742 + key: 2, 743 + query: mutation, 744 + }); 745 + 746 + const opMutationTwo = client.createRequestOperation('mutation', { 747 + key: 3, 748 + query: mutation, 749 + }); 750 + 751 + const response = jest.fn( 752 + (forwardOp: Operation): OperationResult => { 753 + if (forwardOp.key === 1) { 754 + return { operation: opOne, data: queryOneData }; 755 + } else if (forwardOp.key === 2) { 756 + return { operation: opMutationOne, data: mutationData }; 757 + } else if (forwardOp.key === 3) { 758 + return { operation: opMutationTwo, data: mutationData }; 759 + } 760 + 761 + return undefined as any; 762 + } 763 + ); 764 + 765 + const result = jest.fn(); 766 + const forward: ExchangeIO = ops$ => pipe(ops$, delay(3), map(response)); 767 + 768 + const optimistic = { 769 + concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any, 770 + }; 771 + 772 + pipe( 773 + cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 774 + filter(x => x.operation.operationName === 'mutation'), 775 + tap(result), 776 + publish 777 + ); 778 + 779 + next(opOne); 780 + jest.runAllTimers(); 781 + expect(response).toHaveBeenCalledTimes(1); 782 + expect(result).toHaveBeenCalledTimes(0); 783 + 784 + next(opMutationOne); 785 + jest.advanceTimersByTime(1); 786 + next(opMutationTwo); 787 + 788 + expect(response).toHaveBeenCalledTimes(1); 789 + expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2); 790 + expect(reexec).toHaveBeenCalledTimes(2); 791 + expect(result).toHaveBeenCalledTimes(0); 792 + 793 + jest.advanceTimersByTime(2); 794 + expect(response).toHaveBeenCalledTimes(2); 795 + expect(result).toHaveBeenCalledTimes(0); 796 + 797 + jest.runAllTimers(); 798 + expect(response).toHaveBeenCalledTimes(3); 799 + expect(result).toHaveBeenCalledTimes(2); 800 + }); 801 + 802 + it('blocks refetches of overlapping queries', () => { 803 + jest.useFakeTimers(); 804 + 805 + const mutation = gql` 806 + mutation { 807 + concealAuthor { 808 + id 809 + name 810 + } 811 + } 812 + `; 813 + 814 + const optimisticMutationData = { 815 + __typename: 'Mutation', 816 + concealAuthor: { 817 + __typename: 'Author', 818 + id: '123', 819 + name: '[REDACTED OFFLINE]', 820 + }, 821 + }; 822 + 823 + const client = createClient({ url: 'http://0.0.0.0' }); 824 + const { source: ops$, next } = makeSubject<Operation>(); 825 + 826 + const reexec = jest 827 + .spyOn(client, 'reexecuteOperation') 828 + .mockImplementation(next); 829 + 830 + const opOne = client.createRequestOperation( 831 + 'query', 832 + { 833 + key: 1, 834 + query: queryOne, 835 + }, 836 + { 837 + requestPolicy: 'cache-and-network', 838 + } 839 + ); 840 + 841 + const opMutation = client.createRequestOperation('mutation', { 842 + key: 2, 843 + query: mutation, 844 + }); 845 + 846 + const response = jest.fn( 847 + (forwardOp: Operation): OperationResult => { 848 + if (forwardOp.key === 1) { 849 + return { operation: opOne, data: queryOneData }; 850 + } 851 + 852 + return undefined as any; 853 + } 854 + ); 855 + 856 + const result = jest.fn(); 857 + const forward: ExchangeIO = ops$ => 858 + pipe( 859 + ops$, 860 + delay(1), 861 + filter(x => x.operationName !== 'mutation'), 862 + map(response) 863 + ); 864 + 865 + const optimistic = { 866 + concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any, 867 + }; 868 + 869 + pipe( 870 + cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), 871 + tap(result), 872 + publish 873 + ); 874 + 875 + next(opOne); 876 + jest.runAllTimers(); 877 + expect(response).toHaveBeenCalledTimes(1); 878 + 879 + next(opMutation); 880 + expect(response).toHaveBeenCalledTimes(1); 881 + expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); 882 + expect(reexec).toHaveBeenCalledTimes(1); 883 + 884 + expect(reexec.mock.calls[0][0]).toHaveProperty( 885 + 'context.requestPolicy', 886 + 'cache-first' 887 + ); 888 + 889 + jest.runAllTimers(); 890 + expect(response).toHaveBeenCalledTimes(1); 891 + 892 + next(opOne); 893 + expect(response).toHaveBeenCalledTimes(1); 894 + expect(reexec).toHaveBeenCalledTimes(1); 895 + }); 896 + 699 897 it('correctly clears on error', () => { 700 898 jest.useFakeTimers(); 701 899 ··· 1529 1727 }, 1530 1728 }); 1531 1729 1532 - expect(reexec).toHaveBeenCalledTimes(1); 1730 + expect(reexec).toHaveBeenCalledTimes(3); 1533 1731 expect(data).toHaveProperty('node.name', 'mutation'); 1534 1732 1535 1733 nextRes({ ··· 1544 1742 }, 1545 1743 }); 1546 1744 1547 - expect(reexec).toHaveBeenCalledTimes(2); 1745 + expect(reexec).toHaveBeenCalledTimes(4); 1548 1746 expect(data).toHaveProperty('node.name', 'mutation'); 1549 1747 }); 1550 1748
+122 -40
exchanges/graphcache/src/cacheExchange.ts
··· 26 26 } from 'wonka'; 27 27 28 28 import { query, write, writeOptimistic } from './operations'; 29 - import { hydrateData } from './store/data'; 30 - import { makeDict } from './helpers/dict'; 29 + import { makeDict, isDictEmpty } from './helpers/dict'; 31 30 import { filterVariables, getMainOperation } from './ast'; 32 - import { Store, noopDataState, reserveLayer } from './store'; 31 + import { Store, noopDataState, hydrateData, reserveLayer } from './store'; 33 32 34 33 import { 35 34 UpdatesConfig, ··· 37 36 OptimisticMutationConfig, 38 37 KeyingConfig, 39 38 StorageAdapter, 39 + Dependencies, 40 40 } from './types'; 41 41 42 42 type OperationResultWithMeta = OperationResult & { 43 43 outcome: CacheOutcome; 44 + dependencies: Dependencies; 44 45 }; 45 46 47 + type Operations = Set<number>; 46 48 type OperationMap = Map<number, Operation>; 47 - 48 - interface DependentOperations { 49 - [key: string]: number[]; 50 - } 49 + type OptimisticDependencies = Map<number, Dependencies>; 50 + type DependentOperations = Record<string, number[]>; 51 51 52 52 // Returns the given operation result with added cacheOutcome meta field 53 53 const addCacheOutcome = (op: Operation, outcome: CacheOutcome): Operation => ({ ··· 96 96 }); 97 97 } 98 98 99 - const optimisticKeysToDependencies = new Map<number, Set<string>>(); 99 + const optimisticKeysToDependencies: OptimisticDependencies = new Map(); 100 + const mutationResultBuffer: OperationResult[] = []; 100 101 const ops: OperationMap = new Map(); 102 + const blockedDependencies: Dependencies = makeDict(); 103 + const requestedRefetch: Operations = new Set(); 101 104 const deps: DependentOperations = makeDict(); 102 105 106 + const isBlockedByOptimisticUpdate = (dependencies: Dependencies): boolean => { 107 + for (const dep in dependencies) if (blockedDependencies[dep]) return true; 108 + return false; 109 + }; 110 + 103 111 const collectPendingOperations = ( 104 - pendingOperations: Set<number>, 105 - dependencies: void | Set<string> 112 + pendingOperations: Operations, 113 + dependencies: void | Dependencies 106 114 ) => { 107 115 if (dependencies) { 108 116 // Collect operations that will be updated due to cache changes 109 - dependencies.forEach(dep => { 117 + for (const dep in dependencies) { 110 118 const keys = deps[dep]; 111 119 if (keys) { 112 120 deps[dep] = []; ··· 114 122 pendingOperations.add(keys[i]); 115 123 } 116 124 } 117 - }); 125 + } 118 126 } 119 127 }; 120 128 121 129 const executePendingOperations = ( 122 130 operation: Operation, 123 - pendingOperations: Set<number> 131 + pendingOperations: Operations 124 132 ) => { 125 133 // Reexecute collected operations and delete them from the mapping 126 134 pendingOperations.forEach(key => { ··· 128 136 const op = ops.get(key); 129 137 if (op) { 130 138 ops.delete(key); 131 - client.reexecuteOperation(toRequestPolicy(op, 'cache-first')); 139 + let policy: RequestPolicy = 'cache-first'; 140 + if (requestedRefetch.has(key)) { 141 + requestedRefetch.delete(key); 142 + policy = 'cache-and-network'; 143 + } 144 + client.reexecuteOperation(toRequestPolicy(op, policy)); 132 145 } 133 146 } 134 147 }); ··· 137 150 // This registers queries with the data layer to ensure commutativity 138 151 const prepareForwardedOperation = (operation: Operation) => { 139 152 if (operation.operationName === 'query') { 153 + // Pre-reserve the position of the result layer 140 154 reserveLayer(store.data, operation.key); 141 155 } else if (operation.operationName === 'teardown') { 156 + // Delete reference to operation if any exists to release it 157 + ops.delete(operation.key); 158 + // Mark operation layer as done 142 159 noopDataState(store.data, operation.key); 143 160 } else if ( 144 161 operation.operationName === 'mutation' && ··· 146 163 ) { 147 164 // This executes an optimistic update for mutations and registers it if necessary 148 165 const { dependencies } = writeOptimistic(store, operation, operation.key); 149 - if (dependencies.size !== 0) { 166 + if (!isDictEmpty(dependencies)) { 167 + // Update blocked optimistic dependencies 168 + for (const dep in dependencies) { 169 + blockedDependencies[dep] = true; 170 + } 171 + 172 + // Store optimistic dependencies for update 150 173 optimisticKeysToDependencies.set(operation.key, dependencies); 151 - const pendingOperations = new Set<number>(); 174 + 175 + // Update related queries 176 + const pendingOperations: Operations = new Set(); 152 177 collectPendingOperations(pendingOperations, dependencies); 153 178 executePendingOperations(operation, pendingOperations); 154 179 } ··· 167 192 }; 168 193 169 194 // This updates the known dependencies for the passed operation 170 - const updateDependencies = (op: Operation, dependencies: Set<string>) => { 171 - dependencies.forEach(dep => { 195 + const updateDependencies = (op: Operation, dependencies: Dependencies) => { 196 + for (const dep in dependencies) { 172 197 (deps[dep] || (deps[dep] = [])).push(op.key); 173 198 ops.set(op.key, op); 174 - }); 199 + } 175 200 }; 176 201 177 202 // Retrieves a query result from cache and adds an `isComplete` hint ··· 186 211 : 'partial' 187 212 : 'miss'; 188 213 189 - if (res.data) { 190 - updateDependencies(operation, res.dependencies); 191 - } 214 + updateDependencies(operation, res.dependencies); 192 215 193 216 return { 194 217 outcome: cacheOutcome, 195 218 operation, 196 219 data: res.data, 220 + dependencies: res.dependencies, 197 221 }; 198 222 }; 199 223 200 224 // Take any OperationResult and update the cache with it 201 - const updateCacheWithResult = (result: OperationResult): OperationResult => { 225 + const updateCacheWithResult = ( 226 + result: OperationResult, 227 + pendingOperations: Operations 228 + ): OperationResult => { 202 229 const { operation, error, extensions } = result; 203 - 204 - // Clear old optimistic values from the store 205 230 const { key } = operation; 206 - const pendingOperations = new Set<number>(); 207 231 208 232 if (operation.operationName === 'mutation') { 209 233 // Collect previous dependencies that have been written for optimistic updates 210 - collectPendingOperations( 211 - pendingOperations, 212 - optimisticKeysToDependencies.get(key) 213 - ); 234 + const dependencies = optimisticKeysToDependencies.get(key); 235 + collectPendingOperations(pendingOperations, dependencies); 214 236 optimisticKeysToDependencies.delete(key); 215 237 } else { 216 238 reserveLayer(store.data, operation.key); 217 239 } 218 240 219 - let queryDependencies: Set<string> | void; 241 + let queryDependencies: void | Dependencies; 220 242 if (result.data) { 221 243 // Write the result to cache and collect all dependencies that need to be 222 244 // updated ··· 235 257 noopDataState(store.data, operation.key); 236 258 } 237 259 238 - // Execute all pending operations related to changed dependencies 239 - executePendingOperations(result.operation, pendingOperations); 240 260 // Update this operation's dependencies if it's a query 241 261 if (queryDependencies) { 242 262 updateDependencies(result.operation, queryDependencies); ··· 290 310 filter(res => { 291 311 return ( 292 312 res.outcome === 'miss' && 293 - res.operation.context.requestPolicy !== 'cache-only' 313 + res.operation.context.requestPolicy !== 'cache-only' && 314 + !isBlockedByOptimisticUpdate(res.dependencies) 294 315 ); 295 316 }), 296 317 map(res => { ··· 314 335 ), 315 336 map( 316 337 (res: OperationResultWithMeta): OperationResult => { 317 - const { operation, outcome } = res; 338 + const { operation, outcome, dependencies } = res; 318 339 const result: OperationResult = { 319 340 operation: addCacheOutcome(operation, outcome), 320 341 data: res.data, ··· 328 349 outcome === 'partial') 329 350 ) { 330 351 result.stale = true; 331 - client.reexecuteOperation( 332 - toRequestPolicy(operation, 'network-only') 333 - ); 352 + if (!isBlockedByOptimisticUpdate(dependencies)) { 353 + client.reexecuteOperation( 354 + toRequestPolicy(operation, 'network-only') 355 + ); 356 + } else if ( 357 + operation.context.requestPolicy === 'cache-and-network' 358 + ) { 359 + requestedRefetch.add(operation.key); 360 + } 334 361 } 335 362 336 363 dispatchDebug({ ··· 353 380 merge([nonCacheOps$, cacheMissOps$]), 354 381 map(prepareForwardedOperation), 355 382 forward, 356 - map(updateCacheWithResult) 383 + share 384 + ); 385 + 386 + // Results that can immediately be resolved 387 + const nonOptimisticResults$ = pipe( 388 + result$, 389 + filter(result => !optimisticKeysToDependencies.has(result.operation.key)), 390 + map(result => { 391 + const pendingOperations: Operations = new Set(); 392 + // Update the cache with the incoming API result 393 + const cacheResult = updateCacheWithResult(result, pendingOperations); 394 + // Execute all dependent queries 395 + executePendingOperations(result.operation, pendingOperations); 396 + return cacheResult; 397 + }) 398 + ); 399 + 400 + // Prevent mutations that were previously optimistic from being flushed 401 + // immediately and instead clear them out slowly 402 + const optimisticMutationCompletion$ = pipe( 403 + result$, 404 + filter(result => optimisticKeysToDependencies.has(result.operation.key)), 405 + mergeMap( 406 + (result: OperationResult): Source<OperationResult> => { 407 + const length = mutationResultBuffer.push(result); 408 + if (length < optimisticKeysToDependencies.size) { 409 + return empty; 410 + } 411 + 412 + for (let i = 0; i < mutationResultBuffer.length; i++) { 413 + reserveLayer(store.data, mutationResultBuffer[i].operation.key); 414 + } 415 + 416 + for (const dep in blockedDependencies) { 417 + delete blockedDependencies[dep]; 418 + } 419 + 420 + const results: OperationResult[] = []; 421 + const pendingOperations: Operations = new Set(); 422 + 423 + let bufferedResult: OperationResult | void; 424 + while ((bufferedResult = mutationResultBuffer.shift())) 425 + results.push( 426 + updateCacheWithResult(bufferedResult, pendingOperations) 427 + ); 428 + 429 + // Execute all dependent queries as a single batch 430 + executePendingOperations(result.operation, pendingOperations); 431 + 432 + return fromArray(results); 433 + } 434 + ) 357 435 ); 358 436 359 - return merge([result$, cacheResult$]); 437 + return merge([ 438 + nonOptimisticResults$, 439 + optimisticMutationCompletion$, 440 + cacheResult$, 441 + ]); 360 442 }; 361 443 };
+5
exchanges/graphcache/src/helpers/dict.ts
··· 1 1 export const makeDict = (): any => Object.create(null); 2 + 3 + export const isDictEmpty = (x: any) => { 4 + for (const _ in x) return false; 5 + return true; 6 + };
+2 -2
exchanges/graphcache/src/operations/invalidate.ts
··· 31 31 read(store, request); 32 32 InMemoryData.unforkDependencies(); 33 33 34 - dependencies.forEach(dependency => { 34 + for (const dependency in dependencies) { 35 35 if (dependency.startsWith(`${store.data.queryRootKey}.`)) { 36 36 const fieldKey = dependency.slice(`${store.data.queryRootKey}.`.length); 37 37 InMemoryData.writeLink(store.data.queryRootKey, fieldKey); ··· 39 39 } else { 40 40 invalidateEntity(dependency); 41 41 } 42 - }); 42 + } 43 43 44 44 InMemoryData.gc(); 45 45 };
+2 -1
exchanges/graphcache/src/operations/query.ts
··· 22 22 Link, 23 23 OperationRequest, 24 24 NullArray, 25 + Dependencies, 25 26 } from '../types'; 26 27 27 28 import { ··· 51 52 } from '../ast'; 52 53 53 54 export interface QueryResult { 54 - dependencies: Set<string>; 55 + dependencies: Dependencies; 55 56 partial: boolean; 56 57 data: null | Data; 57 58 }
+9 -2
exchanges/graphcache/src/operations/write.ts
··· 15 15 16 16 import { invariant, warn, pushDebugNode } from '../helpers/help'; 17 17 18 - import { NullArray, Variables, Data, Link, OperationRequest } from '../types'; 18 + import { 19 + NullArray, 20 + Variables, 21 + Data, 22 + Link, 23 + OperationRequest, 24 + Dependencies, 25 + } from '../types'; 19 26 20 27 import { 21 28 Store, ··· 37 44 38 45 export interface WriteResult { 39 46 data: null | Data; 40 - dependencies: Set<string>; 47 + dependencies: Dependencies; 41 48 } 42 49 43 50 /** Writes a request given its response to the store */
+31 -31
exchanges/graphcache/src/store/data.test.ts
··· 25 25 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); 26 26 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 27 27 28 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 29 - 'Todo:1', 30 - 'Query.todo', 31 - ]); 28 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 29 + 'Todo:1': true, 30 + 'Query.todo': true, 31 + }); 32 32 }); 33 33 34 34 it('keeps readopted entities', () => { ··· 45 45 expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); 46 46 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 47 47 48 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 49 - 'Todo:1', 50 - 'Query.todo', 51 - 'Query.newTodo', 52 - ]); 48 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 49 + 'Todo:1': true, 50 + 'Query.todo': true, 51 + 'Query.newTodo': true, 52 + }); 53 53 }); 54 54 55 55 it('keeps entities with multiple owners', () => { ··· 66 66 expect(InMemoryData.readLink('Query', 'todoB')).toBe('Todo:1'); 67 67 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 68 68 69 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 70 - 'Todo:1', 71 - 'Query.todoA', 72 - 'Query.todoB', 73 - ]); 69 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 70 + 'Todo:1': true, 71 + 'Query.todoA': true, 72 + 'Query.todoB': true, 73 + }); 74 74 }); 75 75 76 76 it('skips entities with optimistic updates', () => { ··· 87 87 88 88 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 89 89 90 - InMemoryData.clearLayer(data, 1); 90 + InMemoryData.reserveLayer(data, 1); 91 91 InMemoryData.gc(); 92 - expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 93 92 94 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 95 - 'Query.todo', 96 - 'Todo:1', 97 - ]); 93 + expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 94 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 95 + 'Query.todo': true, 96 + 'Todo:1': true, 97 + }); 98 98 }); 99 99 100 100 it('erases child entities that are orphaned', () => { ··· 111 111 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 112 112 expect(InMemoryData.readRecord('Author:1', 'id')).toBe(undefined); 113 113 114 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 115 - 'Author:1', 116 - 'Todo:1', 117 - 'Query.todo', 118 - ]); 114 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 115 + 'Author:1': true, 116 + 'Todo:1': true, 117 + 'Query.todo': true, 118 + }); 119 119 }); 120 120 }); 121 121 ··· 156 156 ] 157 157 `); 158 158 159 - expect([...InMemoryData.getCurrentDependencies()]).toEqual([ 160 - 'Query.todo({"id":"1"})', 161 - 'Query.hasTodo({"id":"1"})', 162 - 'Query.randomTodo', 163 - ]); 159 + expect(InMemoryData.getCurrentDependencies()).toEqual({ 160 + 'Query.todo({"id":"1"})': true, 161 + 'Query.hasTodo({"id":"1"})': true, 162 + 'Query.randomTodo': true, 163 + }); 164 164 }); 165 165 166 166 it('returns an empty array when an entity is unknown', () => { 167 167 expect(InMemoryData.inspectFields('Random')).toEqual([]); 168 168 169 - expect([...InMemoryData.getCurrentDependencies()]).toEqual(['Random']); 169 + expect(InMemoryData.getCurrentDependencies()).toEqual({ Random: true }); 170 170 }); 171 171 172 172 it('returns field infos for all optimistic updates', () => {
+10 -9
exchanges/graphcache/src/store/data.ts
··· 6 6 FieldInfo, 7 7 StorageAdapter, 8 8 SerializedEntries, 9 + Dependencies, 9 10 } from '../types'; 10 11 11 12 import { ··· 54 55 } 55 56 56 57 let currentData: null | InMemoryData = null; 57 - let currentDependencies: null | Set<string> = null; 58 - let previousDependencies: null | Set<string> = null; 58 + let currentDependencies: null | Dependencies = null; 59 + let previousDependencies: null | Dependencies = null; 59 60 let currentOptimisticKey: null | number = null; 60 61 let currentIgnoreOptimistic = false; 61 62 ··· 72 73 ) => { 73 74 currentData = data; 74 75 previousDependencies = currentDependencies; 75 - currentDependencies = new Set(); 76 + currentDependencies = makeDict(); 76 77 currentIgnoreOptimistic = false; 77 78 if (process.env.NODE_ENV !== 'production') { 78 79 currentDebugStack.length = 0; ··· 160 161 }; 161 162 162 163 /** As we're writing, we keep around all the records and links we've read or have written to */ 163 - export const getCurrentDependencies = (): Set<string> => { 164 + export const getCurrentDependencies = (): Dependencies => { 164 165 invariant( 165 166 currentDependencies !== null, 166 167 'Invalid Cache call: The cache may only be accessed or mutated during' + ··· 172 173 return currentDependencies; 173 174 }; 174 175 175 - export const forkDependencies = (): Set<string> => { 176 + export const forkDependencies = (): Dependencies => { 176 177 previousDependencies = currentDependencies; 177 - return (currentDependencies = new Set()); 178 + return (currentDependencies = makeDict()); 178 179 }; 179 180 180 181 export const unforkDependencies = () => { ··· 369 370 const updateDependencies = (entityKey: string, fieldKey?: string) => { 370 371 if (fieldKey !== '__typename') { 371 372 if (entityKey !== currentData!.queryRootKey) { 372 - currentDependencies!.add(entityKey); 373 + currentDependencies![entityKey] = true; 373 374 } else if (fieldKey !== undefined) { 374 - currentDependencies!.add(joinKeys(entityKey, fieldKey)); 375 + currentDependencies![joinKeys(entityKey, fieldKey)] = true; 375 376 } 376 377 } 377 378 }; ··· 489 490 }; 490 491 491 492 /** Clears all links and records of an optimistic layer */ 492 - export const clearLayer = (data: InMemoryData, layerKey: number) => { 493 + const clearLayer = (data: InMemoryData, layerKey: number) => { 493 494 if (data.refLock[layerKey]) { 494 495 delete data.refLock[layerKey]; 495 496 delete data.records.optimistic[layerKey];
+1 -1
exchanges/graphcache/src/store/index.ts
··· 3 3 clearDataState, 4 4 noopDataState, 5 5 reserveLayer, 6 - clearLayer, 7 6 getCurrentDependencies, 7 + hydrateData, 8 8 } from './data'; 9 9 10 10 export * from './keys';
+16 -17
exchanges/graphcache/src/store/store.test.ts
··· 140 140 expect(result).toEqual('Go to the shops'); 141 141 // TODO: we have no way of asserting this to really be the case. 142 142 const deps = InMemoryData.getCurrentDependencies(); 143 - expect(deps).toEqual(new Set(['Todo:0', 'Author:0'])); 143 + expect(deps).toEqual({ 'Todo:0': true, 'Author:0': true }); 144 144 InMemoryData.clearDataState(); 145 145 }); 146 146 ··· 148 148 const authorResult = store.resolve('Author:0', 'name'); 149 149 expect(authorResult).toBe('Jovi'); 150 150 const deps = InMemoryData.getCurrentDependencies(); 151 - expect(deps).toEqual(new Set(['Author:0'])); 151 + expect(deps).toEqual({ 'Author:0': true }); 152 152 InMemoryData.clearDataState(); 153 153 }); 154 154 ··· 162 162 const result = store.resolve(parent, 'author'); 163 163 expect(result).toEqual('Author:0'); 164 164 const deps = InMemoryData.getCurrentDependencies(); 165 - expect(deps).toEqual(new Set(['Todo:0'])); 165 + expect(deps).toEqual({ 'Todo:0': true }); 166 166 InMemoryData.clearDataState(); 167 167 }); 168 168 ··· 276 276 ); 277 277 278 278 const deps = InMemoryData.getCurrentDependencies(); 279 - expect(deps).toEqual(new Set(['Todo:0'])); 279 + expect(deps).toEqual({ 'Todo:0': true }); 280 280 281 281 const { data } = query(store, { query: Todos }); 282 282 ··· 308 308 ); 309 309 310 310 const deps = InMemoryData.getCurrentDependencies(); 311 - expect(deps).toEqual(new Set(['Todo:0'])); 311 + expect(deps).toEqual({ 'Todo:0': true }); 312 312 313 313 expect(result).toEqual({ 314 314 id: '0', ··· 410 410 const result = store.readQuery({ query: Todos }); 411 411 412 412 const deps = InMemoryData.getCurrentDependencies(); 413 - expect(deps).toEqual( 414 - new Set([ 415 - 'Query.todos', 416 - 'Todo:0', 417 - 'Todo:1', 418 - 'Todo:2', 419 - 'Author:0', 420 - 'Author:1', 421 - ]) 422 - ); 413 + expect(deps).toEqual({ 414 + 'Query.todos': true, 415 + 'Todo:0': true, 416 + 'Todo:1': true, 417 + 'Todo:2': true, 418 + 'Author:0': true, 419 + 'Author:1': true, 420 + }); 423 421 424 422 expect(result).toEqual({ 425 423 __typename: 'Query', ··· 450 448 }, 451 449 1 452 450 ); 453 - expect(dependencies).toEqual(new Set(['Todo:1'])); 451 + expect(dependencies).toEqual({ 'Todo:1': true }); 454 452 let { data } = query(store, { query: Todos }); 455 453 expect(data).toEqual({ 456 454 __typename: 'Query', ··· 471 469 ], 472 470 }); 473 471 474 - InMemoryData.clearLayer(store.data, 1); 472 + InMemoryData.noopDataState(store.data, 1); 473 + 475 474 ({ data } = query(store, { query: Todos })); 476 475 expect(data).toEqual({ 477 476 __typename: 'Query',
+16 -8
exchanges/graphcache/src/test-utils/examples-1.test.ts
··· 67 67 68 68 const writeRes = write(store, { query: Todos }, todosData); 69 69 70 - const expectedSet = new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']); 71 - expect(writeRes.dependencies).toEqual(expectedSet); 70 + expect(writeRes.dependencies).toEqual({ 71 + 'Query.todos': true, 72 + 'Todo:0': true, 73 + 'Todo:1': true, 74 + 'Todo:2': true, 75 + }); 72 76 73 77 let queryRes = query(store, { query: Todos }); 74 78 ··· 90 94 } 91 95 ); 92 96 93 - expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); 97 + expect(mutationRes.dependencies).toEqual({ 'Todo:2': true }); 94 98 95 99 queryRes = query(store, { query: Todos }); 96 100 ··· 117 121 } 118 122 ); 119 123 120 - expect(newMutationRes.dependencies).toEqual(new Set(['Todo:2'])); 124 + expect(newMutationRes.dependencies).toEqual({ 'Todo:2': true }); 121 125 122 126 queryRes = query(store, { query: Todos }); 123 127 ··· 212 216 213 217 const writeRes = write(store, { query: Todos }, todosData); 214 218 215 - const expectedSet = new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']); 216 - expect(writeRes.dependencies).toEqual(expectedSet); 219 + expect(writeRes.dependencies).toEqual({ 220 + 'Query.todos': true, 221 + 'Todo:0': true, 222 + 'Todo:1': true, 223 + 'Todo:2': true, 224 + }); 217 225 218 226 let queryRes = query(store, { query: Todos }); 219 227 ··· 242 250 } 243 251 ); 244 252 245 - expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); 253 + expect(mutationRes.dependencies).toEqual({ 'Todo:2': true }); 246 254 247 255 queryRes = query(store, { query: Todos }); 248 256 ··· 425 433 426 434 write(store, { query: getRoot }, queryData); 427 435 writeOptimistic(store, { query: updateItem, variables: { id: '2' } }, 1); 428 - InMemoryData.clearLayer(store.data, 1); 436 + InMemoryData.noopDataState(store.data, 1); 429 437 const queryRes = query(store, { query: getRoot }); 430 438 431 439 expect(queryRes.partial).toBe(false);
+1 -1
exchanges/graphcache/src/test-utils/examples-2.test.ts
··· 222 222 223 223 const res = query(store, { query: ItemDetailed }); 224 224 expect(res.partial).toBe(false); 225 - expect(Array.from(res.dependencies).includes('Author:x')).toBe(true); 225 + expect(res.dependencies).toHaveProperty('Author:x', true); 226 226 expect(res.data).toEqual({ 227 227 __typename: 'Query', 228 228 todo: {
+2
exchanges/graphcache/src/types.ts
··· 181 181 read(): Promise<SerializedEntries>; 182 182 write(data: SerializedEntries): Promise<void>; 183 183 } 184 + 185 + export type Dependencies = Record<string, true>;
+1 -1
package.json
··· 71 71 "glob": "^7.1.6", 72 72 "graphql": "^15.0.0", 73 73 "graphql-tag": "^2.10.1", 74 - "husky": "^4.2.3", 74 + "husky": "^4.2.5", 75 75 "invariant": "^2.2.4", 76 76 "jest": "^25.3.0", 77 77 "jest-watch-yarn-workspaces": "^1.1.0",
+15 -7
yarn.lock
··· 3743 3743 ansi-styles "^4.1.0" 3744 3744 supports-color "^7.1.0" 3745 3745 3746 + chalk@^4.0.0: 3747 + version "4.0.0" 3748 + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" 3749 + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== 3750 + dependencies: 3751 + ansi-styles "^4.1.0" 3752 + supports-color "^7.1.0" 3753 + 3746 3754 character-entities-html4@^1.0.0: 3747 3755 version "1.1.4" 3748 3756 resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" ··· 4110 4118 resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" 4111 4119 integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= 4112 4120 4113 - compare-versions@^3.5.1: 4121 + compare-versions@^3.6.0: 4114 4122 version "3.6.0" 4115 4123 resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" 4116 4124 integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== ··· 7192 7200 resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" 7193 7201 integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== 7194 7202 7195 - husky@^4.2.3: 7196 - version "4.2.3" 7197 - resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e" 7198 - integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ== 7203 + husky@^4.2.5: 7204 + version "4.2.5" 7205 + resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36" 7206 + integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ== 7199 7207 dependencies: 7200 - chalk "^3.0.0" 7208 + chalk "^4.0.0" 7201 7209 ci-info "^2.0.0" 7202 - compare-versions "^3.5.1" 7210 + compare-versions "^3.6.0" 7203 7211 cosmiconfig "^6.0.0" 7204 7212 find-versions "^3.2.0" 7205 7213 opencollective-postinstall "^2.0.2"