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 Commutative Query Results (#565)

* Move optimistic layer creation to initDataState

* Move optimistic keys to main data structure

* Implement squashLayer and rename clearLayer

* Add commutative ordering handling to initDataState

* Add reserveLayer call for commutativity to cacheExchange

* Clean up squashData implementation

* Fix console.log not showing in tests

* Move squashing to clearDataState and fix squashing order

* Add key to write in cacheExchange and prevent clearing on queries

* Add initial ordering test

* Fix require path in benchmark

* Fix stopping condition for commutative layers in clearDataState

* Add additional commutativity tests to data.test.ts

* Fix noisy console.warn on double-spy

* Remove optimistic layer for commutative layers that aren't needed

When only one commutative layer exists then clean up all optimistic layers automatically

* Add changeset

* Add some additional comments

* Move layer creation from reserveLayer to initDataState

* Fix partially completed commutative chains

Previously when the lowest operation completed,
the entire commutative chain would be squashed
and deleted. Instead only the completed operations
until the next lowest uncompleted operation should
be squashed.

authored by

Phil Plückthun and committed by
GitHub
3f83c4e4 294765ef

+398 -93
+5
.changeset/modern-olives-brake.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Apply Query results in-order and commutatively even when results arrive out-of-order
+1 -1
exchanges/graphcache/benchmark/suite.js
··· 1 1 const gql = require('graphql-tag'); 2 2 const { InMemoryCache } = require('@apollo/client'); 3 - const { Store, write, query } = require('../dist/urql-exchange-graphcache.min'); 3 + const { Store, write, query } = require('../dist/urql-exchange-graphcache.cjs.min'); 4 4 5 5 const countries = ['UK', 'BE', 'ES', 'US']; 6 6
+68 -1
exchanges/graphcache/src/cacheExchange.test.ts
··· 5 5 Operation, 6 6 OperationResult, 7 7 } from '@urql/core'; 8 - import { pipe, map, makeSubject, tap, publish, delay } from 'wonka'; 8 + import { 9 + Source, 10 + pipe, 11 + map, 12 + mergeMap, 13 + fromValue, 14 + makeSubject, 15 + tap, 16 + publish, 17 + delay, 18 + } from 'wonka'; 9 19 import { cacheExchange } from './cacheExchange'; 10 20 11 21 const queryOne = gql` ··· 841 851 'partial' 842 852 ); 843 853 }); 854 + 855 + it('applies results that come in out-of-order commutatively and consistently', () => { 856 + jest.useFakeTimers(); 857 + 858 + let data: any; 859 + 860 + const client = createClient({ 861 + url: 'http://0.0.0.0', 862 + requestPolicy: 'cache-and-network', 863 + }); 864 + const { source: ops$, next: next } = makeSubject<Operation>(); 865 + const query = gql` 866 + { 867 + index 868 + } 869 + `; 870 + 871 + const result = (operation: Operation): Source<OperationResult> => 872 + pipe( 873 + fromValue({ 874 + operation, 875 + data: { 876 + __typename: 'Query', 877 + index: operation.key, 878 + }, 879 + }), 880 + delay(operation.key === 2 ? 5 : operation.key * 10) 881 + ); 882 + 883 + const output = jest.fn(result => { 884 + data = result.data; 885 + }); 886 + 887 + pipe( 888 + cacheExchange()({ forward: ops$ => pipe(ops$, mergeMap(result)), client })( 889 + ops$ 890 + ), 891 + tap(output), 892 + publish 893 + ); 894 + 895 + next(client.createRequestOperation('query', { key: 1, query })); 896 + next(client.createRequestOperation('query', { key: 2, query })); 897 + next(client.createRequestOperation('query', { key: 3, query })); 898 + 899 + jest.advanceTimersByTime(5); 900 + expect(output).toHaveBeenCalledTimes(1); 901 + expect(data.index).toBe(2); 902 + 903 + jest.advanceTimersByTime(10); 904 + expect(output).toHaveBeenCalledTimes(2); 905 + expect(data.index).toBe(2); 906 + 907 + jest.advanceTimersByTime(30); 908 + expect(output).toHaveBeenCalledTimes(3); 909 + expect(data.index).toBe(3); 910 + });
+21 -8
exchanges/graphcache/src/cacheExchange.ts
··· 29 29 import { query, write, writeOptimistic } from './operations'; 30 30 import { hydrateData } from './store/data'; 31 31 import { makeDict } from './helpers/dict'; 32 - import { Store, clearOptimistic } from './store'; 32 + import { Store, clearLayer, reserveLayer } from './store'; 33 33 34 34 import { 35 35 UpdatesConfig, ··· 169 169 }); 170 170 }; 171 171 172 + // This registers queries with the data layer to ensure commutativity 173 + const prepareCacheForResult = (operation: Operation) => { 174 + if (operation.operationName === 'query') { 175 + reserveLayer(store.data, operation.key); 176 + } else if (operation.operationName === 'teardown') { 177 + clearLayer(store.data, operation.key); 178 + } 179 + }; 180 + 172 181 // This executes an optimistic update for mutations and registers it if necessary 173 182 const optimisticUpdate = (operation: Operation) => { 174 183 if (isOptimisticMutation(operation)) { ··· 234 243 // Clear old optimistic values from the store 235 244 const { key } = operation; 236 245 const pendingOperations = new Set<number>(); 237 - collectPendingOperations( 238 - pendingOperations, 239 - optimisticKeysToDependencies.get(key) 240 - ); 241 - optimisticKeysToDependencies.delete(key); 242 - clearOptimistic(store.data, key); 246 + 247 + if (!isQuery) { 248 + collectPendingOperations( 249 + pendingOperations, 250 + optimisticKeysToDependencies.get(key) 251 + ); 252 + optimisticKeysToDependencies.delete(key); 253 + clearLayer(store.data, key); 254 + } 243 255 244 256 let writeDependencies: Set<string> | void; 245 257 let queryDependencies: Set<string> | void; 246 258 if (data !== null && data !== undefined) { 247 - writeDependencies = write(store, operation, data).dependencies; 259 + writeDependencies = write(store, operation, data, key).dependencies; 248 260 249 261 if (isQuery) { 250 262 const queryResult = query(store, operation); ··· 350 362 cacheOps$, 351 363 ]), 352 364 filter(op => op.context.requestPolicy !== 'cache-only'), 365 + tap(prepareCacheForResult), 353 366 forward, 354 367 map(updateCacheWithResult) 355 368 );
+1 -1
exchanges/graphcache/src/operations/invalidate.test.ts
··· 42 42 } 43 43 ); 44 44 45 - InMemoryData.initDataState(store.data, 0); 45 + InMemoryData.initDataState(store.data, null); 46 46 jest.clearAllMocks(); 47 47 }); 48 48
+1 -1
exchanges/graphcache/src/operations/query.ts
··· 64 64 request: OperationRequest, 65 65 data?: Data 66 66 ): QueryResult => { 67 - initDataState(store.data, 0); 67 + initDataState(store.data, null); 68 68 const result = read(store, request, data); 69 69 clearDataState(); 70 70 return result;
+1 -1
exchanges/graphcache/src/operations/write.test.ts
··· 148 148 expect(console.warn).toHaveBeenCalledTimes(2); 149 149 expect((console.warn as any).mock.calls[0][0]).toMatch(/undefined/); 150 150 151 - InMemoryData.initDataState(store.data, 0); 151 + InMemoryData.initDataState(store.data, null); 152 152 // The field must still be `'test'` 153 153 expect(InMemoryData.readRecord('Query', 'field')).toBe('test'); 154 154 });
+5 -4
exchanges/graphcache/src/operations/write.ts
··· 55 55 export const write = ( 56 56 store: Store, 57 57 request: OperationRequest, 58 - data: Data 58 + data: Data, 59 + key?: number 59 60 ): WriteResult => { 60 - initDataState(store.data, 0); 61 + initDataState(store.data, key || null); 61 62 const result = startWrite(store, request, data); 62 63 clearDataState(); 63 64 return result; ··· 100 101 export const writeOptimistic = ( 101 102 store: Store, 102 103 request: OperationRequest, 103 - optimisticKey: number 104 + key: number 104 105 ): WriteResult => { 105 - initDataState(store.data, optimisticKey); 106 + initDataState(store.data, key, true); 106 107 107 108 const operation = getMainOperation(request.query); 108 109 const result: WriteResult = { dependencies: getCurrentDependencies() };
+124 -5
exchanges/graphcache/src/store/data.test.ts
··· 78 78 InMemoryData.writeRecord('Todo:1', 'id', '1'); 79 79 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 80 80 81 - InMemoryData.initDataState(data, 1); 81 + InMemoryData.initDataState(data, 1, true); 82 82 InMemoryData.writeLink('Query', 'temp', 'Todo:1'); 83 - InMemoryData.initDataState(data, 0); 83 + InMemoryData.initDataState(data, 0, true); 84 84 85 85 InMemoryData.writeLink('Query', 'todo', undefined); 86 86 InMemoryData.gc(data); 87 87 88 88 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 89 89 90 - InMemoryData.clearOptimistic(data, 1); 90 + InMemoryData.clearLayer(data, 1); 91 91 InMemoryData.gc(data); 92 92 expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 93 93 ··· 170 170 }); 171 171 172 172 it('returns field infos for all optimistic updates', () => { 173 - InMemoryData.initDataState(data, 1); 173 + InMemoryData.initDataState(data, 1, true); 174 174 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 175 175 176 176 expect(InMemoryData.inspectFields('Random')).toMatchInlineSnapshot( ··· 181 181 it('avoids duplicate field infos', () => { 182 182 InMemoryData.writeLink('Query', 'todo', 'Todo:1'); 183 183 184 - InMemoryData.initDataState(data, 1); 184 + InMemoryData.initDataState(data, 1, true); 185 185 InMemoryData.writeLink('Query', 'todo', 'Todo:2'); 186 186 187 187 expect(InMemoryData.inspectFields('Query')).toMatchInlineSnapshot(` ··· 195 195 `); 196 196 }); 197 197 }); 198 + 199 + describe('commutative changes', () => { 200 + it('always applies out-of-order updates in-order', () => { 201 + InMemoryData.reserveLayer(data, 1); 202 + InMemoryData.reserveLayer(data, 2); 203 + 204 + InMemoryData.initDataState(data, 2); 205 + InMemoryData.writeRecord('Query', 'index', 2); 206 + InMemoryData.clearDataState(); 207 + 208 + InMemoryData.initDataState(data, null); 209 + expect(InMemoryData.readRecord('Query', 'index')).toBe(2); 210 + 211 + InMemoryData.initDataState(data, 1); 212 + InMemoryData.writeRecord('Query', 'index', 1); 213 + InMemoryData.clearDataState(); 214 + 215 + InMemoryData.initDataState(data, null); 216 + expect(InMemoryData.readRecord('Query', 'index')).toBe(2); 217 + }); 218 + 219 + it('creates optimistic layers that may be removed using clearLayer', () => { 220 + InMemoryData.reserveLayer(data, 1); 221 + InMemoryData.reserveLayer(data, 2); 222 + 223 + InMemoryData.initDataState(data, 2); 224 + InMemoryData.writeRecord('Query', 'index', 2); 225 + InMemoryData.clearDataState(); 226 + 227 + InMemoryData.initDataState(data, null); 228 + expect(InMemoryData.readRecord('Query', 'index')).toBe(2); 229 + 230 + // Actively clearing out layer 2 231 + InMemoryData.clearLayer(data, 2); 232 + 233 + InMemoryData.initDataState(data, null); 234 + expect(InMemoryData.readRecord('Query', 'index')).toBe(undefined); 235 + 236 + InMemoryData.initDataState(data, 1); 237 + InMemoryData.writeRecord('Query', 'index', 1); 238 + InMemoryData.clearDataState(); 239 + 240 + InMemoryData.initDataState(data, null); 241 + expect(InMemoryData.readRecord('Query', 'index')).toBe(1); 242 + }); 243 + 244 + it('overrides data using optimistic layers', () => { 245 + InMemoryData.reserveLayer(data, 1); 246 + InMemoryData.reserveLayer(data, 2); 247 + InMemoryData.reserveLayer(data, 3); 248 + 249 + InMemoryData.initDataState(data, 2); 250 + InMemoryData.writeRecord('Query', 'index', 2); 251 + InMemoryData.clearDataState(); 252 + 253 + InMemoryData.initDataState(data, 3); 254 + InMemoryData.writeRecord('Query', 'index', 3); 255 + InMemoryData.clearDataState(); 256 + 257 + // Regular write that isn't optimistic 258 + InMemoryData.initDataState(data, null); 259 + InMemoryData.writeRecord('Query', 'index', 1); 260 + InMemoryData.clearDataState(); 261 + 262 + InMemoryData.initDataState(data, null); 263 + expect(InMemoryData.readRecord('Query', 'index')).toBe(3); 264 + }); 265 + 266 + it('avoids optimistic layers when only one layer is pending', () => { 267 + InMemoryData.reserveLayer(data, 1); 268 + 269 + InMemoryData.initDataState(data, 1); 270 + InMemoryData.writeRecord('Query', 'index', 2); 271 + InMemoryData.clearDataState(); 272 + 273 + // This will be applied and visible since the above write isn't optimistic 274 + InMemoryData.initDataState(data, null); 275 + InMemoryData.writeRecord('Query', 'index', 1); 276 + InMemoryData.clearDataState(); 277 + 278 + InMemoryData.initDataState(data, null); 279 + expect(InMemoryData.readRecord('Query', 'index')).toBe(1); 280 + }); 281 + 282 + it('continues applying optimistic layers even if the first one completes', () => { 283 + InMemoryData.reserveLayer(data, 1); 284 + InMemoryData.reserveLayer(data, 2); 285 + InMemoryData.reserveLayer(data, 3); 286 + InMemoryData.reserveLayer(data, 4); 287 + 288 + InMemoryData.initDataState(data, 1); 289 + InMemoryData.writeRecord('Query', 'index', 1); 290 + InMemoryData.clearDataState(); 291 + 292 + InMemoryData.initDataState(data, null); 293 + expect(InMemoryData.readRecord('Query', 'index')).toBe(1); 294 + 295 + InMemoryData.initDataState(data, 3); 296 + InMemoryData.writeRecord('Query', 'index', 3); 297 + InMemoryData.clearDataState(); 298 + 299 + InMemoryData.initDataState(data, null); 300 + expect(InMemoryData.readRecord('Query', 'index')).toBe(3); 301 + 302 + InMemoryData.initDataState(data, 4); 303 + InMemoryData.writeRecord('Query', 'index', 4); 304 + InMemoryData.clearDataState(); 305 + 306 + InMemoryData.initDataState(data, null); 307 + expect(InMemoryData.readRecord('Query', 'index')).toBe(4); 308 + 309 + InMemoryData.initDataState(data, 2); 310 + InMemoryData.writeRecord('Query', 'index', 2); 311 + InMemoryData.clearDataState(); 312 + 313 + InMemoryData.initDataState(data, null); 314 + expect(InMemoryData.readRecord('Query', 'index')).toBe(4); 315 + }); 316 + });
+148 -51
exchanges/graphcache/src/store/data.ts
··· 18 18 interface NodeMap<T> { 19 19 optimistic: OptimisticMap<KeyMap<Dict<T | undefined>>>; 20 20 base: KeyMap<Dict<T>>; 21 - keys: number[]; 22 21 } 23 22 24 23 export interface InMemoryData { 24 + /** Ensure persistence is never scheduled twice at a time */ 25 25 persistenceScheduled: boolean; 26 + /** Batches changes to the data layer that'll be written to `storage` */ 26 27 persistenceBatch: SerializedEntries; 28 + /** Ensure garbage collection is never scheduled twice at a time */ 27 29 gcScheduled: boolean; 30 + /** A list of entities that have been flagged for gargabe collection since no references to them are left */ 28 31 gcBatch: Set<string>; 32 + /** The API's "Query" typename which is needed to filter dependencies */ 29 33 queryRootKey: string; 34 + /** Number of references to each entity (except "Query") */ 30 35 refCount: Dict<number>; 36 + /** Number of references to each entity on optimistic layers */ 31 37 refLock: OptimisticMap<Dict<number>>; 38 + /** A map of entity fields (key-value entries per entity) */ 32 39 records: NodeMap<EntityField>; 40 + /** A map of entity links which are connections from one entity to another (key-value entries per entity) */ 33 41 links: NodeMap<Link>; 42 + /** A set of Query operation keys that are in-flight and awaiting a result */ 43 + commutativeKeys: Set<number>; 44 + /** The order of optimistic layers */ 45 + optimisticOrder: number[]; 46 + /** This may be a persistence adapter that will receive changes in a batch */ 34 47 storage: StorageAdapter | null; 35 48 } 36 49 ··· 41 54 const makeNodeMap = <T>(): NodeMap<T> => ({ 42 55 optimistic: makeDict(), 43 56 base: new Map(), 44 - keys: [], 45 57 }); 46 58 47 59 /** Before reading or writing the global state needs to be initialised */ 48 60 export const initDataState = ( 49 61 data: InMemoryData, 50 - optimisticKey: number | null 62 + layerKey: number | null, 63 + forceOptimistic?: boolean 51 64 ) => { 52 65 currentData = data; 53 66 currentDependencies = new Set(); 54 - currentOptimisticKey = optimisticKey; 55 67 if (process.env.NODE_ENV !== 'production') { 56 68 currentDebugStack.length = 0; 57 69 } 70 + 71 + if (!layerKey) { 72 + currentOptimisticKey = null; 73 + } else if ( 74 + forceOptimistic || 75 + (data.commutativeKeys.size > 1 && data.commutativeKeys.has(layerKey)) 76 + ) { 77 + // An optimistic update of a mutation may force an optimistic layer, 78 + // or this Query update may be applied optimistically since it's part 79 + // of a commutate chain 80 + currentOptimisticKey = layerKey; 81 + createLayer(data, layerKey); 82 + } else { 83 + // Otherwise we don't create an optimistic layer and clear the 84 + // operation's one if it already exists 85 + currentOptimisticKey = null; 86 + clearLayer(data, layerKey); 87 + } 58 88 }; 59 89 60 90 /** Reset the data state after read/write is complete */ 61 91 export const clearDataState = () => { 62 92 const data = currentData!; 93 + const optimisticKey = currentOptimisticKey; 94 + currentOptimisticKey = null; 95 + 96 + if (optimisticKey && data.commutativeKeys.has(optimisticKey)) { 97 + const commutativeIndex = 98 + data.optimisticOrder.length - data.commutativeKeys.size; 99 + const blockingKey = data.optimisticOrder[data.optimisticOrder.length - 1]; 100 + // If this is a Query operation that is in the list of commutative keys 101 + // and is the "first" one and hence blocking all others, we squash all 102 + // results and empty the list of commutative keys 103 + if (blockingKey === optimisticKey) { 104 + const squash: number[] = []; 105 + const orderSize = data.optimisticOrder.length; 106 + // Collect all completed, commutative layers until and excluding the first 107 + // pending one that overrides the others 108 + for (let i = commutativeIndex; i < orderSize; i++) { 109 + const layerKey = data.optimisticOrder[i]; 110 + if (!data.refLock[layerKey]) break; 111 + squash.unshift(layerKey); 112 + } 113 + 114 + // Apply all completed, commutative layers 115 + for (let i = 0, l = squash.length; i < l; i++) { 116 + squashLayer(squash[i]); 117 + } 118 + } 119 + } 120 + 121 + currentData = null; 122 + currentDependencies = null; 123 + if (process.env.NODE_ENV !== 'production') { 124 + currentDebugStack.length = 0; 125 + } 63 126 64 127 if (!data.gcScheduled && data.gcBatch.size > 0) { 65 128 data.gcScheduled = true; ··· 76 139 data.persistenceBatch = makeDict(); 77 140 }); 78 141 } 79 - 80 - currentData = null; 81 - currentDependencies = null; 82 - currentOptimisticKey = null; 83 - if (process.env.NODE_ENV !== 'production') { 84 - currentDebugStack.length = 0; 85 - } 86 142 }; 87 143 88 144 /** As we're writing, we keep around all the records and links we've read or have written to */ ··· 108 164 refLock: makeDict(), 109 165 links: makeNodeMap(), 110 166 records: makeNodeMap(), 167 + commutativeKeys: new Set(), 168 + optimisticOrder: [], 111 169 storage: null, 112 170 }); 113 171 ··· 120 178 ) => { 121 179 // Optimistic values are written to a map in the optimistic dict 122 180 // All other values are written to the base map 123 - let keymap: KeyMap<Dict<T | undefined>>; 124 - if (currentOptimisticKey) { 125 - // If the optimistic map doesn't exist yet, it' created, and 126 - // the optimistic key is stored (in order of priority) 127 - if (map.optimistic[currentOptimisticKey] === undefined) { 128 - map.optimistic[currentOptimisticKey] = new Map(); 129 - map.keys.unshift(currentOptimisticKey); 130 - } 131 - 132 - keymap = map.optimistic[currentOptimisticKey]; 133 - } else { 134 - keymap = map.base; 135 - } 181 + const keymap: KeyMap<Dict<T | undefined>> = currentOptimisticKey 182 + ? map.optimistic[currentOptimisticKey] 183 + : map.base; 136 184 137 185 // On the map itself we get or create the entity as a dict 138 186 let entity = keymap.get(entityKey) as Dict<T | undefined>; ··· 156 204 entityKey: string, 157 205 fieldKey: string 158 206 ): T | undefined => { 207 + let node: Dict<T | undefined> | undefined; 208 + 159 209 // This first iterates over optimistic layers (in order) 160 - for (let i = 0, l = map.keys.length; i < l; i++) { 161 - const optimistic = map.optimistic[map.keys[i]]; 162 - const node = optimistic.get(entityKey); 210 + for (let i = 0, l = currentData!.optimisticOrder.length; i < l; i++) { 211 + const optimistic = map.optimistic[currentData!.optimisticOrder[i]]; 163 212 // If the node and node value exists it is returned, including undefined 164 - if (node !== undefined && fieldKey in node) { 213 + if ( 214 + optimistic && 215 + (node = optimistic.get(entityKey)) !== undefined && 216 + fieldKey in node 217 + ) { 165 218 return node[fieldKey]; 166 219 } 167 220 } 168 221 169 222 // Otherwise we read the non-optimistic base value 170 - const node = map.base.get(entityKey); 223 + node = map.base.get(entityKey); 171 224 return node !== undefined ? node[fieldKey] : undefined; 172 225 }; 173 226 174 - /** Clears an optimistic layers from a NodeMap */ 175 - const clearOptimisticNodes = <T>(map: NodeMap<T>, optimisticKey: number) => { 176 - // Check whether the optimistic layer exists on the NodeMap 177 - const index = map.keys.indexOf(optimisticKey); 178 - if (index > -1) { 179 - // Then delete it and splice out the optimisticKey 180 - delete map.optimistic[optimisticKey]; 181 - map.keys.splice(index, 1); 182 - } 183 - }; 184 - 185 227 /** Adjusts the reference count of an entity on a refCount dict by "by" and updates the gcBatch */ 186 228 const updateRCForEntity = ( 187 229 gcBatch: void | Set<string>, 188 230 refCount: Dict<number>, 189 231 entityKey: string, 190 232 by: number 191 - ) => { 233 + ): void => { 192 234 // Retrieve the reference count 193 235 const count = refCount[entityKey] !== undefined ? refCount[entityKey] : 0; 194 236 // Adjust it by the "by" value ··· 207 249 refCount: Dict<number>, 208 250 link: Link | undefined, 209 251 by: number 210 - ) => { 252 + ): void => { 211 253 if (typeof link === 'string') { 212 254 updateRCForEntity(gcBatch, refCount, link, by); 213 255 } else if (Array.isArray(link)) { ··· 225 267 fieldInfos: FieldInfo[], 226 268 seenFieldKeys: Set<string>, 227 269 node: Dict<T> | undefined 228 - ) => { 270 + ): void => { 229 271 if (node !== undefined) { 230 272 for (const fieldKey in node) { 231 273 if (!seenFieldKeys.has(fieldKey)) { ··· 249 291 extractNodeFields(fieldInfos, seenFieldKeys, map.base.get(entityKey)); 250 292 251 293 // Then extracts FieldInfo for the entity from the optimistic maps 252 - for (let i = 0, l = map.keys.length; i < l; i++) { 253 - const optimistic = map.optimistic[map.keys[i]]; 294 + for (let i = 0, l = currentData!.optimisticOrder.length; i < l; i++) { 295 + const optimistic = map.optimistic[currentData!.optimisticOrder[i]]; 254 296 extractNodeFields(fieldInfos, seenFieldKeys, optimistic.get(entityKey)); 255 297 } 256 298 }; ··· 408 450 updateRCForLink(gcBatch, refCount, link, 1); 409 451 }; 410 452 453 + /** Reserves an optimistic layer and preorders it */ 454 + export const reserveLayer = (data: InMemoryData, layerKey: number) => { 455 + if (!data.commutativeKeys.has(layerKey)) { 456 + // The new layer needs to be reserved in front of all other commutative 457 + // keys but after all non-commutative keys (which are added by `forceUpdate`) 458 + data.optimisticOrder.splice( 459 + data.optimisticOrder.length - data.commutativeKeys.size, 460 + 0, 461 + layerKey 462 + ); 463 + data.commutativeKeys.add(layerKey); 464 + } 465 + }; 466 + 467 + /** Creates an optimistic layer of links and records */ 468 + const createLayer = (data: InMemoryData, layerKey: number) => { 469 + if (data.optimisticOrder.indexOf(layerKey) === -1) { 470 + data.optimisticOrder.unshift(layerKey); 471 + } 472 + 473 + if (!data.refLock[layerKey]) { 474 + data.refLock[layerKey] = makeDict(); 475 + data.links.optimistic[layerKey] = new Map(); 476 + data.records.optimistic[layerKey] = new Map(); 477 + } 478 + }; 479 + 411 480 /** Removes an optimistic layer of links and records */ 412 - export const clearOptimistic = (data: InMemoryData, optimisticKey: number) => { 413 - // We also delete the optimistic reference locks 414 - delete data.refLock[optimisticKey]; 415 - clearOptimisticNodes(data.records, optimisticKey); 416 - clearOptimisticNodes(data.links, optimisticKey); 481 + export const clearLayer = (data: InMemoryData, layerKey: number) => { 482 + const index = data.optimisticOrder.indexOf(layerKey); 483 + if (index > -1) { 484 + data.optimisticOrder.splice(index, 1); 485 + data.commutativeKeys.delete(layerKey); 486 + } 487 + 488 + if (data.refLock[layerKey]) { 489 + delete data.refLock[layerKey]; 490 + delete data.records.optimistic[layerKey]; 491 + delete data.links.optimistic[layerKey]; 492 + } 493 + }; 494 + 495 + /** Merges an optimistic layer of links and records into the base data */ 496 + const squashLayer = (layerKey: number) => { 497 + const links = currentData!.links.optimistic[layerKey]; 498 + if (links) { 499 + links.forEach((keyMap, entityKey) => { 500 + for (const fieldKey in keyMap) 501 + writeLink(entityKey, fieldKey, keyMap[fieldKey]); 502 + }); 503 + } 504 + 505 + const records = currentData!.records.optimistic[layerKey]; 506 + if (records) { 507 + records.forEach((keyMap, entityKey) => { 508 + for (const fieldKey in keyMap) 509 + writeRecord(entityKey, fieldKey, keyMap[fieldKey]); 510 + }); 511 + } 512 + 513 + clearLayer(currentData!, layerKey); 417 514 }; 418 515 419 516 /** Return an array of FieldInfo (info on all the fields and their arguments) for a given entity */ ··· 435 532 storage: StorageAdapter, 436 533 entries: SerializedEntries 437 534 ) => { 438 - initDataState(data, 0); 535 + initDataState(data, null); 439 536 for (const key in entries) { 440 537 const dotIndex = key.indexOf('.'); 441 538 const entityKey = key.slice(2, dotIndex);
+2 -1
exchanges/graphcache/src/store/index.ts
··· 1 1 export { 2 2 initDataState, 3 3 clearDataState, 4 - clearOptimistic, 4 + reserveLayer, 5 + clearLayer, 5 6 getCurrentDependencies, 6 7 } from './data'; 7 8
+12 -12
exchanges/graphcache/src/store/store.test.ts
··· 86 86 }, 87 87 ], 88 88 }; 89 - InMemoryData.initDataState(store.data, 0); 89 + InMemoryData.initDataState(store.data, null); 90 90 write(store, { query: Todos }, todosData); 91 91 InMemoryData.initDataState(store.data, null); 92 92 }); ··· 132 132 it('should be able to invalidate data (one relation key)', () => { 133 133 let { data } = query(store, { query: Todos }); 134 134 135 - InMemoryData.initDataState(store.data, 0); 135 + InMemoryData.initDataState(store.data, null); 136 136 expect((data as any).todos).toHaveLength(3); 137 137 expect(InMemoryData.readRecord('Todo:0', 'text')).toBe('Go to the shops'); 138 138 store.invalidateQuery(Todos); ··· 141 141 ({ data } = query(store, { query: Todos })); 142 142 expect(data).toBe(null); 143 143 144 - InMemoryData.initDataState(store.data, 0); 144 + InMemoryData.initDataState(store.data, null); 145 145 expect(InMemoryData.readRecord('Todo:0', 'text')).toBe(undefined); 146 146 }); 147 147 ··· 168 168 }); 169 169 expect((data as any).appointment.info).toBe('urql meeting'); 170 170 171 - InMemoryData.initDataState(store.data, 0); 171 + InMemoryData.initDataState(store.data, null); 172 172 expect(InMemoryData.readRecord('Appointment:1', 'info')).toBe( 173 173 'urql meeting' 174 174 ); ··· 181 181 })); 182 182 expect(data).toBe(null); 183 183 184 - InMemoryData.initDataState(store.data, 0); 184 + InMemoryData.initDataState(store.data, null); 185 185 expect(InMemoryData.readRecord('Appointment:1', 'info')).toBe(undefined); 186 186 }); 187 187 188 188 it('should be able to write a fragment', () => { 189 - InMemoryData.initDataState(store.data, 0); 189 + InMemoryData.initDataState(store.data, null); 190 190 191 191 store.writeFragment( 192 192 gql` ··· 223 223 }); 224 224 225 225 it('should be able to read a fragment', () => { 226 - InMemoryData.initDataState(store.data, 0); 226 + InMemoryData.initDataState(store.data, null); 227 227 const result = store.readFragment( 228 228 gql` 229 229 fragment _ on Todo { ··· 249 249 }); 250 250 251 251 it('should be able to update a query', () => { 252 - InMemoryData.initDataState(store.data, 0); 252 + InMemoryData.initDataState(store.data, null); 253 253 store.updateQuery({ query: Todos }, data => ({ 254 254 ...data, 255 255 todos: [ ··· 309 309 } 310 310 ); 311 311 312 - InMemoryData.initDataState(store.data, 0); 312 + InMemoryData.initDataState(store.data, null); 313 313 store.updateQuery({ query: Appointment, variables: { id: '1' } }, data => ({ 314 314 ...data, 315 315 appointment: { ··· 334 334 }); 335 335 336 336 it('should be able to read a query', () => { 337 - InMemoryData.initDataState(store.data, 0); 337 + InMemoryData.initDataState(store.data, null); 338 338 const result = store.readQuery({ query: Todos }); 339 339 340 340 const deps = InMemoryData.getCurrentDependencies(); ··· 399 399 ], 400 400 }); 401 401 402 - InMemoryData.clearOptimistic(store.data, 1); 402 + InMemoryData.clearLayer(store.data, 1); 403 403 ({ data } = query(store, { query: Todos })); 404 404 expect(data).toEqual({ 405 405 __typename: 'Query', ··· 477 477 expectedData 478 478 ); 479 479 480 - InMemoryData.initDataState(store.data, 0); 480 + InMemoryData.initDataState(store.data, null); 481 481 InMemoryData.writeLink( 482 482 'Query', 483 483 store.keyOfField('appointment', { id: '1' }),
+1 -1
exchanges/graphcache/src/test-utils/examples-1.test.ts
··· 417 417 418 418 write(store, { query: getRoot }, queryData); 419 419 writeOptimistic(store, { query: updateItem, variables: { id: '2' } }, 1); 420 - InMemoryData.clearOptimistic(store.data, 1); 420 + InMemoryData.clearLayer(store.data, 1); 421 421 const queryRes = query(store, { query: getRoot }); 422 422 423 423 expect(queryRes.partial).toBe(false);
+8 -6
scripts/jest/setup.js
··· 7 7 8 8 jest.restoreAllMocks(); 9 9 10 + const originalConsole = console; 10 11 global.console = { 11 - ...console, 12 - log: jest.fn(), 13 - warn: jest.fn(), 14 - error: jest.fn(message => { 15 - throw new Error(message); 16 - }) 12 + ...originalConsole, 13 + warn() { /* noop */ }, 14 + error(message) { throw new Error(message); } 17 15 }; 16 + 17 + jest.spyOn(console, 'log'); 18 + jest.spyOn(console, 'warn'); 19 + jest.spyOn(console, 'error');