Repository: urql-graphql/urql (ts)

Issue:
RFC: Graphcache support changing `__typename` component of the cache
## Summary

The format for keys in the graphcache index is `<__typename>:<id>`. The `id` component is a bit ambiguous, as different apis will use different identifiers. Consequently, graphcache allows specifying a `keys` field on the options object that allows users to override how that id portion is computed for the key.

However, there is no way to do the same for the `__typename` component. This makes sense -- it is a standard part of the GQL api and there's little reason to override.

Unfortunately, the current caching model has no internal notion of inheritance (or unions). What this means is that if we have a resolver that returns `Car` whose two inhabitants are `FourWheeler` and `TwoWheeler`, a `FourWheeler` with id `123` will be cached as `FourWheeler:123`. 

Now let's say we have a mutation that unceremoniously rips the back two wheels off our car and returns it to the frontend. The id remains `123` and the entity is still a `Car`, but the underlying `__typename` would change to `TwoWheeler`. Graphcache will see this and insert a **brand new** cache entry called `TwoWheeler:123`, and not update the status of `FourWheeler:123`.

In a perfect world, this feature request would be to include better handling of that (maybe by generating multiple cache keys for each type and then having a separate type of link that joins them? not sure -- out of scope here).  However, there is a much simpler solution that handles the 95 percentile of cases (at least, that I personally encounter).

## Proposed Solution

Even if graphcache isn't really able to understand what the relationship between elements of a common inheritance structure is, the programmer does. In the same way that we are allowed to use the `keys` option on graphcache to re-key (really re-id) a record, we could also expose a `typenames` option that let's us "re-type" a record.

I don't know if this is a perfect solution. It really only works if cache keys are write-only (i.e. never parsed). As far as I can tell, this is the case, but I only gave a cursory glance.

For what it's worth, I've implemented this naive solution for our codebase, and so far it seems to be working exactly as expected. However I would love to hear the thoughts of the maintainers on whether this is a feasible (and safe!) solution. 

## Requirements

For me personally, the primary requirement is that I can use mutations that safely re-type an entity of a given inheritance chain (or union) and have the cache keep the link between it and previous versions.

## POC

https://gist.github.com/zwade/64b96dea8b483720be32eba51713a8bc

Pull request:
feat(graphcache): Add globalIDs option
Resolves #3176

## Summary

Add `globalIDs` option to omit typenames from entity cache keys.

This allows schemas with global ID guarantees and no ID overlap to combine entities in the cache. While this isn't recommended per se, since it's often a schema design problem, rather than a cache problem, it can be used to create strategic overlaps in the cache, when a type of an object implementing an interface “changes”

## Set of changes

- Add `globalIDs` option and update `keyOfEntity` function


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/dull-fishes-push.md b/.changeset/dull-fishes-push.md
new file mode 100644
index 00000000..0d3b5c5d
--- /dev/null
+++ b/.changeset/dull-fishes-push.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': minor
+---
+
+Add `globalIDs` configuration option to omit typenames in cache keys.
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/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts
index a5789c00..9debadf8 100644
--- a/exchanges/graphcache/src/store/store.ts
+++ b/exchanges/graphcache/src/store/store.ts
@@ -50,6 +50,7 @@ export class Store<
   updates: UpdatesConfig;
   optimisticMutations: OptimisticMutationConfig;
   keys: KeyingConfig;
+  globalIDs: Set<string> | boolean;
   schema?: SchemaIntrospector;
 
   rootFields: { query: string; mutation: string; subscription: string };
@@ -62,6 +63,10 @@ export class Store<
     this.optimisticMutations = opts.optimistic || {};
     this.keys = opts.keys || {};
 
+    this.globalIDs = Array.isArray(opts.globalIDs)
+      ? new Set(opts.globalIDs)
+      : !!opts.globalIDs;
+
     let queryName = 'Query';
     let mutationName = 'Mutation';
     let subscriptionName = 'Subscription';
@@ -107,22 +112,30 @@ 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 && data === contextRef.parent) return contextRef.parentKey;
-
-    if (data == null || typeof data === 'string') return data || null;
-    if (!data.__typename) return null;
-    if (this.rootNames[data.__typename]) return data.__typename;
+    if (contextRef && data === contextRef.parent) {
+      return contextRef.parentKey;
+    } else if (data == null || typeof data === 'string') {
+      return data || null;
+    } else if (!data.__typename) {
+      return null;
+    } else if (this.rootNames[data.__typename]) {
+      return data.__typename;
+    }
 
-    let key: string | null | void;
+    let key: string | null = null;
     if (this.keys[data.__typename]) {
-      key = this.keys[data.__typename](data);
+      key = this.keys[data.__typename](data) || null;
     } else if (data.id != null) {
       key = `${data.id}`;
     } else if (data._id != null) {
       key = `${data._id}`;
     }
 
-    return key ? `${data.__typename}:${key}` : null;
+    const typename = data.__typename;
+    const globalID =
+      this.globalIDs === true ||
+      (this.globalIDs && this.globalIDs.has(typename));
+    return globalID || !key ? key : `${typename}:${key}`;
   }
 
   resolve(entity: Entity, field: string, args?: FieldArgs): DataField {
diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts
index 5eee2b1a..81fc5863 100644
--- a/exchanges/graphcache/src/types.ts
+++ b/exchanges/graphcache/src/types.ts
@@ -581,6 +581,20 @@ export type CacheExchangeOpts = {
    * the full keys docs.
    */
   keys?: KeyingConfig;
+  /** Enables global IDs for keying GraphQL types.
+   *
+   * @remarks
+   * When `globalIDs` are enabled, GraphQL object type names will not contribute
+   * to the keys of entities and instead only their ID fields (or `keys` return
+   * values will be used.
+   *
+   * This is useful to overlap types of differing typenames. While this isn’t recommended
+   * it can be necessary to represent more complex interface relationships.
+   *
+   * If this should only be applied to a limited set of type names, a list of
+   * type names may be passed instead.
+   */
+  globalIDs?: string[] | boolean;
   /** Configures Graphcache with Schema Introspection data.
    *
    * @remarks
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/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 08edd52a..04157505 100644
--- a/exchanges/graphcache/src/store/store.test.ts
+++ b/exchanges/graphcache/src/store/store.test.ts
@@ -237,6 +237,20 @@ describe('Store with KeyingConfig', () => {
   });
 });
 
+describe('Store with Global IDs', () => {
+  it('generates keys without typenames when set to true', () => {
+    const store = new Store({ globalIDs: true });
+    expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('123');
+    expect(store.keyOfEntity({ __typename: 'None', id: '123' })).toBe('123');
+  });
+
+  it('generates keys without typenames when matching an input set', () => {
+    const store = new Store({ globalIDs: ['User'] });
+    expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('Any:123');
+    expect(store.keyOfEntity({ __typename: 'User', id: '123' })).toBe('123');
+  });
+});
+
 describe('Store with ResolverConfig', () => {
   it("sets the store's resolvers field to the given argument", () => {
     const resolversOption = {
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,