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.

fix(graphcache): Improve separation of API and owned data (#3165)

authored by

Phil Pluckthun and committed by
GitHub
6031d913 b5e50b4f

+165 -146
+5
.changeset/gentle-melons-march.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Prevent reusal of incoming API data in Graphcache’s produced (“owned”) data. This prevents us from copying the `__typename` and other superfluous fields.
+3 -19
exchanges/graphcache/src/cacheExchange.test.ts
··· 100 100 expect(response).toHaveBeenCalledTimes(1); 101 101 expect(result).toHaveBeenCalledTimes(2); 102 102 103 - expect(result.mock.calls[0][0]).toHaveProperty( 104 - 'operation.context.meta.cacheOutcome', 105 - 'miss' 106 - ); 107 - expect(result.mock.calls[0][0].data).toEqual(expected); 103 + expect(expected).toMatchObject(result.mock.calls[0][0].data); 108 104 expect(result.mock.calls[1][0]).toHaveProperty( 109 105 'operation.context.meta.cacheOutcome', 110 106 'hit' 111 107 ); 112 - expect(result.mock.calls[1][0].data).toEqual(expected); 113 - 108 + expect(expected).toMatchObject(result.mock.calls[1][0].data); 114 109 expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); 115 110 }); 116 111 ··· 230 225 231 226 next(opMultiple); 232 227 expect(response).toHaveBeenCalledTimes(2); 233 - expect(reexec).toHaveBeenCalledWith(opOne); 228 + expect(reexec.mock.calls[0][0]).toHaveProperty('key', opOne.key); 234 229 expect(result).toHaveBeenCalledTimes(3); 235 230 236 231 // test for reference reuse ··· 775 770 expect(reexec).toHaveBeenCalledTimes(1); 776 771 777 772 expect(result.mock.calls[1][0]?.data).toMatchObject({ 778 - __typename: 'Query', 779 773 author: { name: '[REDACTED OFFLINE]' }, 780 774 }); 781 775 ··· 1263 1257 1264 1258 vi.runAllTimers(); 1265 1259 expect(result.mock.calls[1][0].data).toEqual({ 1266 - __typename: 'Mutation', 1267 1260 concealAuthor: { 1268 1261 __typename: 'Author', 1269 1262 id: '123', ··· 1429 1422 expect(response).toHaveBeenCalledTimes(2); 1430 1423 expect(fakeResolver).toHaveBeenCalledTimes(6); 1431 1424 expect(result.mock.calls[1][0].data).toEqual({ 1432 - __typename: 'Mutation', 1433 1425 concealAuthors: [ 1434 1426 { 1435 1427 __typename: 'Author', ··· 1583 1575 ], 1584 1576 }); 1585 1577 1586 - expect(result.mock.calls[0][0]).not.toHaveProperty( 1587 - 'operation.context.meta' 1588 - ); 1589 - 1590 1578 next(queryOperation); 1591 1579 vi.runAllTimers(); 1592 1580 expect(result).toHaveBeenCalledTimes(2); ··· 1724 1712 }, 1725 1713 ], 1726 1714 }); 1727 - 1728 - expect(result.mock.calls[0][0]).not.toHaveProperty( 1729 - 'operation.context.meta' 1730 - ); 1731 1715 1732 1716 next(queryOperation); 1733 1717 vi.runAllTimers();
+47 -38
exchanges/graphcache/src/cacheExchange.ts
··· 20 20 Source, 21 21 } from 'wonka'; 22 22 23 - import { query, write, writeOptimistic } from './operations'; 23 + import { _query } from './operations/query'; 24 + import { _write } from './operations/write'; 24 25 import { addMetadata, toRequestPolicy } from './helpers/operation'; 25 26 import { filterVariables, getMainOperation } from './ast'; 26 - import { Store, noopDataState, hydrateData, reserveLayer } from './store'; 27 + import { 28 + Store, 29 + initDataState, 30 + clearDataState, 31 + noopDataState, 32 + hydrateData, 33 + reserveLayer, 34 + } from './store'; 27 35 import { Data, Dependencies, CacheExchangeOpts } from './types'; 28 36 29 37 interface OperationResultWithMeta extends Partial<OperationResult> { ··· 110 118 if (op) { 111 119 // Collect all dependent operations if the reexecuting operation is a query 112 120 if (operation.kind === 'query') dependentOperations.add(key); 113 - operations.delete(key); 114 121 let policy: RequestPolicy = 'cache-first'; 115 122 if (requestedRefetch.has(key)) { 116 123 requestedRefetch.delete(key); ··· 135 142 if (operation.kind === 'query') { 136 143 // Pre-reserve the position of the result layer 137 144 reserveLayer(store.data, operation.key); 145 + operations.set(operation.key, operation); 138 146 } else if (operation.kind === 'teardown') { 139 147 // Delete reference to operation if any exists to release it 140 148 operations.delete(operation.key); ··· 146 154 operation.kind === 'mutation' && 147 155 operation.context.requestPolicy !== 'network-only' 148 156 ) { 157 + operations.set(operation.key, operation); 149 158 // This executes an optimistic update for mutations and registers it if necessary 150 - const { dependencies } = writeOptimistic( 151 - store, 152 - operation, 153 - operation.key 154 - ); 159 + initDataState('write', store.data, operation.key, true, false); 160 + const { dependencies } = _write(store, operation, undefined, undefined); 161 + clearDataState(); 155 162 if (dependencies.size) { 156 163 // Update blocked optimistic dependencies 157 164 for (const dep of dependencies.values()) blockedDependencies.add(dep); ··· 178 185 ) 179 186 : operation.variables, 180 187 }, 181 - { ...operation.context, originalVariables: operation.variables } 188 + operation.context 182 189 ); 183 190 }; 184 191 ··· 196 203 const operationResultFromCache = ( 197 204 operation: Operation 198 205 ): OperationResultWithMeta => { 199 - const result = query(store, operation, results.get(operation.key)); 206 + initDataState('read', store.data, undefined, false, false); 207 + const result = _query( 208 + store, 209 + operation, 210 + results.get(operation.key), 211 + undefined 212 + ); 213 + clearDataState(); 200 214 const cacheOutcome: CacheOutcome = result.data 201 215 ? !result.partial && !result.hasNext 202 216 ? 'hit' ··· 204 218 : 'miss'; 205 219 206 220 results.set(operation.key, result.data); 207 - operations.set(operation.key, operation); 208 221 updateDependencies(operation, result.dependencies); 209 222 210 223 return { ··· 222 235 pendingOperations: Operations 223 236 ): OperationResult => { 224 237 // Retrieve the original operation to remove changes made by formatDocument 225 - const originalOperation = operations.get(result.operation.key); 226 - const operation = originalOperation 227 - ? makeOperation( 228 - originalOperation.kind, 229 - originalOperation, 230 - result.operation.context 231 - ) 232 - : result.operation; 233 - 238 + const operation = 239 + operations.get(result.operation.key) || result.operation; 234 240 if (operation.kind === 'mutation') { 235 - if (result.operation.context.originalVariables) { 236 - operation.variables = result.operation.context.originalVariables; 237 - delete result.operation.context.originalVariables; 238 - } 239 - 240 241 // Collect previous dependencies that have been written for optimistic updates 241 242 const dependencies = optimisticKeysToDependencies.get(operation.key); 242 243 collectPendingOperations(pendingOperations, dependencies); ··· 251 252 if (data) { 252 253 // Write the result to cache and collect all dependencies that need to be 253 254 // updated 254 - const writeDependencies = write( 255 + initDataState('write', store.data, operation.key, false, false); 256 + const writeDependencies = _write( 255 257 store, 256 258 operation, 257 259 data, 258 - result.error, 259 - operation.key 260 + result.error 260 261 ).dependencies; 262 + clearDataState(); 261 263 collectPendingOperations(pendingOperations, writeDependencies); 262 - 263 - const queryResult = query( 264 + const prevData = 265 + operation.kind === 'query' ? results.get(operation.key) : null; 266 + initDataState( 267 + 'read', 268 + store.data, 269 + operation.key, 270 + false, 271 + prevData !== data 272 + ); 273 + const queryResult = _query( 264 274 store, 265 275 operation, 266 - operation.kind === 'query' 267 - ? results.get(operation.key) || data 268 - : data, 269 - result.error, 270 - operation.key 276 + prevData || data, 277 + result.error 271 278 ); 272 - 279 + clearDataState(); 273 280 data = queryResult.data; 274 281 if (operation.kind === 'query') { 275 282 // Collect the query's dependencies for future pending operation updates ··· 283 290 284 291 // Update this operation's dependencies if it's a query 285 292 if (queryDependencies) { 286 - operations.set(operation.key, operation); 287 293 updateDependencies(result.operation, queryDependencies); 288 294 } 289 295 ··· 374 380 /*noop*/ 375 381 } else if (!isBlockedByOptimisticUpdate(res.dependencies)) { 376 382 client.reexecuteOperation( 377 - toRequestPolicy(res.operation, 'network-only') 383 + toRequestPolicy( 384 + operations.get(res.operation.key) || res.operation, 385 + 'network-only' 386 + ) 378 387 ); 379 388 } else if (requestPolicy === 'cache-and-network') { 380 389 requestedRefetch.add(res.operation.key);
+2 -1
exchanges/graphcache/src/extras/relayPagination.test.ts
··· 1 1 import { gql } from '@urql/core'; 2 2 import { it, expect } from 'vitest'; 3 - import { query, write } from '../operations'; 3 + import { __initAnd_query as query } from '../operations/query'; 4 + import { __initAnd_write as write } from '../operations/write'; 4 5 import { Store } from '../store'; 5 6 import { relayPagination } from './relayPagination'; 6 7
+2 -1
exchanges/graphcache/src/extras/simplePagination.test.ts
··· 1 1 import { gql } from '@urql/core'; 2 2 import { it, expect } from 'vitest'; 3 - import { query, write } from '../operations'; 3 + import { __initAnd_query as query } from '../operations/query'; 4 + import { __initAnd_write as write } from '../operations/write'; 4 5 import { Store } from '../store'; 5 6 import { simplePagination } from './simplePagination'; 6 7
+1 -2
exchanges/graphcache/src/index.ts
··· 1 1 export * from './types'; 2 - export { query, write } from './operations'; 3 - export { Store } from './store'; 2 + export type { Store } from './store'; 4 3 export { cacheExchange } from './cacheExchange'; 5 4 export { offlineExchange } from './offlineExchange';
+1 -1
exchanges/graphcache/src/offlineExchange.test.ts
··· 160 160 161 161 next(queryOp); 162 162 expect(result).toBeCalledTimes(1); 163 - expect(result.mock.calls[0][0].data).toMatchObject(queryOneData); 163 + expect(queryOneData).toMatchObject(result.mock.calls[0][0].data); 164 164 165 165 next(mutationOp); 166 166 expect(result).toBeCalledTimes(1);
-2
exchanges/graphcache/src/operations/index.ts
··· 1 - export { query, read } from './query'; 2 - export { write, writeOptimistic, writeFragment } from './write';
+26 -25
exchanges/graphcache/src/operations/query.test.ts
··· 5 5 import { describe, it, beforeEach, beforeAll, expect } from 'vitest'; 6 6 7 7 import { Store } from '../store'; 8 - import { write } from './write'; 9 - import { query } from './query'; 8 + import { __initAnd_write as write } from './write'; 9 + import { __initAnd_query as query } from './query'; 10 10 11 11 const TODO_QUERY = gql` 12 12 query Todos { ··· 363 363 expect(previousData).toHaveProperty('todos.0.textB', 'old'); 364 364 }); 365 365 366 - it('should not keep references stable', () => { 366 + it('should keep references stable', () => { 367 367 const QUERY = gql` 368 368 query todos { 369 369 __typename ··· 398 398 399 399 write(store, { query: QUERY }, expected); 400 400 401 - const prevData = { 402 - todos: [ 403 - { 404 - __typename: 'Todo', 405 - id: 'prev-0', 406 - }, 407 - { 408 - __typename: 'Todo', 409 - id: '1', 410 - }, 411 - { 412 - __typename: 'Todo', 413 - id: '2', 414 - }, 415 - ], 416 - __typename: 'query_root', 417 - }; 401 + const prevData = query( 402 + store, 403 + { query: QUERY }, 404 + { 405 + todos: [ 406 + { 407 + __typename: 'Todo', 408 + id: 'prev-0', 409 + }, 410 + { 411 + __typename: 'Todo', 412 + id: '1', 413 + }, 414 + { 415 + __typename: 'Todo', 416 + id: '2', 417 + }, 418 + ], 419 + __typename: 'query_root', 420 + } 421 + ).data as any; 418 422 419 - const data = query(store, { query: QUERY }, prevData) 420 - .data as typeof expected; 423 + const data = query(store, { query: QUERY }, prevData).data as any; 421 424 expect(data).toEqual(expected); 422 425 423 - expect(prevData.todos[0]).not.toEqual(data.todos[0]); 424 - expect(prevData.todos[0]).not.toBe(data.todos[0]); 425 - // unchanged references: 426 + expect(prevData.todos[0]).toBe(data.todos[0]); 426 427 expect(prevData.todos[1]).toBe(data.todos[1]); 427 428 expect(prevData.todos[2]).toBe(data.todos[2]); 428 429 });
+25 -19
exchanges/graphcache/src/operations/query.ts
··· 68 68 /** Reads a GraphQL query from the cache. 69 69 * @internal 70 70 */ 71 - export const query = ( 71 + export const __initAnd_query = ( 72 72 store: Store, 73 73 request: OperationRequest, 74 74 data?: Data | null | undefined, ··· 76 76 key?: number 77 77 ): QueryResult => { 78 78 initDataState('read', store.data, key); 79 - const result = read(store, request, data, error); 79 + const result = _query(store, request, data, error); 80 80 clearDataState(); 81 81 return result; 82 82 }; 83 83 84 - export const read = ( 84 + /** Reads a GraphQL query from the cache. 85 + * @internal 86 + */ 87 + export const _query = ( 85 88 store: Store, 86 89 request: OperationRequest, 87 90 input?: Data | null | undefined, ··· 105 108 pushDebugNode(rootKey, operation); 106 109 } 107 110 108 - if (!input) input = makeData(); 109 111 // NOTE: This may reuse "previous result data" as indicated by the 110 112 // `originalData` argument in readRoot(). This behaviour isn't used 111 113 // for readSelection() however, which always produces results from 112 114 // scratch 113 115 const data = 114 116 rootKey !== ctx.store.rootFields['query'] 115 - ? readRoot(ctx, rootKey, rootSelect, input) 116 - : readSelection(ctx, rootKey, rootSelect, input); 117 + ? readRoot(ctx, rootKey, rootSelect, input || makeData()) 118 + : readSelection(ctx, rootKey, rootSelect, input || makeData()); 117 119 118 120 if (process.env.NODE_ENV !== 'production') { 119 121 popDebugNode(); ··· 143 145 const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx); 144 146 145 147 let node: FieldNode | void; 146 - let hasChanged = false; 148 + let hasChanged = InMemoryData.currentForeignData; 147 149 const output = makeData(input); 148 150 while ((node = iterate())) { 149 151 const fieldAlias = getFieldAlias(node); ··· 181 183 ): Link<Data> => { 182 184 if (Array.isArray(originalData)) { 183 185 const newData = new Array(originalData.length); 184 - let hasChanged = false; 186 + let hasChanged = InMemoryData.currentForeignData; 185 187 for (let i = 0, l = originalData.length; i < l; i++) { 186 188 // Add the current index to the walked path before reading the field's value 187 189 ctx.__internal.path.push(i); ··· 208 210 } 209 211 }; 210 212 211 - export const readFragment = ( 213 + export const _queryFragment = ( 212 214 store: Store, 213 215 query: DocumentNode, 214 216 entity: Partial<Data> | string, ··· 337 339 let hasFields = false; 338 340 let hasPartials = false; 339 341 let hasNext = false; 340 - let hasChanged = typename !== input.__typename; 342 + let hasChanged = InMemoryData.currentForeignData; 341 343 let node: FieldNode | void; 342 344 const output = makeData(input); 343 345 while ((node = iterate()) !== undefined) { ··· 456 458 // Now that dataFieldValue has been retrieved it'll be set on data 457 459 // If it's uncached (undefined) but nullable we can continue assembling 458 460 // a partial query result 459 - if (dataFieldValue === undefined && deferRef.current) { 461 + if (dataFieldValue === undefined && deferRef) { 460 462 // The field is undelivered and uncached, but is included in a deferred fragment 461 463 hasNext = true; 462 464 } else if ( ··· 500 502 select: SelectionSet, 501 503 prevData: void | null | Data | Data[], 502 504 result: void | DataField, 503 - skipNull: boolean 505 + isOwnedData: boolean 504 506 ): DataField | void => { 505 507 if (Array.isArray(result)) { 506 508 const { store } = ctx; ··· 511 513 : false; 512 514 const data = new Array(result.length); 513 515 let hasChanged = 514 - !Array.isArray(prevData) || result.length !== prevData.length; 516 + !isOwnedData || 517 + !Array.isArray(prevData) || 518 + result.length !== prevData.length; 515 519 for (let i = 0, l = result.length; i < l; i++) { 516 520 // Add the current index to the walked path before reading the field's value 517 521 ctx.__internal.path.push(i); ··· 524 528 select, 525 529 prevData != null ? prevData[i] : undefined, 526 530 result[i], 527 - skipNull 531 + isOwnedData 528 532 ); 529 533 // After processing the field, remove the current index from the path 530 534 ctx.__internal.path.pop(); ··· 542 546 return hasChanged ? data : prevData; 543 547 } else if (result === null || result === undefined) { 544 548 return result; 545 - } else if (skipNull && prevData === null) { 549 + } else if (!isOwnedData && prevData === null) { 546 550 return null; 547 551 } else if (isDataOrKey(result)) { 548 552 const data = (prevData || makeData()) as Data; ··· 569 573 fieldName: string, 570 574 select: SelectionSet, 571 575 prevData: void | null | Data | Data[], 572 - skipNull: boolean 576 + isOwnedData: boolean 573 577 ): DataField | undefined => { 574 578 if (Array.isArray(link)) { 575 579 const { store } = ctx; ··· 578 582 : false; 579 583 const newLink = new Array(link.length); 580 584 let hasChanged = 581 - !Array.isArray(prevData) || newLink.length !== prevData.length; 585 + !isOwnedData || 586 + !Array.isArray(prevData) || 587 + newLink.length !== prevData.length; 582 588 for (let i = 0, l = link.length; i < l; i++) { 583 589 // Add the current index to the walked path before reading the field's value 584 590 ctx.__internal.path.push(i); ··· 590 596 fieldName, 591 597 select, 592 598 prevData != null ? prevData[i] : undefined, 593 - skipNull 599 + isOwnedData 594 600 ); 595 601 // After processing the field, remove the current index from the path 596 602 ctx.__internal.path.pop(); ··· 606 612 } 607 613 608 614 return hasChanged ? newLink : (prevData as Data[]); 609 - } else if (link === null || (prevData === null && skipNull)) { 615 + } else if (link === null || (prevData === null && isOwnedData)) { 610 616 return null; 611 617 } 612 618
+5 -6
exchanges/graphcache/src/operations/shared.ts
··· 51 51 }; 52 52 } 53 53 54 - export const contextRef: { current: Context | null } = { current: null }; 55 - export const deferRef: { current: boolean } = { current: false }; 54 + export let contextRef: Context | null = null; 55 + export let deferRef = false; 56 56 57 57 // Checks whether the current data field is a cache miss because of a GraphQLError 58 58 export const getFieldError = (ctx: Context): ErrorLike | undefined => ··· 110 110 fieldKey: string, 111 111 fieldName: string 112 112 ) => { 113 - contextRef.current = ctx; 113 + contextRef = ctx; 114 114 ctx.parent = data; 115 115 ctx.parentTypeName = typename; 116 116 ctx.parentKey = entityKey; ··· 168 168 let index = 0; 169 169 170 170 return function next() { 171 - if (!deferRef.current && childDeferred) deferRef.current = childDeferred; 171 + if (!deferRef && childDeferred) deferRef = childDeferred; 172 172 173 173 if (childIterator) { 174 174 const node = childIterator(); ··· 208 208 } 209 209 210 210 childDeferred = !!isDeferred(node, ctx.variables); 211 - if (!deferRef.current && childDeferred) 212 - deferRef.current = childDeferred; 211 + if (!deferRef && childDeferred) deferRef = childDeferred; 213 212 214 213 return (childIterator = makeSelectionIterator( 215 214 typename,
+1 -1
exchanges/graphcache/src/operations/write.test.ts
··· 4 4 import { minifyIntrospectionQuery } from '@urql/introspection'; 5 5 import { vi, expect, it, beforeEach, describe, beforeAll } from 'vitest'; 6 6 7 - import { write } from './write'; 7 + import { __initAnd_write as write } from './write'; 8 8 import * as InMemoryData from '../store/data'; 9 9 import { Store } from '../store'; 10 10
+17 -14
exchanges/graphcache/src/operations/write.ts
··· 39 39 clearDataState, 40 40 joinKeys, 41 41 keyOfField, 42 + makeData, 42 43 } from '../store'; 43 44 44 45 import * as InMemoryData from '../store/data'; ··· 61 62 /** Writes a GraphQL response to the cache. 62 63 * @internal 63 64 */ 64 - export const write = ( 65 + export const __initAnd_write = ( 65 66 store: Store, 66 67 request: OperationRequest, 67 68 data: Data, ··· 69 70 key?: number 70 71 ): WriteResult => { 71 72 initDataState('write', store.data, key || null); 72 - const result = startWrite(store, request, data, error); 73 + const result = _write(store, request, data, error); 73 74 clearDataState(); 74 75 return result; 75 76 }; 76 77 77 - export const writeOptimistic = ( 78 + export const __initAnd_writeOptimistic = ( 78 79 store: Store, 79 80 request: OperationRequest, 80 81 key: number ··· 89 90 } 90 91 91 92 initDataState('write', store.data, key, true); 92 - const result = startWrite(store, request, {} as Data, undefined, true); 93 + const result = _write(store, request, {} as Data, undefined); 93 94 clearDataState(); 94 95 return result; 95 96 }; 96 97 97 - export const startWrite = ( 98 + export const _write = ( 98 99 store: Store, 99 100 request: OperationRequest, 100 - data: Data, 101 - error?: CombinedError | undefined, 102 - isOptimistic?: boolean 101 + data?: Data, 102 + error?: CombinedError | undefined 103 103 ) => { 104 104 const operation = getMainOperation(request.query); 105 - const result: WriteResult = { data, dependencies: getCurrentDependencies() }; 105 + const result: WriteResult = { 106 + data: data || makeData(), 107 + dependencies: getCurrentDependencies(), 108 + }; 106 109 const kind = store.rootFields[operation.operation]; 107 110 108 111 const ctx = makeContext( ··· 111 114 getFragments(request.query), 112 115 kind, 113 116 kind, 114 - !!isOptimistic, 117 + InMemoryData.currentOptimistic, 115 118 error 116 119 ); 117 120 ··· 119 122 pushDebugNode(kind, operation); 120 123 } 121 124 122 - writeSelection(ctx, kind, getSelectionSet(operation), data); 125 + writeSelection(ctx, kind, getSelectionSet(operation), result.data!); 123 126 124 127 if (process.env.NODE_ENV !== 'production') { 125 128 popDebugNode(); ··· 128 131 return result; 129 132 }; 130 133 131 - export const writeFragment = ( 134 + export const _writeFragment = ( 132 135 store: Store, 133 136 query: DocumentNode, 134 137 data: Partial<Data>, ··· 245 248 if ( 246 249 rootField === 'query' && 247 250 fieldValue === undefined && 248 - !deferRef.current && 251 + !deferRef && 249 252 !ctx.optimistic 250 253 ) { 251 254 const expected = ··· 274 277 // Fields marked as deferred that aren't defined must be skipped 275 278 // Otherwise, we also ignore undefined values in optimistic updaters 276 279 (fieldValue === undefined && 277 - (deferRef.current || (ctx.optimistic && rootField === 'query'))) 280 + (deferRef || (ctx.optimistic && rootField === 'query'))) 278 281 ) { 279 282 continue; 280 283 }
+6 -3
exchanges/graphcache/src/store/data.ts
··· 63 63 let currentData: null | InMemoryData = null; 64 64 let currentDependencies: null | Dependencies = null; 65 65 let currentOptimisticKey: null | number = null; 66 - let currentOptimistic = false; 66 + export let currentForeignData = false; 67 + export let currentOptimistic = false; 67 68 68 69 /** Creates a new data object unless it's been created in this data run */ 69 70 export const makeData = (data?: Data): Data => { 70 71 let newData: Data; 71 72 if (data) { 72 73 if (currentOwnership!.has(data)) return data; 73 - newData = currentDataMapping!.get(data) || ({ ...data } as Data); 74 + newData = currentDataMapping!.get(data) || ({} as Data); 74 75 currentDataMapping!.set(data, newData); 75 76 } else { 76 77 newData = {} as Data; ··· 90 91 operationType: OperationType, 91 92 data: InMemoryData, 92 93 layerKey?: number | null, 93 - isOptimistic?: boolean 94 + isOptimistic?: boolean, 95 + isForeignData?: boolean 94 96 ) => { 95 97 currentOwnership = new WeakSet(); 96 98 currentDataMapping = new WeakMap(); ··· 98 100 currentData = data; 99 101 currentDependencies = new Set(); 100 102 currentOptimistic = !!isOptimistic; 103 + currentForeignData = !!isForeignData; 101 104 if (process.env.NODE_ENV !== 'production') { 102 105 currentDebugStack.length = 0; 103 106 }
+6 -2
exchanges/graphcache/src/store/store.test.ts
··· 12 12 13 13 import { Data, StorageAdapter } from '../types'; 14 14 import { makeContext, updateContext } from '../operations/shared'; 15 - import { query } from '../operations/query'; 16 - import { write, writeOptimistic } from '../operations/write'; 17 15 import * as InMemoryData from './data'; 18 16 import { Store } from './store'; 19 17 import { noop } from '../test-utils/utils'; 18 + 19 + import { __initAnd_query as query } from '../operations/query'; 20 + import { 21 + __initAnd_write as write, 22 + __initAnd_writeOptimistic as writeOptimistic, 23 + } from '../operations/write'; 20 24 21 25 const mocked = (x: any): any => x; 22 26
+7 -8
exchanges/graphcache/src/store/store.ts
··· 19 19 20 20 import { invariant } from '../helpers/help'; 21 21 import { contextRef, ensureLink } from '../operations/shared'; 22 - import { read, readFragment } from '../operations/query'; 23 - import { writeFragment, startWrite } from '../operations/write'; 22 + import { _query, _queryFragment } from '../operations/query'; 23 + import { _write, _writeFragment } from '../operations/write'; 24 24 import { invalidateEntity } from '../operations/invalidate'; 25 25 import { keyOfField } from './keys'; 26 26 import * as InMemoryData from './data'; ··· 107 107 // In resolvers and updaters we may have a specific parent 108 108 // object available that can be used to skip to a specific parent 109 109 // key directly without looking at its incomplete properties 110 - if (contextRef.current && data === contextRef.current.parent) 111 - return contextRef.current!.parentKey; 110 + if (contextRef && data === contextRef.parent) return contextRef.parentKey; 112 111 113 112 if (data == null || typeof data === 'string') return data || null; 114 113 if (!data.__typename) return null; ··· 167 166 request.query = formatDocument(request.query); 168 167 const output = updater(this.readQuery(request)); 169 168 if (output !== null) { 170 - startWrite(this, request, output as any); 169 + _write(this, request, output as any, undefined); 171 170 } 172 171 } 173 172 174 173 readQuery<T = Data, V = Variables>(input: QueryInput<T, V>): T | null { 175 174 const request = createRequest(input.query, input.variables!); 176 175 request.query = formatDocument(request.query); 177 - return read(this, request).data as T | null; 176 + return _query(this, request, undefined, undefined).data as T | null; 178 177 } 179 178 180 179 readFragment<T = Data, V = Variables>( ··· 183 182 variables?: V, 184 183 fragmentName?: string 185 184 ): T | null { 186 - return readFragment( 185 + return _queryFragment( 187 186 this, 188 187 formatDocument(fragment), 189 188 entity as Data, ··· 198 197 variables?: V, 199 198 fragmentName?: string 200 199 ): void { 201 - writeFragment( 200 + _writeFragment( 202 201 this, 203 202 formatDocument(fragment), 204 203 data as Data,
+5 -1
exchanges/graphcache/src/test-utils/examples-1.test.ts
··· 1 1 import { gql } from '@urql/core'; 2 2 import { it, expect, afterEach } from 'vitest'; 3 - import { query, write, writeOptimistic } from '../operations'; 3 + import { __initAnd_query as query } from '../operations/query'; 4 + import { 5 + __initAnd_write as write, 6 + __initAnd_writeOptimistic as writeOptimistic, 7 + } from '../operations/write'; 4 8 import * as InMemoryData from '../store/data'; 5 9 import { Store } from '../store'; 6 10 import { Data } from '../types';
+2 -1
exchanges/graphcache/src/test-utils/examples-2.test.ts
··· 1 1 import { gql } from '@urql/core'; 2 2 import { it, afterEach, expect } from 'vitest'; 3 - import { query, write } from '../operations'; 3 + import { __initAnd_query as query } from '../operations/query'; 4 + import { __initAnd_write as write } from '../operations/write'; 4 5 import { Store } from '../store'; 5 6 6 7 const Item = gql`
+2 -1
exchanges/graphcache/src/test-utils/examples-3.test.ts
··· 1 1 import { gql } from '@urql/core'; 2 2 import { it, afterEach, expect } from 'vitest'; 3 - import { query, write } from '../operations'; 3 + import { __initAnd_query as query } from '../operations/query'; 4 + import { __initAnd_write as write } from '../operations/write'; 4 5 import { Store } from '../store'; 5 6 6 7 afterEach(() => {
+2 -1
exchanges/graphcache/src/test-utils/suite.test.ts
··· 1 1 import { DocumentNode } from 'graphql'; 2 2 import { gql } from '@urql/core'; 3 3 import { it, expect } from 'vitest'; 4 - import { query, write } from '../operations'; 4 + import { __initAnd_query as query } from '../operations/query'; 5 + import { __initAnd_write as write } from '../operations/write'; 5 6 import { Store } from '../store'; 6 7 7 8 interface TestCase {