Repository: urql-graphql/urql (ts)

Issue:
Cached data bug after page navigation with next-urql + exchange-graphcache
### Describe the bug

I've created a [reproduction](https://codesandbox.io/p/sandbox/condescending-resonance-pndq8r) of an issue where useQuery is returning data with __typename fields before page navigation and the same data without __typename fields after navigating. All cached data returned from useQuery after any navigation has occurred on the site is missing __typenames, which isn't a problem in itself since we would never rely on those values without explicitly including that field in the query, but what is having a negative effect is that the cached data coming out of useQuery does not match the previous data by reference. And this is true on every render so when any other component on a page fetches data that touches the cache it results in rerendering everything everywhere. This is bad for performance and also is breaking part of our application where we are feeding an array of objects out of useQuery into a table component which replaces rows when object references change, resulting in any row state (selected checkboxes in our case) being wiped out and replaced with default state.

Our workaround at the moment is to always include __typename in all of our queries. This makes Graphcache behave predictably across page navigation and cached data is always returned with the same references as previous results so things are working as expected/desired. Of course we would rather not have to include this unnecessary field in all of our queries, and there's now a chance that team members may forget to do this and introduce subtle bugs and performance issues. Ideally the results from useQuery would either always include __typename or never include it, and in either case references should always match previous results.

In troubleshooting this we came across a number of issues/PRs that were somewhat related to our issue here, but not exactly, and I couldn't find anything calling out page navigation in Next causing this strange change in how Graphcache behaves. Here are some links here for reference:  #1268 #1366 #2701 #2718

### Reproduction

https://codesandbox.io/p/sandbox/condescending-resonance-pndq8r

### Urql version

urql v4.0.0
@urql/exchange-graphcache v6.0.1
next-urql v5.0.0

### Validations

- [X] I can confirm that this is a bug report, and not a feature request, RFC, question, or discussion, for which [GitHub Discussions](https://github.com/urql-graphql/urql/discussions) should be used
- [X] Read the [docs](https://formidable.com/open-source/urql/docs/).
- [X] Follow our [Code of Conduct](https://github.com/urql-graphql/urql/blob/main/CODE_OF_CONDUCT.md)

Pull request:
fix(graphcache): Improve separation of API and owned data
Resolves #3160

## Summary

This prevents us from:
- copying the incoming API data causing `__typename` to be applied to output data by default
- reusing incoming API data and treating it as equivalent to Graphcache’s owned data

In short, we don't differentiate in the `cacheExchange` whether we're passing `query()` previous Graphcache data or API data. This data is then used to diff against the new result, reusing as much of it as possible when it's unchanged. It's also used to complete the output data when some cache resolution fails, to fall back to API data.

However, we shouldn't reuse this data blindly when it's API data, since that data can slightly differ and carry `__typename` fields even when those aren't specified in the selection set.

## Set of changes

- Replace `originalVariables` with better `operations.set` tracking
- Stop copying original data when creating “owned” data
- Only reuse original data when it's previously “owned” data, and otherwise consider it changed
- Add “foreign data” flag to `initDataState`
- Remove operations helper usage in `cacheExchange` and intead call `initDataState` and `clearDataState` manually
- Clean up usage of `operations/*`


Patch:
diff --git a/.changeset/gentle-melons-march.md b/.changeset/gentle-melons-march.md
new file mode 100644
index 00000000..19cd8233
--- /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.ts b/exchanges/graphcache/src/cacheExchange.ts
index 2d26d7a6..eee2a6d5 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<OperationResult> {
@@ -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/index.ts b/exchanges/graphcache/src/index.ts
index 976af3eb..2ec62f7f 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/operations/index.ts b/exchanges/graphcache/src/operations/index.ts
deleted file mode 100644
index 71f31e89..00000000
--- 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.ts b/exchanges/graphcache/src/operations/query.ts
index 255f614b..f6346369 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<Data> => {
   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<Data> | 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 cd743d37..e24f0552 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.ts b/exchanges/graphcache/src/operations/write.ts
index eac90971..dc596d66 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<Data>,
@@ -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 7e0dcc38..cb4bc65a 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.ts b/exchanges/graphcache/src/store/store.ts
index 23b02dc6..a5789c00 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<T = Data, V = Variables>(input: QueryInput<T, V>): 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<T = Data, V = Variables>(
@@ -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,


Test patch:
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index 63e9320e..e2d16a54 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/extras/relayPagination.test.ts b/exchanges/graphcache/src/extras/relayPagination.test.ts
index 0cfe3b33..0445d52a 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 43f34ac7..5ed4c054 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/offlineExchange.test.ts b/exchanges/graphcache/src/offlineExchange.test.ts
index 413d4e95..3468f662 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/query.test.ts b/exchanges/graphcache/src/operations/query.test.ts
index 5dd4493f..feaac8e7 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/write.test.ts b/exchanges/graphcache/src/operations/write.test.ts
index daac0af5..bad199f9 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/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 76d14c15..a507a19e 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/test-utils/examples-1.test.ts b/exchanges/graphcache/src/test-utils/examples-1.test.ts
index c70b7b11..8a0ae98a 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 a61c3ba1..ebf4d39d 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 5784284e..c5cf0eda 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 ba12537c..e69a67a7 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 {