Repository: urql-graphql/urql (ts)

Issue:
Using `graphcache@5.0.0` or 6 the cache in the default storage is not loaded anymore on the first page load
After upgrading from 4 to 5 (or 6) `@urql/exchange-graphcache` doing some tests with a fake delay on the backend I discovered that when I refresh the page (F5 or Ctrl+R) the cache present in the IndexedDB is not used.

**The REPL** (with SvelteKit too) is here: https://codesandbox.io/p/sandbox/romantic-feather-xmc826

**Steps**:

1. Go to https://xmc826-5173.csb.app/todos

1. the todos are downloaded, showed and stored in the IndexedDB after 3 seconds of [fake delay on the server resolver](https://codesandbox.io/p/sandbox/apollo-server-urql-svelte-crud-h1pcl?file=%2Fschema.js&selection=%5B%7B%22endColumn%22%3A25%2C%22endLineNumber%22%3A132%2C%22startColumn%22%3A7%2C%22startLineNumber%22%3A132%7D%5D)

1. Click on the reload button in the navbar (or the browser's one)

1. The "Loading listStore..." message appears during the 3 fake delay seconds

1. I expect instead to see the todos immediately (during the `cache-and-network` call which is still happening)

If I rollback only `"@urql/exchange-graphcache"` to `"4.4.3"` this "cold cache" behavior works again.

**What could be causing the problem?**

Thank you in advance.

### Urql version

[package.json](https://codesandbox.io/p/sandbox/romantic-feather-xmc826?selection=%5B%7B%22endColumn%22%3A13%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A13%2C%22startLineNumber%22%3A19%7D%5D&file=%2Fpackage.json)


### 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 description:
fix(graphcache): Prevent offlineExchange from issuing duplicate operations
Follow-up to #3199
Resolves #3093

## Set of changes

- Add a crude teardown+seen routine to `failedQueue` in `offlineExchange`'s `flushQueue`


Patch:
diff --git a/.changeset/breezy-melons-deny.md b/.changeset/breezy-melons-deny.md
new file mode 100644
index 00000000..cc2f9dbb
--- /dev/null
+++ b/.changeset/breezy-melons-deny.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Respect `additionalTypenames` on subscriptions and re-execute queries for them as well, as one would intuitively expect.
diff --git a/.changeset/curvy-poems-compare.md b/.changeset/curvy-poems-compare.md
new file mode 100644
index 00000000..8faaf8db
--- /dev/null
+++ b/.changeset/curvy-poems-compare.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-retry': minor
+---
+
+Reset `retryExchange`’s previous attempts and delay if an operation succeeds. This prevents the exchange from keeping its old retry count and delay if the operation delivered a result in the meantime. This is important for it to help recover from failing subscriptions.
diff --git a/.changeset/hungry-cars-hide.md b/.changeset/hungry-cars-hide.md
new file mode 100644
index 00000000..abe1679a
--- /dev/null
+++ b/.changeset/hungry-cars-hide.md
@@ -0,0 +1,5 @@
+---
+'@urql/vue': patch
+---
+
+Prevent multiple operations being executed in a row when multiple inputs change simultaneously (e.g. `isPaused` and query inputs)
diff --git a/.changeset/hungry-zebras-count.md b/.changeset/hungry-zebras-count.md
new file mode 100644
index 00000000..0485a67e
--- /dev/null
+++ b/.changeset/hungry-zebras-count.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Prevent `offlineExchange` from issuing duplicate operations.
diff --git a/.changeset/spicy-comics-shave.md b/.changeset/spicy-comics-shave.md
new file mode 100644
index 00000000..9b9b3c73
--- /dev/null
+++ b/.changeset/spicy-comics-shave.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': patch
+---
+
+Fix reference equality not being preserved. This is a fix on top of [#3165](https://github.com/urql-graphql/urql/pull/3165), and was previously not addressed to avoid having to test for corner cases that are hard to cover. If you experience issues with this fix, please let us know.
diff --git a/exchanges/graphcache/src/offlineExchange.ts b/exchanges/graphcache/src/offlineExchange.ts
index f0d9a832..194421f0 100644
--- a/exchanges/graphcache/src/offlineExchange.ts
+++ b/exchanges/graphcache/src/offlineExchange.ts
@@ -151,11 +151,16 @@ export const offlineExchange =
       const flushQueue = () => {
         if (!isFlushingQueue) {
           isFlushingQueue = true;
+
+          const sent = new Set<number>();
           for (let i = 0; i < failedQueue.length; i++) {
             const operation = failedQueue[i];
-            if (operation.kind === 'mutation')
-              next(makeOperation('teardown', operation));
-            next(operation);
+            if (operation.kind === 'mutation' || !sent.has(operation.key)) {
+              if (operation.kind !== 'subscription')
+                next(makeOperation('teardown', operation));
+              sent.add(operation.key);
+              next(toRequestPolicy(operation, 'cache-first'));
+            }
           }
 
           failedQueue.length = 0;
diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts
index 83f3cf69..cf4d2b5f 100644
--- a/exchanges/graphcache/src/operations/query.ts
+++ b/exchanges/graphcache/src/operations/query.ts
@@ -523,9 +523,9 @@ const resolveResolverResult = (
     const _isListNullable = store.schema
       ? isListNullable(store.schema, typename, fieldName)
       : false;
-    const data = new Array(result.length);
+    const data = InMemoryData.makeData(prevData, true);
     let hasChanged =
-      !isOwnedData ||
+      InMemoryData.currentForeignData ||
       !Array.isArray(prevData) ||
       result.length !== prevData.length;
     for (let i = 0, l = result.length; i < l; i++) {
@@ -561,7 +561,7 @@ const resolveResolverResult = (
   } else if (!isOwnedData && prevData === null) {
     return null;
   } else if (isDataOrKey(result)) {
-    const data = (prevData || InMemoryData.makeData()) as Data;
+    const data = (prevData || InMemoryData.makeData(prevData)) as Data;
     return typeof result === 'string'
       ? readSelection(ctx, result, select, data)
       : readSelection(ctx, key, select, data, result);
@@ -592,11 +592,11 @@ const resolveLink = (
     const _isListNullable = store.schema
       ? isListNullable(store.schema, typename, fieldName)
       : false;
-    const newLink = new Array(link.length);
+    const newLink = InMemoryData.makeData(prevData, true);
     let hasChanged =
-      !isOwnedData ||
+      InMemoryData.currentForeignData ||
       !Array.isArray(prevData) ||
-      newLink.length !== prevData.length;
+      link.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);
@@ -632,7 +632,7 @@ const resolveLink = (
     ctx,
     link,
     select,
-    (prevData || InMemoryData.makeData()) as Data
+    (prevData || InMemoryData.makeData(prevData)) as Data
   );
 };
 
diff --git a/exchanges/graphcache/src/store/data.ts b/exchanges/graphcache/src/store/data.ts
index 6d936303..7346eb74 100644
--- a/exchanges/graphcache/src/store/data.ts
+++ b/exchanges/graphcache/src/store/data.ts
@@ -8,6 +8,7 @@ import {
   SerializedEntries,
   Dependencies,
   OperationType,
+  DataField,
   Data,
 } from '../types';
 
@@ -59,8 +60,8 @@ export interface InMemoryData {
   storage: StorageAdapter | null;
 }
 
-let currentOwnership: null | WeakSet<Data> = null;
-let currentDataMapping: null | WeakMap<Data, Data> = null;
+let currentOwnership: null | WeakSet<any> = null;
+let currentDataMapping: null | WeakMap<any, any> = null;
 let currentData: null | InMemoryData = null;
 let currentOptimisticKey: null | number = null;
 export let currentOperation: null | OperationType = null;
@@ -68,20 +69,28 @@ export let currentDependencies: null | Dependencies = null;
 export let currentForeignData = false;
 export let currentOptimistic = false;
 
+export function makeData(data: DataField | void, isArray?: false): Data;
+export function makeData(data: DataField | void, isArray: true): DataField[];
+
 /** Creates a new data object unless it's been created in this data run */
-export const makeData = (data?: Data): Data => {
-  let newData: Data;
+export function makeData(data?: DataField | void, isArray?: boolean) {
+  let newData: Data | Data[] | undefined;
   if (data) {
     if (currentOwnership!.has(data)) return data;
-    newData = currentDataMapping!.get(data) || ({} as Data);
+    newData = currentDataMapping!.get(data) as any;
+  }
+
+  if (newData == null) {
+    newData = (isArray ? [] : {}) as any;
+  }
+
+  if (data) {
     currentDataMapping!.set(data, newData);
-  } else {
-    newData = {} as Data;
   }
 
   currentOwnership!.add(newData);
   return newData;
-};
+}
 
 export const ownsData = (data?: Data): boolean =>
   !!data && currentOwnership!.has(data);
diff --git a/exchanges/retry/src/retryExchange.ts b/exchanges/retry/src/retryExchange.ts
index 9a0d3936..aafff0fe 100644
--- a/exchanges/retry/src/retryExchange.ts
+++ b/exchanges/retry/src/retryExchange.ts
@@ -78,6 +78,11 @@ export interface RetryExchangeOptions {
   ): Operation | null | undefined;
 }
 
+interface RetryState {
+  count: number;
+  delay: number | null;
+}
+
 /** Exchange factory that retries failed operations.
  *
  * @param options - A {@link RetriesExchangeOptions} configuration object.
@@ -117,10 +122,14 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
 
       const retryWithBackoff$ = pipe(
         retry$,
-        mergeMap((op: Operation) => {
-          const { key, context } = op;
-          const retryCount = (context.retryCount || 0) + 1;
-          let delayAmount = context.retryDelay || MIN_DELAY;
+        mergeMap((operation: Operation) => {
+          const retry: RetryState = operation.context.retry || {
+            count: 0,
+            delay: null,
+          };
+
+          const retryCount = ++retry.count;
+          let delayAmount = retry.delay || MIN_DELAY;
 
           const backoffFactor = Math.random() + 1.5;
           // if randomDelay is enabled and it won't exceed the max delay, apply a random
@@ -137,7 +146,7 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
             filter(op => {
               return (
                 (op.kind === 'query' || op.kind === 'teardown') &&
-                op.key === key
+                op.key === operation.key
               );
             })
           );
@@ -145,7 +154,7 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
           dispatchDebug({
             type: 'retryAttempt',
             message: `The operation has failed and a retry has been triggered (${retryCount} / ${MAX_ATTEMPTS})`,
-            operation: op,
+            operation,
             data: {
               retryCount,
             },
@@ -154,10 +163,9 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
           // Add new retryDelay and retryCount to operation
           return pipe(
             fromValue(
-              makeOperation(op.kind, op, {
-                ...op.context,
-                retryDelay: delayAmount,
-                retryCount,
+              makeOperation(operation.kind, operation, {
+                ...operation.context,
+                retry,
               })
             ),
             debounce(() => delayAmount),
@@ -171,6 +179,7 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
         merge([operations$, retryWithBackoff$]),
         forward,
         filter(res => {
+          const retry = res.operation.context.retry as RetryState | undefined;
           // Only retry if the error passes the conditional retryIf function (if passed)
           // or if the error contains a networkError
           if (
@@ -179,12 +188,16 @@ export const retryExchange = (options: RetryExchangeOptions): Exchange => {
               ? !retryIf(res.error, res.operation)
               : !retryWith && !res.error.networkError)
           ) {
+            // Reset the delay state for a successful operation
+            if (retry) {
+              retry.count = 0;
+              retry.delay = null;
+            }
             return true;
           }
 
           const maxNumberAttemptsExceeded =
-            (res.operation.context.retryCount || 0) >= MAX_ATTEMPTS - 1;
-
+            ((retry && retry.count) || 0) >= MAX_ATTEMPTS - 1;
           if (!maxNumberAttemptsExceeded) {
             const operation = retryWith
               ? retryWith(res.error, res.operation)
diff --git a/packages/core/src/exchanges/cache.ts b/packages/core/src/exchanges/cache.ts
index 64e711e3..87873fbc 100755
--- a/packages/core/src/exchanges/cache.ts
+++ b/packages/core/src/exchanges/cache.ts
@@ -114,12 +114,21 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => {
         let { operation } = response;
         if (!operation) return;
 
-        const typenames = collectTypesFromResponse(response.data).concat(
-          operation.context.additionalTypenames || []
-        );
+        let typenames = operation.context.additionalTypenames || [];
+        // NOTE: For now, we only respect `additionalTypenames` from subscriptions to
+        // avoid unexpected breaking changes
+        // We'd expect live queries or other update mechanisms to be more suitable rather
+        // than using subscriptions as “signals” to reexecute queries. However, if they’re
+        // just used as signals, it’s intuitive to hook them up using `additionalTypenames`
+        if (response.operation.kind !== 'subscription') {
+          typenames = collectTypesFromResponse(response.data).concat(typenames);
+        }
 
         // Invalidates the cache given a mutation's response
-        if (response.operation.kind === 'mutation') {
+        if (
+          response.operation.kind === 'mutation' ||
+          response.operation.kind === 'subscription'
+        ) {
           const pendingOperations = new Set<number>();
 
           dispatchDebug({
diff --git a/packages/site/vercel.json b/packages/site/vercel.json
new file mode 100644
index 00000000..7ae9a3de
--- /dev/null
+++ b/packages/site/vercel.json
@@ -0,0 +1,5 @@
+{
+  "github": {
+    "silent": true
+  }
+}
diff --git a/packages/vue-urql/src/useQuery.ts b/packages/vue-urql/src/useQuery.ts
index 62339442..860653be 100644
--- a/packages/vue-urql/src/useQuery.ts
+++ b/packages/vue-urql/src/useQuery.ts
@@ -1,6 +1,14 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 
-import { WatchStopHandle, Ref, ref, watchEffect, reactive, isRef } from 'vue';
+import {
+  WatchStopHandle,
+  Ref,
+  shallowRef,
+  ref,
+  watchEffect,
+  reactive,
+  isRef,
+} from 'vue';
 
 import { Subscription, Source, pipe, subscribe, onEnd } from 'wonka';
 
@@ -14,11 +22,10 @@ import {
   RequestPolicy,
   Operation,
   createRequest,
-  GraphQLRequest,
 } from '@urql/core';
 
 import { useClient } from './useClient';
-import { unwrapPossibleProxy } from './utils';
+import { unwrapPossibleProxy, updateShallowRef } from './utils';
 
 type MaybeRef<T> = T | Ref<T>;
 type MaybeRefObj<T extends {}> = { [K in keyof T]: MaybeRef<T[K]> };
@@ -258,31 +265,34 @@ export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>(
     ? _args.pause
     : ref(!!_args.pause);
 
-  const request: Ref<GraphQLRequest<T, V>> = ref(
-    createRequest<T, V>(
+  const input = shallowRef({
+    request: createRequest<T, V>(
       unwrapPossibleProxy(args.query as any),
       unwrapPossibleProxy<V>(args.variables as V)
-    ) as any
-  );
+    ),
+    requestPolicy: unwrapPossibleProxy(args.requestPolicy),
+    isPaused: isPaused.value,
+  });
 
   const source: Ref<Source<OperationResult<T, V>> | undefined> = ref();
 
   stops.push(
     watchEffect(() => {
-      const newRequest = createRequest<T, V>(
-        unwrapPossibleProxy(args.query as any),
-        unwrapPossibleProxy<V>(args.variables as V)
-      );
-      if (request.value.key !== newRequest.key) {
-        request.value = newRequest;
-      }
+      updateShallowRef(input, {
+        request: createRequest<T, V>(
+          unwrapPossibleProxy(args.query as any),
+          unwrapPossibleProxy<V>(args.variables as V)
+        ),
+        requestPolicy: unwrapPossibleProxy(args.requestPolicy),
+        isPaused: isPaused.value,
+      });
     }, watchOptions)
   );
 
   stops.push(
     watchEffect(() => {
-      source.value = !isPaused.value
-        ? client.value.executeQuery<T, V>(request.value, {
+      source.value = !input.value.isPaused
+        ? client.value.executeQuery<T, V>(input.value.request, {
             requestPolicy: unwrapPossibleProxy(
               args.requestPolicy
             ) as RequestPolicy,
@@ -301,11 +311,16 @@ export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>(
     fetching,
     isPaused,
     executeQuery(opts?: Partial<OperationContext>): UseQueryResponse<T, V> {
-      const s = (source.value = client.value.executeQuery<T, V>(request.value, {
-        requestPolicy: unwrapPossibleProxy(args.requestPolicy) as RequestPolicy,
-        ...args.context,
-        ...opts,
-      }));
+      const s = (source.value = client.value.executeQuery<T, V>(
+        input.value.request,
+        {
+          requestPolicy: unwrapPossibleProxy(
+            args.requestPolicy
+          ) as RequestPolicy,
+          ...args.context,
+          ...opts,
+        }
+      ));
 
       return {
         ...response,
diff --git a/packages/vue-urql/src/useSubscription.ts b/packages/vue-urql/src/useSubscription.ts
index 33078eb3..f353231d 100644
--- a/packages/vue-urql/src/useSubscription.ts
+++ b/packages/vue-urql/src/useSubscription.ts
@@ -2,7 +2,15 @@
 
 import { Source, pipe, subscribe, onEnd } from 'wonka';
 
-import { WatchStopHandle, Ref, ref, watchEffect, reactive, isRef } from 'vue';
+import {
+  WatchStopHandle,
+  Ref,
+  ref,
+  shallowRef,
+  watchEffect,
+  reactive,
+  isRef,
+} from 'vue';
 
 import {
   Client,
@@ -13,11 +21,10 @@ import {
   OperationContext,
   Operation,
   createRequest,
-  GraphQLRequest,
 } from '@urql/core';
 
 import { useClient } from './useClient';
-import { unwrapPossibleProxy } from './utils';
+import { unwrapPossibleProxy, updateShallowRef } from './utils';
 
 type MaybeRef<T> = Exclude<T, void> | Ref<Exclude<T, void>>;
 type MaybeRefObj<T extends {}> = { [K in keyof T]: MaybeRef<T[K]> };
@@ -259,31 +266,32 @@ export function callUseSubscription<
     ? _args.pause
     : ref(!!_args.pause);
 
-  const request: Ref<GraphQLRequest<T, V>> = ref(
-    createRequest<T, V>(
+  const input = shallowRef({
+    request: createRequest<T, V>(
       unwrapPossibleProxy(args.query as any),
       unwrapPossibleProxy<V>(args.variables as V)
-    ) as any
-  );
+    ),
+    isPaused: isPaused.value,
+  });
 
   const source: Ref<Source<OperationResult<T, V>> | undefined> = ref();
 
   stops.push(
     watchEffect(() => {
-      const newRequest = createRequest<T, V>(
-        unwrapPossibleProxy(args.query as any),
-        unwrapPossibleProxy<V>(args.variables as V)
-      );
-      if (request.value.key !== newRequest.key) {
-        request.value = newRequest;
-      }
+      updateShallowRef(input, {
+        request: createRequest<T, V>(
+          unwrapPossibleProxy(args.query as any),
+          unwrapPossibleProxy<V>(args.variables as V)
+        ),
+        isPaused: isPaused.value,
+      });
     }, watchOptions)
   );
 
   stops.push(
     watchEffect(() => {
       source.value = !isPaused.value
-        ? client.value.executeSubscription<T, V>(request.value, {
+        ? client.value.executeSubscription<T, V>(input.value.request, {
             ...(unwrapPossibleProxy(args.context) as Partial<OperationContext>),
           })
         : undefined;
@@ -333,10 +341,13 @@ export function callUseSubscription<
     executeSubscription(
       opts?: Partial<OperationContext>
     ): UseSubscriptionResponse<T, R, V> {
-      source.value = client.value.executeSubscription<T, V>(request.value, {
-        ...unwrapPossibleProxy(args.context),
-        ...opts,
-      });
+      source.value = client.value.executeSubscription<T, V>(
+        input.value.request,
+        {
+          ...unwrapPossibleProxy(args.context),
+          ...opts,
+        }
+      );
 
       return state;
     },
diff --git a/packages/vue-urql/src/utils.ts b/packages/vue-urql/src/utils.ts
index 161d2d3a..0e3ad4e4 100644
--- a/packages/vue-urql/src/utils.ts
+++ b/packages/vue-urql/src/utils.ts
@@ -1,7 +1,38 @@
-import { Ref, isRef } from 'vue';
+import { GraphQLRequest, AnyVariables } from '@urql/core';
+import { Ref, ShallowRef, isRef } from 'vue';
 
 export function unwrapPossibleProxy<V>(possibleProxy: V | Ref<V>): V {
   return possibleProxy && isRef(possibleProxy)
     ? possibleProxy.value
     : possibleProxy;
 }
+
+export interface RequestState<
+  Data = any,
+  Variables extends AnyVariables = AnyVariables
+> {
+  request: GraphQLRequest<Data, Variables>;
+  isPaused: boolean;
+}
+
+export function createRequestState<
+  Data = any,
+  Variables extends AnyVariables = AnyVariables
+>(
+  request: GraphQLRequest<Data, Variables>,
+  isPaused: boolean
+): RequestState<Data, Variables> {
+  return { request, isPaused };
+}
+
+export const updateShallowRef = <T extends Record<string, any>>(
+  ref: ShallowRef<T>,
+  next: T
+) => {
+  for (const key in next) {
+    if (ref.value[key] !== next[key]) {
+      ref.value = next;
+      return;
+    }
+  }
+};


Test patch:
diff --git a/exchanges/graphcache/src/operations/query.test.ts b/exchanges/graphcache/src/operations/query.test.ts
index e90d9639..6c99ddf4 100644
--- a/exchanges/graphcache/src/operations/query.test.ts
+++ b/exchanges/graphcache/src/operations/query.test.ts
@@ -363,10 +363,14 @@ describe('Query', () => {
     expect(previousData).toHaveProperty('todos.0.textB', 'old');
   });
 
-  it('should keep references stable', () => {
+  it('should keep references stable (1)', () => {
     const QUERY = gql`
       query todos {
         __typename
+        todos {
+          __typename
+          test
+        }
         todos {
           __typename
           id
@@ -383,14 +387,17 @@ describe('Query', () => {
         {
           __typename: 'Todo',
           id: '0',
+          test: '0',
         },
         {
           __typename: 'Todo',
           id: '1',
+          test: '1',
         },
         {
           __typename: 'Todo',
           id: '2',
+          test: '2',
         },
       ],
       __typename: 'query_root',
@@ -405,15 +412,18 @@ describe('Query', () => {
         todos: [
           {
             __typename: 'Todo',
-            id: 'prev-0',
+            id: '0',
+            test: 'prev-0',
           },
           {
             __typename: 'Todo',
             id: '1',
+            test: '1',
           },
           {
             __typename: 'Todo',
             id: '2',
+            test: '2',
           },
         ],
         __typename: 'query_root',
@@ -426,5 +436,87 @@ describe('Query', () => {
     expect(prevData.todos[0]).toBe(data.todos[0]);
     expect(prevData.todos[1]).toBe(data.todos[1]);
     expect(prevData.todos[2]).toBe(data.todos[2]);
+    expect(prevData.todos).toBe(data.todos);
+    expect(prevData).toBe(data);
+  });
+
+  it('should keep references stable (negative test)', () => {
+    const QUERY = gql`
+      query todos {
+        __typename
+        todos {
+          __typename
+          id
+        }
+        todos {
+          __typename
+          test
+        }
+      }
+    `;
+
+    const store = new Store({
+      schema: alteredRoot,
+    });
+
+    const expected = {
+      todos: [
+        {
+          __typename: 'Todo',
+          id: '0',
+          test: '0',
+        },
+        {
+          __typename: 'Todo',
+          id: '1',
+          test: '1',
+        },
+        {
+          __typename: 'Todo',
+          id: '2',
+          test: '2',
+        },
+      ],
+      __typename: 'query_root',
+    };
+
+    write(store, { query: QUERY }, expected);
+
+    const prevData = query(
+      store,
+      { query: QUERY },
+      {
+        todos: [
+          {
+            __typename: 'Todo',
+            id: '0',
+            test: 'prev-0',
+          },
+          {
+            __typename: 'Todo',
+            id: '1',
+            test: '1',
+          },
+          {
+            __typename: 'Todo',
+            id: '2',
+            test: '2',
+          },
+        ],
+        __typename: 'query_root',
+      }
+    ).data as any;
+
+    expected.todos[0].test = 'x';
+    write(store, { query: QUERY }, expected);
+
+    const data = query(store, { query: QUERY }, prevData).data as any;
+    expect(data).toEqual(expected);
+
+    expect(prevData.todos[1]).toBe(data.todos[1]);
+    expect(prevData.todos[2]).toBe(data.todos[2]);
+    expect(prevData.todos[0]).not.toBe(data.todos[0]);
+    expect(prevData.todos).not.toBe(data.todos);
+    expect(prevData).not.toBe(data);
   });
 });
diff --git a/exchanges/retry/src/retryExchange.test.ts b/exchanges/retry/src/retryExchange.test.ts
index 2a418bea..13de90f8 100644
--- a/exchanges/retry/src/retryExchange.test.ts
+++ b/exchanges/retry/src/retryExchange.test.ts
@@ -1,4 +1,14 @@
-import { pipe, map, makeSubject, publish, tap } from 'wonka';
+import {
+  Source,
+  pipe,
+  map,
+  makeSubject,
+  mergeMap,
+  fromValue,
+  fromArray,
+  publish,
+  tap,
+} from 'wonka';
 import { vi, expect, it, beforeEach, afterEach } from 'vitest';
 
 import {
@@ -67,7 +77,7 @@ beforeEach(() => {
   ({ source: ops$, next } = makeSubject<Operation>());
 });
 
-it(`retries if it hits an error and works for multiple concurrent operations`, () => {
+it('retries if it hits an error and works for multiple concurrent operations', () => {
   const queryTwo = gql`
     {
       films {
@@ -149,7 +159,7 @@ it('should retry x number of times and then return the successful result', () =>
     // @ts-ignore
     return {
       operation: forwardOp,
-      ...(forwardOp.context.retryCount! >= numberRetriesBeforeSuccess
+      ...(forwardOp.context.retry?.count >= numberRetriesBeforeSuccess
         ? { data: queryOneData }
         : { error: queryOneError }),
     };
@@ -186,6 +196,68 @@ it('should retry x number of times and then return the successful result', () =>
   expect(result).toHaveBeenCalledTimes(1);
 });
 
+it('should reset the retry counter if an operation succeeded first', () => {
+  let call = 0;
+  const response = vi.fn((forwardOp: Operation): Source<any> => {
+    expect(forwardOp.key).toBe(op.key);
+    if (call === 0) {
+      call++;
+      return fromValue({
+        operation: forwardOp,
+        error: queryOneError,
+      } as any);
+    } else if (call === 1) {
+      call++;
+      return fromArray([
+        {
+          operation: forwardOp,
+          data: queryOneData,
+        } as any,
+        {
+          operation: forwardOp,
+          error: queryOneError,
+        } as any,
+      ]);
+    } else {
+      expect(forwardOp.context.retry).toEqual({ count: 1, delay: null });
+
+      return fromValue({
+        operation: forwardOp,
+        data: queryOneData,
+      } as any);
+    }
+  });
+
+  const result = vi.fn();
+  const forward: ExchangeIO = ops$ => {
+    return pipe(ops$, mergeMap(response));
+  };
+
+  const mockRetryIf = vi.fn(() => true);
+
+  pipe(
+    retryExchange({
+      ...mockOptions,
+      retryIf: mockRetryIf,
+    })({
+      forward,
+      client,
+      dispatchDebug,
+    })(ops$),
+    tap(result),
+    publish
+  );
+
+  next(op);
+  vi.runAllTimers();
+
+  expect(mockRetryIf).toHaveBeenCalledTimes(2);
+  expect(mockRetryIf).toHaveBeenCalledWith(queryOneError as any, op);
+
+  expect(response).toHaveBeenCalledTimes(3);
+  expect(result).toHaveBeenCalledTimes(2);
+});
+
 it(`should still retry if retryIf undefined but there is a networkError`, () => {
   const errorWithNetworkError = {
     ...queryOneError,