diff --git a/.changeset/gentle-melons-march.md b/.changeset/gentle-melons-march.md new file mode 100644 index 0000000000..19cd8233bf --- /dev/null +++ b/.changeset/gentle-melons-march.md @@ -0,0 +1,5 @@ +--- +'@urql/exchange-graphcache': patch +--- + +Prevent reusal of incoming API data in Graphcache’s produced (“owned”) data. This prevents us from copying the `__typename` and other superfluous fields. diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts index 63e9320e71..e2d16a5463 100644 --- a/exchanges/graphcache/src/cacheExchange.test.ts +++ b/exchanges/graphcache/src/cacheExchange.test.ts @@ -100,17 +100,12 @@ describe('data dependencies', () => { expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(2); - expect(result.mock.calls[0][0]).toHaveProperty( - 'operation.context.meta.cacheOutcome', - 'miss' - ); - expect(result.mock.calls[0][0].data).toEqual(expected); + expect(expected).toMatchObject(result.mock.calls[0][0].data); expect(result.mock.calls[1][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'hit' ); - expect(result.mock.calls[1][0].data).toEqual(expected); - + expect(expected).toMatchObject(result.mock.calls[1][0].data); expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); }); @@ -230,7 +225,7 @@ describe('data dependencies', () => { next(opMultiple); expect(response).toHaveBeenCalledTimes(2); - expect(reexec).toHaveBeenCalledWith(opOne); + expect(reexec.mock.calls[0][0]).toHaveProperty('key', opOne.key); expect(result).toHaveBeenCalledTimes(3); // test for reference reuse @@ -775,7 +770,6 @@ describe('optimistic updates', () => { expect(reexec).toHaveBeenCalledTimes(1); expect(result.mock.calls[1][0]?.data).toMatchObject({ - __typename: 'Query', author: { name: '[REDACTED OFFLINE]' }, }); @@ -1263,7 +1257,6 @@ describe('custom resolvers', () => { vi.runAllTimers(); expect(result.mock.calls[1][0].data).toEqual({ - __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', @@ -1429,7 +1422,6 @@ describe('custom resolvers', () => { expect(response).toHaveBeenCalledTimes(2); expect(fakeResolver).toHaveBeenCalledTimes(6); expect(result.mock.calls[1][0].data).toEqual({ - __typename: 'Mutation', concealAuthors: [ { __typename: 'Author', @@ -1583,10 +1575,6 @@ describe('schema awareness', () => { ], }); - expect(result.mock.calls[0][0]).not.toHaveProperty( - 'operation.context.meta' - ); - next(queryOperation); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(2); @@ -1725,10 +1713,6 @@ describe('schema awareness', () => { ], }); - expect(result.mock.calls[0][0]).not.toHaveProperty( - 'operation.context.meta' - ); - next(queryOperation); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(2); diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index 2d26d7a6c2..eee2a6d52a 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -20,10 +20,18 @@ import { Source, } from 'wonka'; -import { query, write, writeOptimistic } from './operations'; +import { _query } from './operations/query'; +import { _write } from './operations/write'; import { addMetadata, toRequestPolicy } from './helpers/operation'; import { filterVariables, getMainOperation } from './ast'; -import { Store, noopDataState, hydrateData, reserveLayer } from './store'; +import { + Store, + initDataState, + clearDataState, + noopDataState, + hydrateData, + reserveLayer, +} from './store'; import { Data, Dependencies, CacheExchangeOpts } from './types'; interface OperationResultWithMeta extends Partial { @@ -110,7 +118,6 @@ export const cacheExchange = if (op) { // Collect all dependent operations if the reexecuting operation is a query if (operation.kind === 'query') dependentOperations.add(key); - operations.delete(key); let policy: RequestPolicy = 'cache-first'; if (requestedRefetch.has(key)) { requestedRefetch.delete(key); @@ -135,6 +142,7 @@ export const cacheExchange = if (operation.kind === 'query') { // Pre-reserve the position of the result layer reserveLayer(store.data, operation.key); + operations.set(operation.key, operation); } else if (operation.kind === 'teardown') { // Delete reference to operation if any exists to release it operations.delete(operation.key); @@ -146,12 +154,11 @@ export const cacheExchange = operation.kind === 'mutation' && operation.context.requestPolicy !== 'network-only' ) { + operations.set(operation.key, operation); // This executes an optimistic update for mutations and registers it if necessary - const { dependencies } = writeOptimistic( - store, - operation, - operation.key - ); + initDataState('write', store.data, operation.key, true, false); + const { dependencies } = _write(store, operation, undefined, undefined); + clearDataState(); if (dependencies.size) { // Update blocked optimistic dependencies for (const dep of dependencies.values()) blockedDependencies.add(dep); @@ -178,7 +185,7 @@ export const cacheExchange = ) : operation.variables, }, - { ...operation.context, originalVariables: operation.variables } + operation.context ); }; @@ -196,7 +203,14 @@ export const cacheExchange = const operationResultFromCache = ( operation: Operation ): OperationResultWithMeta => { - const result = query(store, operation, results.get(operation.key)); + initDataState('read', store.data, undefined, false, false); + const result = _query( + store, + operation, + results.get(operation.key), + undefined + ); + clearDataState(); const cacheOutcome: CacheOutcome = result.data ? !result.partial && !result.hasNext ? 'hit' @@ -204,7 +218,6 @@ export const cacheExchange = : 'miss'; results.set(operation.key, result.data); - operations.set(operation.key, operation); updateDependencies(operation, result.dependencies); return { @@ -222,21 +235,9 @@ export const cacheExchange = pendingOperations: Operations ): OperationResult => { // Retrieve the original operation to remove changes made by formatDocument - const originalOperation = operations.get(result.operation.key); - const operation = originalOperation - ? makeOperation( - originalOperation.kind, - originalOperation, - result.operation.context - ) - : result.operation; - + const operation = + operations.get(result.operation.key) || result.operation; if (operation.kind === 'mutation') { - if (result.operation.context.originalVariables) { - operation.variables = result.operation.context.originalVariables; - delete result.operation.context.originalVariables; - } - // Collect previous dependencies that have been written for optimistic updates const dependencies = optimisticKeysToDependencies.get(operation.key); collectPendingOperations(pendingOperations, dependencies); @@ -251,25 +252,31 @@ export const cacheExchange = if (data) { // Write the result to cache and collect all dependencies that need to be // updated - const writeDependencies = write( + initDataState('write', store.data, operation.key, false, false); + const writeDependencies = _write( store, operation, data, - result.error, - operation.key + result.error ).dependencies; + clearDataState(); collectPendingOperations(pendingOperations, writeDependencies); - - const queryResult = query( + const prevData = + operation.kind === 'query' ? results.get(operation.key) : null; + initDataState( + 'read', + store.data, + operation.key, + false, + prevData !== data + ); + const queryResult = _query( store, operation, - operation.kind === 'query' - ? results.get(operation.key) || data - : data, - result.error, - operation.key + prevData || data, + result.error ); - + clearDataState(); data = queryResult.data; if (operation.kind === 'query') { // Collect the query's dependencies for future pending operation updates @@ -283,7 +290,6 @@ export const cacheExchange = // Update this operation's dependencies if it's a query if (queryDependencies) { - operations.set(operation.key, operation); updateDependencies(result.operation, queryDependencies); } @@ -374,7 +380,10 @@ export const cacheExchange = /*noop*/ } else if (!isBlockedByOptimisticUpdate(res.dependencies)) { client.reexecuteOperation( - toRequestPolicy(res.operation, 'network-only') + toRequestPolicy( + operations.get(res.operation.key) || res.operation, + 'network-only' + ) ); } else if (requestPolicy === 'cache-and-network') { requestedRefetch.add(res.operation.key); diff --git a/exchanges/graphcache/src/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts index 0cfe3b3378..0445d52ae1 100644 --- a/exchanges/graphcache/src/extras/relayPagination.test.ts +++ b/exchanges/graphcache/src/extras/relayPagination.test.ts @@ -1,6 +1,7 @@ import { gql } from '@urql/core'; import { it, expect } from 'vitest'; -import { query, write } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store'; import { relayPagination } from './relayPagination'; diff --git a/exchanges/graphcache/src/extras/simplePagination.test.ts b/exchanges/graphcache/src/extras/simplePagination.test.ts index 43f34ac748..5ed4c0546d 100644 --- a/exchanges/graphcache/src/extras/simplePagination.test.ts +++ b/exchanges/graphcache/src/extras/simplePagination.test.ts @@ -1,6 +1,7 @@ import { gql } from '@urql/core'; import { it, expect } from 'vitest'; -import { query, write } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store'; import { simplePagination } from './simplePagination'; diff --git a/exchanges/graphcache/src/index.ts b/exchanges/graphcache/src/index.ts index 976af3eb57..2ec62f7ff0 100644 --- a/exchanges/graphcache/src/index.ts +++ b/exchanges/graphcache/src/index.ts @@ -1,5 +1,4 @@ export * from './types'; -export { query, write } from './operations'; -export { Store } from './store'; +export type { Store } from './store'; export { cacheExchange } from './cacheExchange'; export { offlineExchange } from './offlineExchange'; diff --git a/exchanges/graphcache/src/offlineExchange.test.ts b/exchanges/graphcache/src/offlineExchange.test.ts index 413d4e9585..3468f6626b 100644 --- a/exchanges/graphcache/src/offlineExchange.test.ts +++ b/exchanges/graphcache/src/offlineExchange.test.ts @@ -160,7 +160,7 @@ describe('offline', () => { next(queryOp); expect(result).toBeCalledTimes(1); - expect(result.mock.calls[0][0].data).toMatchObject(queryOneData); + expect(queryOneData).toMatchObject(result.mock.calls[0][0].data); next(mutationOp); expect(result).toBeCalledTimes(1); diff --git a/exchanges/graphcache/src/operations/index.ts b/exchanges/graphcache/src/operations/index.ts deleted file mode 100644 index 71f31e89b3..0000000000 --- a/exchanges/graphcache/src/operations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { query, read } from './query'; -export { write, writeOptimistic, writeFragment } from './write'; diff --git a/exchanges/graphcache/src/operations/query.test.ts b/exchanges/graphcache/src/operations/query.test.ts index 5dd4493fe9..feaac8e78e 100644 --- a/exchanges/graphcache/src/operations/query.test.ts +++ b/exchanges/graphcache/src/operations/query.test.ts @@ -5,8 +5,8 @@ import { minifyIntrospectionQuery } from '@urql/introspection'; import { describe, it, beforeEach, beforeAll, expect } from 'vitest'; import { Store } from '../store'; -import { write } from './write'; -import { query } from './query'; +import { __initAnd_write as write } from './write'; +import { __initAnd_query as query } from './query'; const TODO_QUERY = gql` query Todos { @@ -363,7 +363,7 @@ describe('Query', () => { expect(previousData).toHaveProperty('todos.0.textB', 'old'); }); - it('should not keep references stable', () => { + it('should keep references stable', () => { const QUERY = gql` query todos { __typename @@ -398,31 +398,32 @@ describe('Query', () => { write(store, { query: QUERY }, expected); - const prevData = { - todos: [ - { - __typename: 'Todo', - id: 'prev-0', - }, - { - __typename: 'Todo', - id: '1', - }, - { - __typename: 'Todo', - id: '2', - }, - ], - __typename: 'query_root', - }; + const prevData = query( + store, + { query: QUERY }, + { + todos: [ + { + __typename: 'Todo', + id: 'prev-0', + }, + { + __typename: 'Todo', + id: '1', + }, + { + __typename: 'Todo', + id: '2', + }, + ], + __typename: 'query_root', + } + ).data as any; - const data = query(store, { query: QUERY }, prevData) - .data as typeof expected; + const data = query(store, { query: QUERY }, prevData).data as any; expect(data).toEqual(expected); - expect(prevData.todos[0]).not.toEqual(data.todos[0]); - expect(prevData.todos[0]).not.toBe(data.todos[0]); - // unchanged references: + expect(prevData.todos[0]).toBe(data.todos[0]); expect(prevData.todos[1]).toBe(data.todos[1]); expect(prevData.todos[2]).toBe(data.todos[2]); }); diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts index 255f614bc8..f6346369bd 100644 --- a/exchanges/graphcache/src/operations/query.ts +++ b/exchanges/graphcache/src/operations/query.ts @@ -68,7 +68,7 @@ export interface QueryResult { /** Reads a GraphQL query from the cache. * @internal */ -export const query = ( +export const __initAnd_query = ( store: Store, request: OperationRequest, data?: Data | null | undefined, @@ -76,12 +76,15 @@ export const query = ( key?: number ): QueryResult => { initDataState('read', store.data, key); - const result = read(store, request, data, error); + const result = _query(store, request, data, error); clearDataState(); return result; }; -export const read = ( +/** Reads a GraphQL query from the cache. + * @internal + */ +export const _query = ( store: Store, request: OperationRequest, input?: Data | null | undefined, @@ -105,15 +108,14 @@ export const read = ( pushDebugNode(rootKey, operation); } - if (!input) input = makeData(); // NOTE: This may reuse "previous result data" as indicated by the // `originalData` argument in readRoot(). This behaviour isn't used // for readSelection() however, which always produces results from // scratch const data = rootKey !== ctx.store.rootFields['query'] - ? readRoot(ctx, rootKey, rootSelect, input) - : readSelection(ctx, rootKey, rootSelect, input); + ? readRoot(ctx, rootKey, rootSelect, input || makeData()) + : readSelection(ctx, rootKey, rootSelect, input || makeData()); if (process.env.NODE_ENV !== 'production') { popDebugNode(); @@ -143,7 +145,7 @@ const readRoot = ( const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx); let node: FieldNode | void; - let hasChanged = false; + let hasChanged = InMemoryData.currentForeignData; const output = makeData(input); while ((node = iterate())) { const fieldAlias = getFieldAlias(node); @@ -181,7 +183,7 @@ const readRootField = ( ): Link => { if (Array.isArray(originalData)) { const newData = new Array(originalData.length); - let hasChanged = false; + let hasChanged = InMemoryData.currentForeignData; for (let i = 0, l = originalData.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); @@ -208,7 +210,7 @@ const readRootField = ( } }; -export const readFragment = ( +export const _queryFragment = ( store: Store, query: DocumentNode, entity: Partial | string, @@ -337,7 +339,7 @@ const readSelection = ( let hasFields = false; let hasPartials = false; let hasNext = false; - let hasChanged = typename !== input.__typename; + let hasChanged = InMemoryData.currentForeignData; let node: FieldNode | void; const output = makeData(input); while ((node = iterate()) !== undefined) { @@ -456,7 +458,7 @@ const readSelection = ( // Now that dataFieldValue has been retrieved it'll be set on data // If it's uncached (undefined) but nullable we can continue assembling // a partial query result - if (dataFieldValue === undefined && deferRef.current) { + if (dataFieldValue === undefined && deferRef) { // The field is undelivered and uncached, but is included in a deferred fragment hasNext = true; } else if ( @@ -500,7 +502,7 @@ const resolveResolverResult = ( select: SelectionSet, prevData: void | null | Data | Data[], result: void | DataField, - skipNull: boolean + isOwnedData: boolean ): DataField | void => { if (Array.isArray(result)) { const { store } = ctx; @@ -511,7 +513,9 @@ const resolveResolverResult = ( : false; const data = new Array(result.length); let hasChanged = - !Array.isArray(prevData) || result.length !== prevData.length; + !isOwnedData || + !Array.isArray(prevData) || + result.length !== prevData.length; for (let i = 0, l = result.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); @@ -524,7 +528,7 @@ const resolveResolverResult = ( select, prevData != null ? prevData[i] : undefined, result[i], - skipNull + isOwnedData ); // After processing the field, remove the current index from the path ctx.__internal.path.pop(); @@ -542,7 +546,7 @@ const resolveResolverResult = ( return hasChanged ? data : prevData; } else if (result === null || result === undefined) { return result; - } else if (skipNull && prevData === null) { + } else if (!isOwnedData && prevData === null) { return null; } else if (isDataOrKey(result)) { const data = (prevData || makeData()) as Data; @@ -569,7 +573,7 @@ const resolveLink = ( fieldName: string, select: SelectionSet, prevData: void | null | Data | Data[], - skipNull: boolean + isOwnedData: boolean ): DataField | undefined => { if (Array.isArray(link)) { const { store } = ctx; @@ -578,7 +582,9 @@ const resolveLink = ( : false; const newLink = new Array(link.length); let hasChanged = - !Array.isArray(prevData) || newLink.length !== prevData.length; + !isOwnedData || + !Array.isArray(prevData) || + newLink.length !== prevData.length; for (let i = 0, l = link.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); @@ -590,7 +596,7 @@ const resolveLink = ( fieldName, select, prevData != null ? prevData[i] : undefined, - skipNull + isOwnedData ); // After processing the field, remove the current index from the path ctx.__internal.path.pop(); @@ -606,7 +612,7 @@ const resolveLink = ( } return hasChanged ? newLink : (prevData as Data[]); - } else if (link === null || (prevData === null && skipNull)) { + } else if (link === null || (prevData === null && isOwnedData)) { return null; } diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts index cd743d3774..e24f0552e4 100644 --- a/exchanges/graphcache/src/operations/shared.ts +++ b/exchanges/graphcache/src/operations/shared.ts @@ -51,8 +51,8 @@ export interface Context { }; } -export const contextRef: { current: Context | null } = { current: null }; -export const deferRef: { current: boolean } = { current: false }; +export let contextRef: Context | null = null; +export let deferRef = false; // Checks whether the current data field is a cache miss because of a GraphQLError export const getFieldError = (ctx: Context): ErrorLike | undefined => @@ -110,7 +110,7 @@ export const updateContext = ( fieldKey: string, fieldName: string ) => { - contextRef.current = ctx; + contextRef = ctx; ctx.parent = data; ctx.parentTypeName = typename; ctx.parentKey = entityKey; @@ -168,7 +168,7 @@ export const makeSelectionIterator = ( let index = 0; return function next() { - if (!deferRef.current && childDeferred) deferRef.current = childDeferred; + if (!deferRef && childDeferred) deferRef = childDeferred; if (childIterator) { const node = childIterator(); @@ -208,8 +208,7 @@ export const makeSelectionIterator = ( } childDeferred = !!isDeferred(node, ctx.variables); - if (!deferRef.current && childDeferred) - deferRef.current = childDeferred; + if (!deferRef && childDeferred) deferRef = childDeferred; return (childIterator = makeSelectionIterator( typename, diff --git a/exchanges/graphcache/src/operations/write.test.ts b/exchanges/graphcache/src/operations/write.test.ts index daac0af5d3..bad199f9f8 100644 --- a/exchanges/graphcache/src/operations/write.test.ts +++ b/exchanges/graphcache/src/operations/write.test.ts @@ -4,7 +4,7 @@ import { gql, CombinedError } from '@urql/core'; import { minifyIntrospectionQuery } from '@urql/introspection'; import { vi, expect, it, beforeEach, describe, beforeAll } from 'vitest'; -import { write } from './write'; +import { __initAnd_write as write } from './write'; import * as InMemoryData from '../store/data'; import { Store } from '../store'; diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts index eac9097164..dc596d6638 100644 --- a/exchanges/graphcache/src/operations/write.ts +++ b/exchanges/graphcache/src/operations/write.ts @@ -39,6 +39,7 @@ import { clearDataState, joinKeys, keyOfField, + makeData, } from '../store'; import * as InMemoryData from '../store/data'; @@ -61,7 +62,7 @@ export interface WriteResult { /** Writes a GraphQL response to the cache. * @internal */ -export const write = ( +export const __initAnd_write = ( store: Store, request: OperationRequest, data: Data, @@ -69,12 +70,12 @@ export const write = ( key?: number ): WriteResult => { initDataState('write', store.data, key || null); - const result = startWrite(store, request, data, error); + const result = _write(store, request, data, error); clearDataState(); return result; }; -export const writeOptimistic = ( +export const __initAnd_writeOptimistic = ( store: Store, request: OperationRequest, key: number @@ -89,20 +90,22 @@ export const writeOptimistic = ( } initDataState('write', store.data, key, true); - const result = startWrite(store, request, {} as Data, undefined, true); + const result = _write(store, request, {} as Data, undefined); clearDataState(); return result; }; -export const startWrite = ( +export const _write = ( store: Store, request: OperationRequest, - data: Data, - error?: CombinedError | undefined, - isOptimistic?: boolean + data?: Data, + error?: CombinedError | undefined ) => { const operation = getMainOperation(request.query); - const result: WriteResult = { data, dependencies: getCurrentDependencies() }; + const result: WriteResult = { + data: data || makeData(), + dependencies: getCurrentDependencies(), + }; const kind = store.rootFields[operation.operation]; const ctx = makeContext( @@ -111,7 +114,7 @@ export const startWrite = ( getFragments(request.query), kind, kind, - !!isOptimistic, + InMemoryData.currentOptimistic, error ); @@ -119,7 +122,7 @@ export const startWrite = ( pushDebugNode(kind, operation); } - writeSelection(ctx, kind, getSelectionSet(operation), data); + writeSelection(ctx, kind, getSelectionSet(operation), result.data!); if (process.env.NODE_ENV !== 'production') { popDebugNode(); @@ -128,7 +131,7 @@ export const startWrite = ( return result; }; -export const writeFragment = ( +export const _writeFragment = ( store: Store, query: DocumentNode, data: Partial, @@ -245,7 +248,7 @@ const writeSelection = ( if ( rootField === 'query' && fieldValue === undefined && - !deferRef.current && + !deferRef && !ctx.optimistic ) { const expected = @@ -274,7 +277,7 @@ const writeSelection = ( // Fields marked as deferred that aren't defined must be skipped // Otherwise, we also ignore undefined values in optimistic updaters (fieldValue === undefined && - (deferRef.current || (ctx.optimistic && rootField === 'query'))) + (deferRef || (ctx.optimistic && rootField === 'query'))) ) { continue; } diff --git a/exchanges/graphcache/src/store/data.ts b/exchanges/graphcache/src/store/data.ts index 7e0dcc3851..cb4bc65a5b 100644 --- a/exchanges/graphcache/src/store/data.ts +++ b/exchanges/graphcache/src/store/data.ts @@ -63,14 +63,15 @@ let currentOperation: null | OperationType = null; let currentData: null | InMemoryData = null; let currentDependencies: null | Dependencies = null; let currentOptimisticKey: null | number = null; -let currentOptimistic = false; +export let currentForeignData = false; +export let currentOptimistic = false; /** Creates a new data object unless it's been created in this data run */ export const makeData = (data?: Data): Data => { let newData: Data; if (data) { if (currentOwnership!.has(data)) return data; - newData = currentDataMapping!.get(data) || ({ ...data } as Data); + newData = currentDataMapping!.get(data) || ({} as Data); currentDataMapping!.set(data, newData); } else { newData = {} as Data; @@ -90,7 +91,8 @@ export const initDataState = ( operationType: OperationType, data: InMemoryData, layerKey?: number | null, - isOptimistic?: boolean + isOptimistic?: boolean, + isForeignData?: boolean ) => { currentOwnership = new WeakSet(); currentDataMapping = new WeakMap(); @@ -98,6 +100,7 @@ export const initDataState = ( currentData = data; currentDependencies = new Set(); currentOptimistic = !!isOptimistic; + currentForeignData = !!isForeignData; if (process.env.NODE_ENV !== 'production') { currentDebugStack.length = 0; } diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts index 76d14c156c..a507a19ed6 100644 --- a/exchanges/graphcache/src/store/store.test.ts +++ b/exchanges/graphcache/src/store/store.test.ts @@ -12,12 +12,16 @@ import { import { Data, StorageAdapter } from '../types'; import { makeContext, updateContext } from '../operations/shared'; -import { query } from '../operations/query'; -import { write, writeOptimistic } from '../operations/write'; import * as InMemoryData from './data'; import { Store } from './store'; import { noop } from '../test-utils/utils'; +import { __initAnd_query as query } from '../operations/query'; +import { + __initAnd_write as write, + __initAnd_writeOptimistic as writeOptimistic, +} from '../operations/write'; + const mocked = (x: any): any => x; const Appointment = gql` diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts index 23b02dc6a0..a5789c0080 100644 --- a/exchanges/graphcache/src/store/store.ts +++ b/exchanges/graphcache/src/store/store.ts @@ -19,8 +19,8 @@ import { import { invariant } from '../helpers/help'; import { contextRef, ensureLink } from '../operations/shared'; -import { read, readFragment } from '../operations/query'; -import { writeFragment, startWrite } from '../operations/write'; +import { _query, _queryFragment } from '../operations/query'; +import { _write, _writeFragment } from '../operations/write'; import { invalidateEntity } from '../operations/invalidate'; import { keyOfField } from './keys'; import * as InMemoryData from './data'; @@ -107,8 +107,7 @@ export class Store< // In resolvers and updaters we may have a specific parent // object available that can be used to skip to a specific parent // key directly without looking at its incomplete properties - if (contextRef.current && data === contextRef.current.parent) - return contextRef.current!.parentKey; + if (contextRef && data === contextRef.parent) return contextRef.parentKey; if (data == null || typeof data === 'string') return data || null; if (!data.__typename) return null; @@ -167,14 +166,14 @@ export class Store< request.query = formatDocument(request.query); const output = updater(this.readQuery(request)); if (output !== null) { - startWrite(this, request, output as any); + _write(this, request, output as any, undefined); } } readQuery(input: QueryInput): T | null { const request = createRequest(input.query, input.variables!); request.query = formatDocument(request.query); - return read(this, request).data as T | null; + return _query(this, request, undefined, undefined).data as T | null; } readFragment( @@ -183,7 +182,7 @@ export class Store< variables?: V, fragmentName?: string ): T | null { - return readFragment( + return _queryFragment( this, formatDocument(fragment), entity as Data, @@ -198,7 +197,7 @@ export class Store< variables?: V, fragmentName?: string ): void { - writeFragment( + _writeFragment( this, formatDocument(fragment), data as Data, diff --git a/exchanges/graphcache/src/test-utils/examples-1.test.ts b/exchanges/graphcache/src/test-utils/examples-1.test.ts index c70b7b118b..8a0ae98a24 100644 --- a/exchanges/graphcache/src/test-utils/examples-1.test.ts +++ b/exchanges/graphcache/src/test-utils/examples-1.test.ts @@ -1,6 +1,10 @@ import { gql } from '@urql/core'; import { it, expect, afterEach } from 'vitest'; -import { query, write, writeOptimistic } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { + __initAnd_write as write, + __initAnd_writeOptimistic as writeOptimistic, +} from '../operations/write'; import * as InMemoryData from '../store/data'; import { Store } from '../store'; import { Data } from '../types'; diff --git a/exchanges/graphcache/src/test-utils/examples-2.test.ts b/exchanges/graphcache/src/test-utils/examples-2.test.ts index a61c3ba172..ebf4d39dac 100644 --- a/exchanges/graphcache/src/test-utils/examples-2.test.ts +++ b/exchanges/graphcache/src/test-utils/examples-2.test.ts @@ -1,6 +1,7 @@ import { gql } from '@urql/core'; import { it, afterEach, expect } from 'vitest'; -import { query, write } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store'; const Item = gql` diff --git a/exchanges/graphcache/src/test-utils/examples-3.test.ts b/exchanges/graphcache/src/test-utils/examples-3.test.ts index 5784284ed1..c5cf0edad3 100644 --- a/exchanges/graphcache/src/test-utils/examples-3.test.ts +++ b/exchanges/graphcache/src/test-utils/examples-3.test.ts @@ -1,6 +1,7 @@ import { gql } from '@urql/core'; import { it, afterEach, expect } from 'vitest'; -import { query, write } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store'; afterEach(() => { diff --git a/exchanges/graphcache/src/test-utils/suite.test.ts b/exchanges/graphcache/src/test-utils/suite.test.ts index ba12537cb9..e69a67a7fc 100644 --- a/exchanges/graphcache/src/test-utils/suite.test.ts +++ b/exchanges/graphcache/src/test-utils/suite.test.ts @@ -1,7 +1,8 @@ import { DocumentNode } from 'graphql'; import { gql } from '@urql/core'; import { it, expect } from 'vitest'; -import { query, write } from '../operations'; +import { __initAnd_query as query } from '../operations/query'; +import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store'; interface TestCase {