Repository: urql-graphql/urql
Pull request #3061 (2023-03-16 02:22:25)

refactor(core): Refactor hasNext/stale on OperationResult to be required
## Summary

Since `OperationResult.hasNext` is now important to determine whether operations should be deduplicated (as before, to be fair), and `OperationResult.stale` is important for `toPromise` to find the right result, it's a little dangerous not to have them be required.

Hence, the thinking here is to make them required and to ensure that for `@defer`/`@stream` we won't run into cases where those never reset to `false` or where deferred/streamed responses are duplicated.

## Set of changes

- Update `hasNext` and `stale` to be required
- Update tests and types


Patch:
diff --git a/.changeset/calm-buckets-scream.md b/.changeset/calm-buckets-scream.md
new file mode 100644
index 00000000..c9de0dd4
--- /dev/null
+++ b/.changeset/calm-buckets-scream.md
@@ -0,0 +1,18 @@
+---
+'@urql/core': major
+---
+
+Remove `defaultExchanges` from `@urql/core` and make `exchanges` a required property on `Client` construction.
+In doing so we make the `urql` package more tree-shakeable as the three default exchanges are in no code paths
+meaning they can be removed if not used.
+
+A migration would look as follows if you are currently creating a client without exchanges
+
+```js
+import { createClient, cacheExchange, fetchExchange } from '@urql/core'
+
+const client = createClient({
+  url: '',
+  exchanges: [cacheExchange, fetchExchange]
+});
+```
diff --git a/.changeset/four-boxes-impress.md b/.changeset/four-boxes-impress.md
new file mode 100644
index 00000000..79814c48
--- /dev/null
+++ b/.changeset/four-boxes-impress.md
@@ -0,0 +1,7 @@
+---
+'@urql/exchange-graphcache': major
+'@urql/svelte': major
+'@urql/core': major
+---
+
+Update `OperationResult.hasNext` and `OperationResult.stale` to be required fields. If you have a custom exchange creating results, you'll have to add these fields or use the `makeResult`, `mergeResultPatch`, or `makeErrorResult` helpers.
diff --git a/.changeset/honest-apples-join.md b/.changeset/honest-apples-join.md
new file mode 100644
index 00000000..dc5efc5d
--- /dev/null
+++ b/.changeset/honest-apples-join.md
@@ -0,0 +1,6 @@
+---
+'@urql/preact': major
+'urql': major
+---
+
+Remove the default `Client` from `Context`. Previously, `urql` kept a legacy default client in its context, with default exchanges and calling an API at `/graphql`. This has now been removed and you will have to create your own `Client` if you were relying on this behaviour.
diff --git a/.changeset/silent-numbers-look.md b/.changeset/silent-numbers-look.md
new file mode 100644
index 00000000..888b2422
--- /dev/null
+++ b/.changeset/silent-numbers-look.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Ensure network errors are always issued with `CombinedError`s, while downstream errors are re-thrown.
diff --git a/.changeset/wise-cherries-juggle.md b/.changeset/wise-cherries-juggle.md
new file mode 100644
index 00000000..310f8c76
--- /dev/null
+++ b/.changeset/wise-cherries-juggle.md
@@ -0,0 +1,10 @@
+---
+'@urql/core': minor
+---
+
+Deprecate the `dedupExchange`. The functionality of deduplicating queries and subscriptions has now been moved into and absorbed by the `Client`.
+
+Previously, the `Client` already started doing some work to share results between
+queries, and to avoid dispatching operations as needed. It now only dispatches operations
+strictly when the `dedupExchange` would allow so as well, moving its logic into the
+`Client`.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 25261b26..a87dd346 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,14 +23,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
@@ -71,14 +71,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
@@ -120,14 +120,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
@@ -175,14 +175,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1c34fe7e..99cccddf 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,14 +20,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml
index d6202ac3..cbc388fe 100644
--- a/.github/workflows/site.yml
+++ b/.github/workflows/site.yml
@@ -29,14 +29,14 @@ jobs:
           node-version: 18
 
       - name: Setup pnpm
-        uses: pnpm/action-setup@v2.2.2
+        uses: pnpm/action-setup@v2.2.4
         with:
           version: 7
           run_install: false
 
       - name: Get pnpm store directory
         id: pnpm-store
-        run: echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+        run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
 
       - name: Use pnpm store
         uses: actions/cache@v3
diff --git a/docs/advanced/authoring-exchanges.md b/docs/advanced/authoring-exchanges.md
index 41a342a9..2710f56b 100644
--- a/docs/advanced/authoring-exchanges.md
+++ b/docs/advanced/authoring-exchanges.md
@@ -283,7 +283,6 @@ The default order of exchanges is:
 ```js
 import { dedupExchange, cacheExchange, fetchExchange } from 'urql';
 
-// Also exported as `defaultExchanges`:
 [dedupExchange, cacheExchange, fetchExchange];
 ```
 
diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md
index f626c9b4..5b94634e 100644
--- a/docs/advanced/subscriptions.md
+++ b/docs/advanced/subscriptions.md
@@ -13,12 +13,14 @@ APIs and ability to handle GraphQL subscriptions.
 To add support for subscriptions we need to add the `subscriptionExchange` to our `Client`.
 
 ```js
-import { Client, defaultExchanges, subscriptionExchange } from 'urql';
+import { Client, dedupExchange, cacheExchange, fetchExchange, subscriptionExchange } from 'urql';
 
 const client = new Client({
   url: 'http://localhost:3000/graphql',
   exchanges: [
-    ...defaultExchanges,
+    dedupExchange,
+    cacheExchange,
+    fetchExchange,
     subscriptionExchange({
       forwardSubscription,
     }),
@@ -45,7 +47,7 @@ object with a `.subscribe()` method accepting an observer.
 For backends supporting `graphql-ws`, we recommend using the [graphql-ws](https://github.com/enisdenjo/graphql-ws) client.
 
 ```js
-import { createClient, defaultExchanges, subscriptionExchange } from 'urql';
+import { createClient, dedupExchange, cacheExchange, fetchExchange, subscriptionExchange } from 'urql';
 import { createClient as createWSClient } from 'graphql-ws';
 
 const wsClient = createWSClient({
@@ -55,7 +57,9 @@ const wsClient = createWSClient({
 const client = createClient({
   url: '/graphql',
   exchanges: [
-    ...defaultExchanges,
+    dedupExchange,
+    cacheExchange,
+    fetchExchange,
     subscriptionExchange({
       forwardSubscription: request => ({
         subscribe: sink => ({
@@ -81,7 +85,7 @@ package](https://github.com/apollographql/subscriptions-transport-ws) can be use
 > The `subscriptions-transport-ws` package isn't actively maintained. If your API supports the new protocol or you can swap the package out, consider using [`graphql-ws`](#setting-up-graphql-ws) instead.
 
 ```js
-import { Client, defaultExchanges, subscriptionExchange } from 'urql';
+import { Client, dedupExchange, cacheExchange, fetchExchange, subscriptionExchange } from 'urql';
 import { SubscriptionClient } from 'subscriptions-transport-ws';
 
 const subscriptionClient = new SubscriptionClient('ws://localhost/graphql', { reconnect: true });
@@ -89,7 +93,9 @@ const subscriptionClient = new SubscriptionClient('ws://localhost/graphql', { re
 const client = new Client({
   url: '/graphql',
   exchanges: [
-    ...defaultExchanges,
+    dedupExchange,
+    cacheExchange,
+    fetchExchange,
     subscriptionExchange({
       forwardSubscription: request => subscriptionClient.request(request),
     }),
diff --git a/docs/api/core.md b/docs/api/core.md
index b417e9b8..7c9e5092 100644
--- a/docs/api/core.md
+++ b/docs/api/core.md
@@ -22,7 +22,7 @@ It accepts several options on creation.
 
 | Input             | Type                                        | Description                                                                                                                                                                                                                                                                                                    |
 | ----------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `exchanges`       | `Exchange[]`                                | An array of `Exchange`s that the client should use instead of the list of `defaultExchanges`                                                                                                                                                                                                                   |
+| `exchanges`       | `Exchange[]`                                | An array of `Exchange`s that the client should use                                                                                                                                                                                                                   |
 | `url`             | `string`                                    | The GraphQL API URL as used by `fetchExchange`                                                                                                                                                                                                                                                                 |
 | `fetchOptions`    | `RequestInit \| () => RequestInit`          | Additional `fetchOptions` that `fetch` in `fetchExchange` should use to make a request                                                                                                                                                                                                                         |
 | `fetch`           | `typeof fetch`                              | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch`                                                                                                                                                                                                    |
@@ -523,15 +523,6 @@ this problem.
 
 It's used by the [`Client`](#client) when the `maskTypename` option is enabled.
 
-### defaultExchanges
-
-This is an array of the default `Exchange`s that the `Client` uses when the `exchanges` option isn't
-passed.
-
-```js
-const defaultExchanges = [dedupExchange, cacheExchange, fetchExchange];
-```
-
 ### composeExchanges
 
 This utility accepts an array of `Exchange`s and composes them into a single one.
diff --git a/docs/architecture.md b/docs/architecture.md
index 8d8fcba2..0c5f4d95 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -45,11 +45,12 @@ Whenever we decide to send a request to our API we start by using `urql`'s
 extra information on how the GraphQL requests are executed.
 
 ```js
-import { Client } from '@urql/core';
+import { Client, dedupExchange, cacheExchange, fetchExchange } from '@urql/core';
 
 new Client({
   url: 'http://localhost:3000/graphql',
   requestPolicy: 'cache-first',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange]
 });
 ```
 
diff --git a/docs/basics/core.md b/docs/basics/core.md
index 295aca9e..11742e5b 100644
--- a/docs/basics/core.md
+++ b/docs/basics/core.md
@@ -134,10 +134,11 @@ The `@urql/core` package exports a function called `createClient` which we can u
 create the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
 
 ```js
-import { createClient } from '@urql/core';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/core';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 ```
 
@@ -153,6 +154,7 @@ GraphQL API.
 ```js
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   fetchOptions: () => {
     const token = getToken();
     return {
@@ -168,17 +170,13 @@ As we've seen above, the most important option for the `Client` is `url`, since
 without it. However, another important option on the `Client` is the `exchanges` option.
 
 This option passes a list of exchanges to the `Client`, which tell it how to execute our requests
-and how to cache data in a certain order. By default, this will be populated with the list of
-`defaultExchanges`.
+and how to cache data in a certain order.
 
 ```js
-import { createClient, defaultExchanges } from '@urql/core';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/core';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
-  // the default:
-  exchanges: defaultExchanges,
-  // the same as:
   exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 ```
diff --git a/docs/basics/react-preact.md b/docs/basics/react-preact.md
index 3b20dc34..269b5198 100644
--- a/docs/basics/react-preact.md
+++ b/docs/basics/react-preact.md
@@ -41,10 +41,11 @@ The `urql` and `@urql/preact` packages export a method called `createClient` whi
 create the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
 
 ```js
-import { createClient } from 'urql';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 ```
 
@@ -60,6 +61,7 @@ GraphQL API.
 ```js
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   fetchOptions: () => {
     const token = getToken();
     return {
@@ -76,10 +78,11 @@ To make use of the `Client` in React & Preact we will have to provide it via the
 the `Provider` export.
 
 ```jsx
-import { createClient, Provider } from 'urql';
+import { createClient, Provider, dedupExchange, cacheExchange, fetchExchange } from 'urql';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 
 const App = () => (
@@ -245,6 +248,7 @@ import { createClient } from 'urql';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   // every operation will by default use cache-and-network rather
   // than cache-first now:
   requestPolicy: 'cache-and-network',
diff --git a/docs/basics/svelte.md b/docs/basics/svelte.md
index 0e884e66..f2da40d1 100644
--- a/docs/basics/svelte.md
+++ b/docs/basics/svelte.md
@@ -48,10 +48,11 @@ The `@urql/svelte` package exports a method called `createClient` which we can u
 the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
 
 ```js
-import { createClient } from '@urql/svelte';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/svelte';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 ```
 
@@ -67,6 +68,7 @@ GraphQL API.
 ```js
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   fetchOptions: () => {
     const token = getToken();
     return {
@@ -85,10 +87,11 @@ components. This will share one `Client` with the rest of our app, if we for ins
 
 ```html
 <script>
-  import { createClient, setContextClient } from '@urql/svelte';
+  import { createClient, setContextClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/svelte';
 
   const client = createClient({
     url: 'http://localhost:3000/graphql',
+    exchanges: [dedupExchange, cacheExchange, fetchExchange],
   });
 
   setContextClient(client);
@@ -292,6 +295,7 @@ import { createClient } from '@urql/svelte';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+    exchanges: [dedupExchange, cacheExchange, fetchExchange],
   // every operation will by default use cache-and-network rather
   // than cache-first now:
   requestPolicy: 'cache-and-network',
diff --git a/docs/basics/vue.md b/docs/basics/vue.md
index 219c9c60..7dd18fc3 100644
--- a/docs/basics/vue.md
+++ b/docs/basics/vue.md
@@ -37,10 +37,11 @@ The `@urql/vue` package exports a method called `createClient` which we can use
 the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
 
 ```js
-import { createClient } from '@urql/vue';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/vue';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 ```
 
@@ -56,6 +57,7 @@ GraphQL API.
 ```js
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   fetchOptions: () => {
     const token = getToken();
     return {
@@ -77,10 +79,11 @@ your parent components and accepts either a `Client` directly or just the option
 
 ```html
 <script>
-  import { createClient, provideClient } from '@urql/vue';
+  import { createClient, provideClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/vue';
 
   const client = createClient({
     url: 'http://localhost:3000/graphql',
+    exchanges: [dedupExchange, cacheExchange, fetchExchange],
   });
 
   provideClient(client);
@@ -361,10 +364,11 @@ provides metadata apart from the usual `query` and `variables` we may pass. This
 we may also change the `Client`'s default `requestPolicy` by passing it there.
 
 ```js
-import { createClient } from '@urql/vue';
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from '@urql/vue';
 
 const client = createClient({
   url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
   // every operation will by default use cache-and-network rather
   // than cache-first now:
   requestPolicy: 'cache-and-network',
diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts
index 8eb26d50..c8ff6e00 100644
--- a/exchanges/graphcache/src/cacheExchange.ts
+++ b/exchanges/graphcache/src/cacheExchange.ts
@@ -26,10 +26,11 @@ import { filterVariables, getMainOperation } from './ast';
 import { Store, noopDataState, hydrateData, reserveLayer } from './store';
 import { Data, Dependencies, CacheExchangeOpts } from './types';
 
-type OperationResultWithMeta = OperationResult & {
+interface OperationResultWithMeta extends Partial<OperationResult> {
+  operation: Operation;
   outcome: CacheOutcome;
   dependencies: Dependencies;
-};
+}
 
 type Operations = Set<number>;
 type OperationMap = Map<number, Operation>;
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 95e0ff37..bb83a5db 100755
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -12,6 +12,7 @@ import {
   Source,
   take,
   takeUntil,
+  takeWhile,
   publish,
   subscribe,
   switchMap,
@@ -23,7 +24,7 @@ import {
 
 import { DocumentNode } from 'graphql';
 
-import { composeExchanges, defaultExchanges } from './exchanges';
+import { composeExchanges } from './exchanges';
 import { fallbackExchange } from './exchanges/fallback';
 
 import {
@@ -111,13 +112,13 @@ export interface ClientOptions {
    * This is the basis for how `urql` handles GraphQL operations, and exchanges handle the creation, execution,
    * and control flow of exchanges for the `Client`.
    *
-   * When this option is left out, the `Client` defaults to a list of {@link defaultExchanges}. By default, these
-   * will implement deduping, caching (via a document cache), and fetching (GraphQL over HTTP).
+   * To easily get started you should consider using the {@link dedupExchange}, {@link cacheExchange} and {@link fetchExchange}
+   * these are all exported from the core package.
    *
    * @see {@link https://formidable.com/open-source/urql/docs/architecture/#the-client-and-exchanges} for more information
    * on what `Exchange`s are and how they work.
    */
-  exchanges?: Exchange[];
+  exchanges: Exchange[];
   /** A configuration flag indicating whether support for "Suspense" is activated.
    *
    * @remarks
@@ -566,13 +567,20 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
 
   // This subject forms the input of operations; executeOperation may be
   // called to dispatch a new operation on the subject
-  const { source: operations$, next: nextOperation } = makeSubject<Operation>();
+  const operations = makeSubject<Operation>();
+
+  function nextOperation(operation: Operation) {
+    const prevReplay = replays.get(operation.key);
+    if (operation.kind === 'mutation' || !prevReplay || !prevReplay.hasNext)
+      operations.next(operation);
+  }
 
   // We define a queued dispatcher on the subject, which empties the queue when it's
   // activated to allow `reexecuteOperation` to be trampoline-scheduled
   let isOperationBatchActive = false;
   function dispatchOperation(operation?: Operation | void) {
     if (operation) nextOperation(operation);
+
     if (!isOperationBatchActive) {
       isOperationBatchActive = true;
       while (isOperationBatchActive && (operation = queue.shift()))
@@ -602,21 +610,33 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
       );
     }
 
+    if (operation.kind !== 'query') {
+      result$ = pipe(
+        result$,
+        onStart(() => {
+          nextOperation(operation);
+        })
+      );
+    }
+
     // A mutation is always limited to just a single result and is never shared
     if (operation.kind === 'mutation') {
-      return pipe(
+      return pipe(result$, take(1));
+    }
+
+    if (operation.kind === 'subscription') {
+      result$ = pipe(
         result$,
-        onStart(() => nextOperation(operation)),
-        take(1)
+        takeWhile(result => !!result.hasNext)
       );
     }
 
-    const source = pipe(
+    return pipe(
       result$,
       // End the results stream when an active teardown event is sent
       takeUntil(
         pipe(
-          operations$,
+          operations.source,
           filter(op => op.kind === 'teardown' && op.key === operation.key)
         )
       ),
@@ -629,7 +649,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
           fromValue(result),
           // Mark a result as stale when a new operation is sent for it
           pipe(
-            operations$,
+            operations.source,
             filter(
               op =>
                 op.kind === 'query' &&
@@ -656,15 +676,13 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
       }),
       share
     );
-
-    return source;
   };
 
   const instance: Client =
     this instanceof Client ? this : Object.create(Client.prototype);
   const client: Client = Object.assign(instance, {
     suspense: !!opts.suspense,
-    operations$,
+    operations$: operations.source,
 
     reexecuteOperation(operation: Operation) {
       // Reexecute operation only if any subscribers are still subscribed to the
@@ -708,33 +726,29 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
 
       return make<OperationResult>(observer => {
         let source = active.get(operation.key);
-
         if (!source) {
           active.set(operation.key, (source = makeResultSource(operation)));
         }
 
-        const isNetworkOperation =
-          operation.context.requestPolicy === 'cache-and-network' ||
-          operation.context.requestPolicy === 'network-only';
-
         return pipe(
           source,
           onStart(() => {
             const prevReplay = replays.get(operation.key);
-
-            if (operation.kind === 'subscription') {
-              return dispatchOperation(operation);
+            const isNetworkOperation =
+              operation.context.requestPolicy === 'cache-and-network' ||
+              operation.context.requestPolicy === 'network-only';
+            if (operation.kind !== 'query') {
+              return;
             } else if (isNetworkOperation) {
               dispatchOperation(operation);
+              if (prevReplay && !prevReplay.hasNext) prevReplay.stale = true;
             }
 
             if (
               prevReplay != null &&
               prevReplay === replays.get(operation.key)
             ) {
-              observer.next(
-                isNetworkOperation ? { ...prevReplay, stale: true } : prevReplay
-              );
+              observer.next(prevReplay);
             } else if (!isNetworkOperation) {
               dispatchOperation(operation);
             }
@@ -812,12 +826,9 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
     dispatchDebug = next as ExchangeInput['dispatchDebug'];
   }
 
-  const exchanges =
-    opts.exchanges !== undefined ? opts.exchanges : defaultExchanges;
-
   // All exchange are composed into a single one and are called using the constructed client
   // and the fallback exchange stream
-  const composedExchange = composeExchanges(exchanges);
+  const composedExchange = composeExchanges(opts.exchanges);
 
   // All exchanges receive inputs using which they can forward operations to the next exchange
   // and receive a stream of results in return, access the client, or dispatch debugging events
@@ -827,7 +838,7 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
       client,
       dispatchDebug,
       forward: fallbackExchange({ dispatchDebug }),
-    })(operations$)
+    })(operations.source)
   );
 
   // Prevent the `results$` exchange pipeline from being closed by active
diff --git a/packages/core/src/exchanges/cache.ts b/packages/core/src/exchanges/cache.ts
index cbe67e32..f43c313f 100755
--- a/packages/core/src/exchanges/cache.ts
+++ b/packages/core/src/exchanges/cache.ts
@@ -76,12 +76,15 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => {
               }),
         });
 
-        const result: OperationResult = {
-          ...cachedResult,
-          operation: addMetadata(operation, {
-            cacheOutcome: cachedResult ? 'hit' : 'miss',
-          }),
-        };
+        let result: OperationResult = cachedResult!;
+        if (process.env.NODE_ENV !== 'production') {
+          result = {
+            ...result,
+            operation: addMetadata(operation, {
+              cacheOutcome: cachedResult ? 'hit' : 'miss',
+            }),
+          };
+        }
 
         if (operation.context.requestPolicy === 'cache-and-network') {
           result.stale = true;
diff --git a/packages/core/src/exchanges/dedup.ts b/packages/core/src/exchanges/dedup.ts
index 41506a75..093a5501 100644
--- a/packages/core/src/exchanges/dedup.ts
+++ b/packages/core/src/exchanges/dedup.ts
@@ -1,57 +1,7 @@
-import { filter, pipe, tap } from 'wonka';
 import { Exchange } from '../types';
 
 /** Default deduplication exchange.
- *
- * @remarks
- * The `dedupExchange` deduplicates queries and subscriptions that are
- * started with identical documents and variables by deduplicating by
- * their {@link Operation.key}.
- * This can prevent duplicate requests from being sent to your GraphQL API.
- *
- * Because this is a very safe exchange to add to any GraphQL setup, it’s
- * not only the default, but we also recommend you to always keep this
- * exchange added and included in your setup.
- *
- * Hint: In React and Vue, some common usage patterns can trigger duplicate
- * operations. For instance, in React a single render will actually
- * trigger two phases that execute an {@link Operation}.
+ * @deprecated
+ * This exchange's functionality is now built into the {@link Client}.
  */
-export const dedupExchange: Exchange = ({ forward, dispatchDebug }) => {
-  const inFlightKeys = new Set<number>();
-  return ops$ =>
-    pipe(
-      forward(
-        pipe(
-          ops$,
-          filter(operation => {
-            if (
-              operation.kind === 'teardown' ||
-              operation.kind === 'mutation'
-            ) {
-              inFlightKeys.delete(operation.key);
-              return true;
-            }
-
-            const isInFlight = inFlightKeys.has(operation.key);
-            inFlightKeys.add(operation.key);
-
-            if (isInFlight) {
-              dispatchDebug({
-                type: 'dedup',
-                message: 'An operation has been deduped.',
-                operation,
-              });
-            }
-
-            return !isInFlight;
-          })
-        )
-      ),
-      tap(result => {
-        if (!result.hasNext) {
-          inFlightKeys.delete(result.operation.key);
-        }
-      })
-    );
-};
+export const dedupExchange: Exchange = ({ forward }) => ops$ => forward(ops$);
diff --git a/packages/core/src/exchanges/index.ts b/packages/core/src/exchanges/index.ts
index bfc21350..3434aa76 100644
--- a/packages/core/src/exchanges/index.ts
+++ b/packages/core/src/exchanges/index.ts
@@ -14,18 +14,3 @@ export type {
 
 export { mapExchange, mapExchange as errorExchange } from './map';
 export type { MapExchangeOpts } from './map';
-
-import { cacheExchange } from './cache';
-import { dedupExchange } from './dedup';
-import { fetchExchange } from './fetch';
-
-/** The default list of exchanges a `Client` falls back to.
- *
- * @remarks
- * When {@link ClientOptions.exchanges} isn’s passed, a {@link Client} is automatically
- * created using this list of default exchanges.
- *
- * By default, this adds deduplication of operations, a basic document cache,
- * and the built-in fetch exchange for GraphQL over HTTP.
- */
-export const defaultExchanges = [dedupExchange, cacheExchange, fetchExchange];
diff --git a/packages/core/src/internal/fetchSource.ts b/packages/core/src/internal/fetchSource.ts
index efbcc74e..4345a22b 100644
--- a/packages/core/src/internal/fetchSource.ts
+++ b/packages/core/src/internal/fetchSource.ts
@@ -104,6 +104,7 @@ async function* fetchOperation(
   url: string,
   fetchOptions: RequestInit
 ) {
+  let networkMode = true;
   let abortController: AbortController | void;
   let result: OperationResult | null = null;
   let response: Response;
@@ -132,16 +133,19 @@ async function* fetchOperation(
     }
 
     for await (const payload of results) {
-      yield (result = result
+      result = result
         ? mergeResultPatch(result, payload, response)
-        : makeResult(operation, payload, response));
+        : makeResult(operation, payload, response);
+      networkMode = false;
+      yield result;
+      networkMode = true;
     }
 
     if (!result) {
       yield (result = makeResult(operation, {}, response));
     }
   } catch (error: any) {
-    if (result) {
+    if (!networkMode) {
       throw error;
     }
 
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index a1e6bcf4..2e6ba39e 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -565,7 +565,7 @@ export interface OperationResult<
    * Most commonly, this flag is set for a cached result when the operation is executed using the
    * `cache-and-network` {@link RequestPolicy}.
    */
-  stale?: boolean;
+  stale: boolean;
   /** Indicates that the GraphQL response is streamed and updated results will follow.
    *
    * @remarks
@@ -575,7 +575,7 @@ export interface OperationResult<
    *
    * For GraphQL subscriptions, this flag will always be set to `true`.
    */
-  hasNext?: boolean;
+  hasNext: boolean;
 }
 
 /** The input parameters a `Client` passes to an `Exchange` when it's created.
diff --git a/packages/core/src/utils/result.ts b/packages/core/src/utils/result.ts
index 16e7cb18..349775a7 100644
--- a/packages/core/src/utils/result.ts
+++ b/packages/core/src/utils/result.ts
@@ -44,6 +44,7 @@ export const makeResult = (
       : undefined,
     extensions: result.extensions ? { ...result.extensions } : undefined,
     hasNext: result.hasNext == null ? defaultHasNext : result.hasNext,
+    stale: false,
   };
 };
 
@@ -126,6 +127,7 @@ export const mergeResultPatch = (
       : undefined,
     extensions: hasExtensions ? extensions : undefined,
     hasNext: !!nextResult.hasNext,
+    stale: false,
   };
 };
 
@@ -154,4 +156,6 @@ export const makeErrorResult = (
     response,
   }),
   extensions: undefined,
+  hasNext: false,
+  stale: false,
 });
diff --git a/packages/preact-urql/README.md b/packages/preact-urql/README.md
index 086ba16b..d2d20a9e 100644
--- a/packages/preact-urql/README.md
+++ b/packages/preact-urql/README.md
@@ -29,11 +29,11 @@ The usage is a 1:1 mapping of the React usage found [here](https://formidable.co
 small example:
 
 ```jsx
-import { createClient, defaultExchanges, Provider, useQuery } from '@urql/preact';
+import { createClient, dedupExchange, cacheExchange, fetchExchange, Provider, useQuery } from '@urql/preact';
 
 const client = createClient({
   url: 'https://myHost/graphql',
-  exchanges: defaultExchanges,
+  exchanges: [dedupExchange, cacheExchange, fetchExchange],
 });
 
 const App = () => (
diff --git a/packages/preact-urql/src/context.ts b/packages/preact-urql/src/context.ts
index c3b63cef..aa29bf18 100644
--- a/packages/preact-urql/src/context.ts
+++ b/packages/preact-urql/src/context.ts
@@ -1,35 +1,28 @@
 import { createContext } from 'preact';
 import { useContext } from 'preact/hooks';
-import { Client, createClient } from '@urql/core';
+import { Client } from '@urql/core';
 
-// We assume some default options here; mainly not to actually be used
-// but not to error catastrophically if someone is just playing around
-const defaultClient = createClient({ url: '/graphql' });
+const OBJ = {};
+export const Context: import('preact').Context<Client | object> = createContext(
+  OBJ
+);
+export const Provider: import('preact').Provider<Client | object> =
+  Context.Provider;
+export const Consumer: import('preact').Consumer<Client | object> =
+  Context.Consumer;
 
-export const Context = createContext<Client>(defaultClient);
-export const Provider = Context.Provider;
-export const Consumer = Context.Consumer;
 Context.displayName = 'UrqlContext';
 
-let hasWarnedAboutDefault = false;
-
 export const useClient = (): Client => {
   const client = useContext(Context);
 
-  if (
-    process.env.NODE_ENV !== 'production' &&
-    client === defaultClient &&
-    !hasWarnedAboutDefault
-  ) {
-    hasWarnedAboutDefault = true;
+  if (client === OBJ && process.env.NODE_ENV !== 'production') {
+    const error =
+      "No client has been specified using urql's Provider. please create a client and add a Provider.";
 
-    console.warn(
-      "Default Client: No client has been specified using urql's Provider." +
-        'This means that urql will be falling back to defaults including making ' +
-        'requests to `/graphql`.\n' +
-        "If that's not what you want, please create a client and add a Provider."
-    );
+    console.error(error);
+    throw new Error(error);
   }
 
-  return client;
+  return client as Client;
 };
diff --git a/packages/react-urql/src/context.ts b/packages/react-urql/src/context.ts
index 39edbcd5..4501d9da 100644
--- a/packages/react-urql/src/context.ts
+++ b/packages/react-urql/src/context.ts
@@ -1,37 +1,27 @@
 import { createContext, useContext } from 'react';
-import { Client, createClient } from '@urql/core';
+import { Client } from '@urql/core';
 
-// We assume some default options here; mainly not to actually be used
-// but not to error catastrophically if someone is just playing around
-const defaultClient = createClient({ url: '/graphql' });
-
-export const Context: import('react').Context<Client> = createContext(
-  defaultClient
+const OBJ = {};
+export const Context: import('react').Context<Client | object> = createContext(
+  OBJ
 );
-export const Provider: import('react').Provider<Client> = Context.Provider;
-export const Consumer: import('react').Consumer<Client> = Context.Consumer;
+export const Provider: import('react').Provider<Client | object> =
+  Context.Provider;
+export const Consumer: import('react').Consumer<Client | object> =
+  Context.Consumer;
 
 Context.displayName = 'UrqlContext';
 
-let hasWarnedAboutDefault = false;
-
 export const useClient = (): Client => {
   const client = useContext(Context);
 
-  if (
-    process.env.NODE_ENV !== 'production' &&
-    client === defaultClient &&
-    !hasWarnedAboutDefault
-  ) {
-    hasWarnedAboutDefault = true;
+  if (client === OBJ && process.env.NODE_ENV !== 'production') {
+    const error =
+      "No client has been specified using urql's Provider. please create a client and add a Provider.";
 
-    console.warn(
-      "Default Client: No client has been specified using urql's Provider." +
-        'This means that urql will be falling back to defaults including making ' +
-        'requests to `/graphql`.\n' +
-        "If that's not what you want, please create a client and add a Provider."
-    );
+    console.error(error);
+    throw new Error(error);
   }
 
-  return client;
+  return client as Client;
 };
diff --git a/packages/svelte-urql/src/common.ts b/packages/svelte-urql/src/common.ts
index 621b5dfb..23ac6648 100644
--- a/packages/svelte-urql/src/common.ts
+++ b/packages/svelte-urql/src/common.ts
@@ -19,11 +19,13 @@ export const fromStore = <T>(store$: Readable<T>): Source<T> =>
   make(observer => store$.subscribe(observer.next));
 
 export const initialResult = {
+  operation: undefined,
   fetching: false,
-  stale: false,
-  error: undefined,
   data: undefined,
+  error: undefined,
   extensions: undefined,
+  hasNext: false,
+  stale: false,
 };
 
 export interface Pausable {


Test patch:
diff --git a/exchanges/auth/src/authExchange.test.ts b/exchanges/auth/src/authExchange.test.ts
index b5ba06ac..1229142f 100644
--- a/exchanges/auth/src/authExchange.test.ts
+++ b/exchanges/auth/src/authExchange.test.ts
@@ -21,13 +21,16 @@ import {
 
 import { vi, expect, it } from 'vitest';
 import { print } from 'graphql';
-import { queryOperation } from '../../../packages/core/src/test-utils';
+import {
+  queryResponse,
+  queryOperation,
+} from '../../../packages/core/src/test-utils';
 import { authExchange } from './authExchange';
 
 const makeExchangeArgs = () => {
   const operations: Operation[] = [];
   const result = vi.fn(
-    (operation: Operation): OperationResult => ({ operation })
+    (operation: Operation): OperationResult => ({ ...queryResponse, operation })
   );
 
   return {
@@ -40,7 +43,10 @@ const makeExchangeArgs = () => {
           tap(op => operations.push(op)),
           map(result)
         ),
-      client: new Client({ url: '/api' }),
+      client: new Client({
+        url: '/api',
+        exchanges: [],
+      }),
     } as any,
   };
 };
@@ -244,7 +250,9 @@ it('triggers authentication when an operation did error', async () => {
   await new Promise(resolve => setTimeout(resolve));
 
   result.mockReturnValueOnce({
+    ...queryResponse,
     operation: queryOperation,
+    data: undefined,
     error: new CombinedError({
       graphQLErrors: [{ message: 'Oops' }],
     }),
diff --git a/exchanges/context/src/context.test.ts b/exchanges/context/src/context.test.ts
index e16fd053..001f7146 100644
--- a/exchanges/context/src/context.test.ts
+++ b/exchanges/context/src/context.test.ts
@@ -9,6 +9,7 @@ import {
   ExchangeIO,
 } from '@urql/core';
 
+import { queryResponse } from '../../../packages/core/src/test-utils';
 import { contextExchange } from './context';
 
 const queryOne = gql`
@@ -32,7 +33,10 @@ const queryOneData = {
 const dispatchDebug = vi.fn();
 let client, op, ops$, next;
 beforeEach(() => {
-  client = createClient({ url: 'http://0.0.0.0' });
+  client = createClient({
+    url: 'http://0.0.0.0',
+    exchanges: [],
+  });
   op = client.createRequestOperation('query', {
     key: 1,
     query: queryOne,
@@ -45,6 +49,7 @@ it(`calls getContext`, () => {
   const response = vi.fn(
     (forwardOp: Operation): OperationResult => {
       return {
+        ...queryResponse,
         operation: forwardOp,
         data: queryOneData,
       };
@@ -80,6 +85,7 @@ it(`calls getContext async`, async () => {
   const response = vi.fn(
     (forwardOp: Operation): OperationResult => {
       return {
+        ...queryResponse,
         operation: forwardOp,
         data: queryOneData,
       };
diff --git a/exchanges/execute/src/execute.test.ts b/exchanges/execute/src/execute.test.ts
index c4070fd1..3185d9a9 100644
--- a/exchanges/execute/src/execute.test.ts
+++ b/exchanges/execute/src/execute.test.ts
@@ -272,6 +272,7 @@ describe('on success response', () => {
       operation: queryOperation,
       data: mockHttpResponseData,
       hasNext: false,
+      stale: false,
     });
   });
 });
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.test.ts b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
index cf2af321..931e13de 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.test.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
@@ -8,8 +8,10 @@ import { minifyIntrospectionQuery } from '@urql/introspection';
 const mocked = (x: any): any => x;
 
 describe('SchemaPredicates', () => {
-  // eslint-disable-next-line
-  const schema = buildClientSchema(minifyIntrospectionQuery(require('../test-utils/simple_schema.json')));
+  const schema = buildClientSchema(
+    // eslint-disable-next-line
+    minifyIntrospectionQuery(require('../test-utils/simple_schema.json'))
+  );
 
   const frag = (value: string): InlineFragmentNode => ({
     kind: Kind.INLINE_FRAGMENT,
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index 9666b6e0..49f79f1a 100644
--- a/exchanges/graphcache/src/cacheExchange.test.ts
+++ b/exchanges/graphcache/src/cacheExchange.test.ts
@@ -6,6 +6,7 @@ import {
   OperationResult,
   CombinedError,
 } from '@urql/core';
+
 import { vi, expect, it, describe } from 'vitest';
 
 import {
@@ -23,6 +24,7 @@ import {
 } from 'wonka';
 
 import { minifyIntrospectionQuery } from '@urql/introspection';
+import { queryResponse } from '../../../packages/core/src/test-utils';
 import { cacheExchange } from './cacheExchange';
 
 const queryOne = gql`
@@ -54,7 +56,10 @@ const dispatchDebug = vi.fn();
 
 describe('data dependencies', () => {
   it('writes queries to the cache', () => {
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const op = client.createRequestOperation('query', {
       key: 1,
       query: queryOne,
@@ -77,7 +82,7 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         expect(forwardOp.key).toBe(op.key);
-        return { operation: forwardOp, data: expected };
+        return { ...queryResponse, operation: forwardOp, data: expected };
       }
     );
 
@@ -111,7 +116,10 @@ describe('data dependencies', () => {
   });
 
   it('respects cache-only operations', () => {
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const op = client.createRequestOperation(
       'query',
       {
@@ -127,7 +135,7 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         expect(forwardOp.key).toBe(op.key);
-        return { operation: forwardOp, data: queryOneData };
+        return { ...queryResponse, operation: forwardOp, data: queryOneData };
       }
     );
 
@@ -174,7 +182,10 @@ describe('data dependencies', () => {
       ],
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -196,9 +207,13 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         } else if (forwardOp.key === 2) {
-          return { operation: opMultiple, data: queryMultipleData };
+          return {
+            ...queryResponse,
+            operation: opMultiple,
+            data: queryMultipleData,
+          };
         }
 
         return undefined as any;
@@ -304,7 +319,10 @@ describe('data dependencies', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -332,11 +350,15 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryByIdDataA };
+          return { ...queryResponse, operation: opOne, data: queryByIdDataA };
         } else if (forwardOp.key === 2) {
-          return { operation: opTwo, data: queryByIdDataB };
+          return { ...queryResponse, operation: opTwo, data: queryByIdDataB };
         } else if (forwardOp.key === 3) {
-          return { operation: opMutation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -411,7 +433,10 @@ describe('data dependencies', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
     const reexec = vi
       .spyOn(client, 'reexecuteOperation')
@@ -431,9 +456,13 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         } else if (forwardOp.key === 2) {
-          return { operation: opUnrelated, data: queryUnrelatedData };
+          return {
+            ...queryResponse,
+            operation: opUnrelated,
+            data: queryUnrelatedData,
+          };
         }
 
         return undefined as any;
@@ -473,7 +502,10 @@ describe('data dependencies', () => {
       concealAuthor: true,
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
@@ -487,7 +519,11 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opMutation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -530,7 +566,10 @@ describe('data dependencies', () => {
       concealAuthor: true,
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     vi.spyOn(client, 'reexecuteOperation').mockImplementation(next);
@@ -544,7 +583,11 @@ describe('data dependencies', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opMutation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -583,7 +626,10 @@ describe('data dependencies', () => {
   });
 
   it('marks errored null fields as uncached but delivers them as expected', () => {
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const query = gql`
@@ -602,6 +648,7 @@ describe('data dependencies', () => {
     });
 
     const queryResult: OperationResult = {
+      ...queryResponse,
       operation,
       data: {
         __typename: 'Query',
@@ -680,7 +727,10 @@ describe('optimistic updates', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -702,9 +752,13 @@ describe('optimistic updates', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         } else if (forwardOp.key === 2) {
-          return { operation: opMutation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -773,7 +827,10 @@ describe('optimistic updates', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -801,11 +858,19 @@ describe('optimistic updates', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         } else if (forwardOp.key === 2) {
-          return { operation: opMutationOne, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutationOne,
+            data: mutationData,
+          };
         } else if (forwardOp.key === 3) {
-          return { operation: opMutationTwo, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutationTwo,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -870,7 +935,10 @@ describe('optimistic updates', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -898,7 +966,7 @@ describe('optimistic updates', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         }
 
         return undefined as any;
@@ -987,7 +1055,10 @@ describe('optimistic updates', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const reexec = vi
@@ -1009,9 +1080,10 @@ describe('optimistic updates', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: authorsQueryData };
+          return { ...queryResponse, operation: opOne, data: authorsQueryData };
         } else if (forwardOp.key === 2) {
           return {
+            ...queryResponse,
             operation: opMutation,
             error: 'error' as any,
             data: { __typename: 'Mutation', addAuthor: null },
@@ -1070,7 +1142,10 @@ describe('optimistic updates', () => {
 
 describe('custom resolvers', () => {
   it('follows resolvers on initial write', () => {
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const opOne = client.createRequestOperation('query', {
@@ -1082,7 +1157,7 @@ describe('custom resolvers', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         }
 
         return undefined as any;
@@ -1143,7 +1218,10 @@ describe('custom resolvers', () => {
       },
     };
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const opOne = client.createRequestOperation('query', {
@@ -1161,9 +1239,13 @@ describe('custom resolvers', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: opOne, data: queryOneData };
+          return { ...queryResponse, operation: opOne, data: queryOneData };
         } else if (forwardOp.key === 2) {
-          return { operation: opMutation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: opMutation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -1227,7 +1309,10 @@ describe('custom resolvers', () => {
       }
     `;
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
 
     const query = gql`
@@ -1304,9 +1389,17 @@ describe('custom resolvers', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: queryOperation, data: queryData };
+          return {
+            ...queryResponse,
+            operation: queryOperation,
+            data: queryData,
+          };
         } else if (forwardOp.key === 2) {
-          return { operation: mutationOperation, data: mutationData };
+          return {
+            ...queryResponse,
+            operation: mutationOperation,
+            data: mutationData,
+          };
         }
 
         return undefined as any;
@@ -1393,7 +1486,10 @@ describe('custom resolvers', () => {
 describe('schema awareness', () => {
   it('reexecutes query and returns data on partial result', () => {
     vi.useFakeTimers();
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
     const reexec = vi
       .spyOn(client, 'reexecuteOperation')
@@ -1458,9 +1554,17 @@ describe('schema awareness', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: initialQueryOperation, data: queryData };
+          return {
+            ...queryResponse,
+            operation: initialQueryOperation,
+            data: queryData,
+          };
         } else if (forwardOp.key === 2) {
-          return { operation: queryOperation, data: queryData };
+          return {
+            ...queryResponse,
+            operation: queryOperation,
+            data: queryData,
+          };
         }
 
         return undefined as any;
@@ -1536,7 +1640,10 @@ describe('schema awareness', () => {
 
   it('reexecutes query and returns data on partial results for nullable lists', () => {
     vi.useFakeTimers();
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next } = makeSubject<Operation>();
     const reexec = vi
       .spyOn(client, 'reexecuteOperation')
@@ -1592,9 +1699,17 @@ describe('schema awareness', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === 1) {
-          return { operation: initialQueryOperation, data: queryData };
+          return {
+            ...queryResponse,
+            operation: initialQueryOperation,
+            data: queryData,
+          };
         } else if (forwardOp.key === 2) {
-          return { operation: queryOperation, data: queryData };
+          return {
+            ...queryResponse,
+            operation: queryOperation,
+            data: queryData,
+          };
         }
 
         return undefined as any;
@@ -1661,6 +1776,7 @@ describe('commutativity', () => {
     const client = createClient({
       url: 'http://0.0.0.0',
       requestPolicy: 'cache-and-network',
+      exchanges: [],
     });
     const { source: ops$, next: next } = makeSubject<Operation>();
     const query = gql`
@@ -1672,6 +1788,7 @@ describe('commutativity', () => {
     const result = (operation: Operation): Source<OperationResult> =>
       pipe(
         fromValue({
+          ...queryResponse,
           operation,
           data: {
             __typename: 'Query',
@@ -1746,7 +1863,10 @@ describe('commutativity', () => {
 
   it('applies optimistic updates on top of commutative queries as query result comes in', () => {
     let data: any;
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -1816,6 +1936,7 @@ describe('commutativity', () => {
     nextOp(queryOpA);
 
     nextRes({
+      ...queryResponse,
       operation: queryOpA,
       data: {
         __typename: 'Query',
@@ -1836,7 +1957,10 @@ describe('commutativity', () => {
 
   it('applies mutation results on top of commutative queries', () => {
     let data: any;
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -1906,6 +2030,7 @@ describe('commutativity', () => {
     nextOp(queryOpB);
 
     nextRes({
+      ...queryResponse,
       operation: queryOpA,
       data: {
         __typename: 'Query',
@@ -1920,6 +2045,7 @@ describe('commutativity', () => {
     expect(data).toHaveProperty('node.name', 'query a');
 
     nextRes({
+      ...queryResponse,
       operation: mutationOp,
       data: {
         __typename: 'Mutation',
@@ -1935,6 +2061,7 @@ describe('commutativity', () => {
     expect(data).toHaveProperty('node.name', 'mutation');
 
     nextRes({
+      ...queryResponse,
       operation: queryOpB,
       data: {
         __typename: 'Query',
@@ -1952,7 +2079,10 @@ describe('commutativity', () => {
 
   it('applies optimistic updates on top of commutative queries until mutation resolves', () => {
     let data: any;
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -2021,6 +2151,7 @@ describe('commutativity', () => {
     nextOp(mutationOp);
 
     nextRes({
+      ...queryResponse,
       operation: queryOp,
       data: {
         __typename: 'Query',
@@ -2035,6 +2166,7 @@ describe('commutativity', () => {
     expect(data).toHaveProperty('node.name', 'optimistic');
 
     nextRes({
+      ...queryResponse,
       operation: mutationOp,
       data: {
         __typename: 'Query',
@@ -2051,7 +2183,10 @@ describe('commutativity', () => {
 
   it('allows subscription results to be commutative when necessary', () => {
     let data: any;
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -2119,6 +2254,7 @@ describe('commutativity', () => {
     nextOp(subscriptionOp);
 
     nextRes({
+      ...queryResponse,
       operation: queryOpA,
       data: {
         __typename: 'Query',
@@ -2131,6 +2267,7 @@ describe('commutativity', () => {
     });
 
     nextRes({
+      ...queryResponse,
       operation: subscriptionOp,
       data: {
         node: {
@@ -2146,7 +2283,10 @@ describe('commutativity', () => {
 
   it('allows subscription results to be commutative above mutations', () => {
     let data: any;
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -2229,6 +2369,7 @@ describe('commutativity', () => {
     nextOp(subscriptionOp);
 
     nextRes({
+      ...queryResponse,
       operation: queryOpA,
       data: {
         __typename: 'Query',
@@ -2243,6 +2384,7 @@ describe('commutativity', () => {
     nextOp(mutationOp);
 
     nextRes({
+      ...queryResponse,
       operation: mutationOp,
       data: {
         node: {
@@ -2254,6 +2396,7 @@ describe('commutativity', () => {
     });
 
     nextRes({
+      ...queryResponse,
       operation: subscriptionOp,
       data: {
         node: {
@@ -2265,6 +2408,7 @@ describe('commutativity', () => {
     });
 
     nextRes({
+      ...queryResponse,
       operation: subscriptionOp,
       data: {
         node: {
@@ -2283,7 +2427,10 @@ describe('commutativity', () => {
     let deferredData: any;
     let combinedData: any;
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const { source: ops$, next: nextOp } = makeSubject<Operation>();
     const { source: res$, next: nextRes } = makeSubject<OperationResult>();
 
@@ -2370,6 +2517,7 @@ describe('commutativity', () => {
     nextOp(normalOp);
 
     nextRes({
+      ...queryResponse,
       operation: deferredOp,
       data: {
         __typename: 'Query',
@@ -2380,6 +2528,7 @@ describe('commutativity', () => {
     expect(deferredData).not.toHaveProperty('deferred');
 
     nextRes({
+      ...queryResponse,
       operation: normalOp,
       data: {
         __typename: 'Query',
@@ -2396,6 +2545,7 @@ describe('commutativity', () => {
     expect(combinedData).toHaveProperty('node.id', 2);
 
     nextRes({
+      ...queryResponse,
       operation: deferredOp,
       data: {
         __typename: 'Query',
diff --git a/exchanges/graphcache/src/offlineExchange.test.ts b/exchanges/graphcache/src/offlineExchange.test.ts
index 384eec39..ff87421b 100644
--- a/exchanges/graphcache/src/offlineExchange.test.ts
+++ b/exchanges/graphcache/src/offlineExchange.test.ts
@@ -9,6 +9,7 @@ import { print } from 'graphql';
 import { vi, expect, it, describe } from 'vitest';
 
 import { pipe, map, makeSubject, tap, publish } from 'wonka';
+import { queryResponse } from '../../../packages/core/src/test-utils';
 import { offlineExchange } from './offlineExchange';
 
 const mutationOne = gql`
@@ -79,7 +80,11 @@ describe('storage', () => {
     const response = vi.fn(
       (forwardOp: Operation): OperationResult => {
         expect(forwardOp.key).toBe(op.key);
-        return { operation: forwardOp, data: mutationOneData };
+        return {
+          ...queryResponse,
+          operation: forwardOp,
+          data: mutationOneData,
+        };
       }
     );
 
@@ -125,10 +130,11 @@ describe('offline', () => {
       (forwardOp: Operation): OperationResult => {
         if (forwardOp.key === queryOp.key) {
           onlineSpy.mockReturnValueOnce(true);
-          return { operation: forwardOp, data: queryOneData };
+          return { ...queryResponse, operation: forwardOp, data: queryOneData };
         } else {
           onlineSpy.mockReturnValueOnce(false);
           return {
+            ...queryResponse,
             operation: forwardOp,
             // @ts-ignore
             error: { networkError: new Error('failed to fetch') },
@@ -184,7 +190,10 @@ describe('offline', () => {
   });
 
   it('should intercept errored queries', async () => {
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const onlineSpy = vi
       .spyOn(navigator, 'onLine', 'get')
       .mockReturnValueOnce(false);
@@ -250,7 +259,10 @@ describe('offline', () => {
 
     const onlineSpy = vi.spyOn(navigator, 'onLine', 'get');
 
-    const client = createClient({ url: 'http://0.0.0.0' });
+    const client = createClient({
+      url: 'http://0.0.0.0',
+      exchanges: [],
+    });
     const reexecuteOperation = vi
       .spyOn(client, 'reexecuteOperation')
       .mockImplementation(() => undefined);
diff --git a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
index 7ea81507..10bd223b 100644
--- a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
+++ b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
@@ -5,6 +5,7 @@ exports[`on error > returns error data 1`] = `
   "data": undefined,
   "error": [CombinedError: [Network] No Content],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": {
@@ -141,6 +142,7 @@ exports[`on error > returns error data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -149,6 +151,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
   "data": undefined,
   "error": [CombinedError: [Network] No Content],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": [MockFunction spy] {
@@ -295,6 +298,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -452,6 +456,7 @@ exports[`on success > returns response data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -587,6 +592,7 @@ exports[`on success > uses a file when given 1`] = `
       "picture": File {},
     },
   },
+  "stale": false,
 }
 `;
 
@@ -728,6 +734,7 @@ exports[`on success > uses multiple files when given 1`] = `
       ],
     },
   },
+  "stale": false,
 }
 `;
 
diff --git a/exchanges/refocus/src/refocusExchange.test.ts b/exchanges/refocus/src/refocusExchange.test.ts
index 178af829..4c89432a 100644
--- a/exchanges/refocus/src/refocusExchange.test.ts
+++ b/exchanges/refocus/src/refocusExchange.test.ts
@@ -9,6 +9,7 @@ import {
   ExchangeIO,
 } from '@urql/core';
 
+import { queryResponse } from '../../../packages/core/src/test-utils';
 import { refocusExchange } from './refocusExchange';
 
 const dispatchDebug = vi.fn();
@@ -33,7 +34,10 @@ const queryOneData = {
 
 let client, op, ops$, next;
 beforeEach(() => {
-  client = createClient({ url: 'http://0.0.0.0' });
+  client = createClient({
+    url: 'http://0.0.0.0',
+    exchanges: [],
+  });
   op = client.createRequestOperation('query', {
     key: 1,
     query: queryOne,
@@ -46,6 +50,7 @@ it(`attaches a listener and redispatches queries on call`, () => {
   const response = vi.fn(
     (forwardOp: Operation): OperationResult => {
       return {
+        ...queryResponse,
         operation: forwardOp,
         data: queryOneData,
       };
diff --git a/exchanges/request-policy/src/requestPolicyExchange.test.ts b/exchanges/request-policy/src/requestPolicyExchange.test.ts
index 16518ed5..98de97e4 100644
--- a/exchanges/request-policy/src/requestPolicyExchange.test.ts
+++ b/exchanges/request-policy/src/requestPolicyExchange.test.ts
@@ -9,6 +9,7 @@ import {
   ExchangeIO,
 } from '@urql/core';
 
+import { queryResponse } from '../../../packages/core/src/test-utils';
 import { requestPolicyExchange } from './requestPolicyExchange';
 
 const dispatchDebug = vi.fn();
@@ -37,7 +38,10 @@ const queryOneData = {
 
 let client, op, ops$, next;
 beforeEach(() => {
-  client = createClient({ url: 'http://0.0.0.0' });
+  client = createClient({
+    url: 'http://0.0.0.0',
+    exchanges: [],
+  });
   op = client.createRequestOperation('query', {
     key: 1,
     query: queryOne,
@@ -50,6 +54,7 @@ it(`upgrades to cache-and-network`, async () => {
   const response = vi.fn(
     (forwardOp: Operation): OperationResult => {
       return {
+        ...queryResponse,
         operation: forwardOp,
         data: queryOneData,
       };
@@ -96,6 +101,7 @@ it(`doesn't upgrade when shouldUpgrade returns false`, async () => {
   const response = vi.fn(
     (forwardOp: Operation): OperationResult => {
       return {
+        ...queryResponse,
         operation: forwardOp,
         data: queryOneData,
       };
diff --git a/exchanges/retry/src/retryExchange.test.ts b/exchanges/retry/src/retryExchange.test.ts
index 55012161..41be980d 100644
--- a/exchanges/retry/src/retryExchange.test.ts
+++ b/exchanges/retry/src/retryExchange.test.ts
@@ -55,7 +55,10 @@ const queryOneError = {
 
 let client, op, ops$, next;
 beforeEach(() => {
-  client = createClient({ url: 'http://0.0.0.0' });
+  client = createClient({
+    url: 'http://0.0.0.0',
+    exchanges: [],
+  });
   op = client.createRequestOperation('query', {
     key: 1,
     query: queryOne,
diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts
index 5680efd6..a1130265 100755
--- a/packages/core/src/client.test.ts
+++ b/packages/core/src/client.test.ts
@@ -31,12 +31,12 @@ const url = 'https://hostname.com';
 
 describe('createClient / Client', () => {
   it('creates an instance of Client', () => {
-    expect(createClient({ url }) instanceof Client).toBeTruthy();
-    expect(new Client({ url }) instanceof Client).toBeTruthy();
+    expect(createClient({ url, exchanges: [] }) instanceof Client).toBeTruthy();
+    expect(new Client({ url, exchanges: [] }) instanceof Client).toBeTruthy();
   });
 
   it('passes snapshot', () => {
-    const client = createClient({ url });
+    const client = createClient({ url, exchanges: [] });
     expect(client).toMatchSnapshot();
   });
 });
@@ -78,7 +78,10 @@ const subscription = {
 };
 
 let receivedOps: Operation[] = [];
-let client = createClient({ url: '1234' });
+let client = createClient({
+  url: '1234',
+  exchanges: [],
+});
 const receiveMock = vi.fn((s: Source<Operation>) =>
   pipe(
     s,
@@ -372,6 +375,8 @@ describe('queuing behavior', () => {
           }
         }),
         map(op => ({
+          stale: false,
+          hasNext: false,
           data: op.key,
           operation: op,
         }))
@@ -427,6 +432,8 @@ describe('queuing behavior', () => {
           ops$,
           filter(op => op.kind !== 'teardown'),
           map(op => ({
+            hasNext: false,
+            stale: false,
             data: ++countRes,
             operation: op,
           })),
@@ -479,7 +486,7 @@ describe('queuing behavior', () => {
 
     expect(output.length).toBe(3);
     expect(output[2]).toHaveProperty('data', 2);
-    expect(output[2]).not.toHaveProperty('stale');
+    expect(output[2]).toHaveProperty('stale', false);
     expect(output[2]).toHaveProperty('operation.key', queryOperation.key);
     expect(output[2]).toHaveProperty(
       'operation.context.requestPolicy',
@@ -499,6 +506,7 @@ describe('queuing behavior', () => {
           data: 1,
           operation: op,
           stale: true,
+          hasNext: false,
         })),
         delay(1)
       );
@@ -562,6 +570,8 @@ describe('shared sources behavior', () => {
       return pipe(
         ops$,
         map(op => ({
+          hasNext: false,
+          stale: false,
           data: ++i,
           operation: op,
         })),
@@ -587,6 +597,8 @@ describe('shared sources behavior', () => {
     expect(resultOne).toHaveBeenCalledWith({
       data: 1,
       operation: queryOperation,
+      stale: false,
+      hasNext: false,
     });
 
     pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
@@ -594,6 +606,8 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 1,
       operation: queryOperation,
+      stale: false,
+      hasNext: false,
     });
 
     vi.advanceTimersByTime(1);
@@ -608,6 +622,8 @@ describe('shared sources behavior', () => {
       return pipe(
         ops$,
         map(op => ({
+          hasNext: false,
+          stale: false,
           data: ++i,
           operation: op,
         })),
@@ -641,6 +657,8 @@ describe('shared sources behavior', () => {
     expect(resultOne).toHaveBeenCalledWith({
       data: 1,
       operation: operationOne,
+      hasNext: false,
+      stale: false,
     });
 
     pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo));
@@ -649,6 +667,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation: operationOne,
       stale: true,
+      hasNext: false,
     });
 
     vi.advanceTimersByTime(1);
@@ -656,6 +675,8 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 2,
       operation: operationTwo,
+      stale: false,
+      hasNext: false,
     });
   });
 
@@ -665,6 +686,8 @@ describe('shared sources behavior', () => {
       return pipe(
         ops$,
         map(op => ({
+          hasNext: false,
+          stale: false,
           data: ++i,
           operation: op,
         })),
@@ -695,6 +718,8 @@ describe('shared sources behavior', () => {
     expect(resultOne).toHaveBeenCalledWith({
       data: 1,
       operation,
+      stale: false,
+      hasNext: false,
     });
 
     pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
@@ -703,6 +728,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation,
       stale: true,
+      hasNext: false,
     });
 
     vi.advanceTimersByTime(1);
@@ -713,6 +739,8 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 2,
       operation,
+      stale: false,
+      hasNext: false,
     });
   });
 
@@ -723,6 +751,8 @@ describe('shared sources behavior', () => {
         ops$,
         filter(op => op.kind !== 'teardown'),
         map(op => ({
+          hasNext: false,
+          stale: false,
           data: ++i,
           operation: op,
         })),
@@ -748,6 +778,8 @@ describe('shared sources behavior', () => {
     expect(resultOne).toHaveBeenCalledWith({
       data: 1,
       operation: queryOperation,
+      hasNext: false,
+      stale: false,
     });
 
     subscription.unsubscribe();
@@ -760,6 +792,8 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 2,
       operation: queryOperation,
+      stale: false,
+      hasNext: false,
     });
   });
 
@@ -771,6 +805,8 @@ describe('shared sources behavior', () => {
         map(op => ({
           data: ++i,
           operation: op,
+          hasNext: false,
+          stale: false,
         })),
         take(1)
       );
@@ -797,6 +833,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation,
       stale: true,
+      hasNext: false,
     });
   });
 
@@ -804,7 +841,7 @@ describe('shared sources behavior', () => {
     const exchange: Exchange = () => ops$ => {
       return pipe(
         ops$,
-        map(op => ({ data: 1, operation: op })),
+        map(op => ({ hasNext: false, stale: false, data: 1, operation: op })),
         filter(() => false)
       );
     };
@@ -830,7 +867,7 @@ describe('shared sources behavior', () => {
       let i = 0;
       return pipe(
         ops$,
-        map(op => ({ data: ++i, operation: op }))
+        map(op => ({ hasNext: false, stale: false, data: ++i, operation: op }))
       );
     };
 
@@ -852,6 +889,8 @@ describe('shared sources behavior', () => {
     expect(resultOne).toHaveBeenCalledWith({
       data: 1,
       operation,
+      hasNext: false,
+      stale: false,
     });
 
     pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
@@ -859,11 +898,15 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 2,
       operation,
+      hasNext: false,
+      stale: false,
     });
 
     expect(resultOne).toHaveBeenCalledWith({
       data: 2,
       operation,
+      hasNext: false,
+      stale: false,
     });
   });
 
@@ -871,7 +914,7 @@ describe('shared sources behavior', () => {
     const exchange: Exchange = () => ops$ => {
       return pipe(
         ops$,
-        map(op => ({ stale: true, data: 1, operation: op })),
+        map(op => ({ hasNext: false, stale: true, data: 1, operation: op })),
         take(1)
       );
     };
@@ -890,6 +933,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation: queryOperation,
       stale: true,
+      hasNext: false,
     });
 
     pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
@@ -898,6 +942,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation: queryOperation,
       stale: true,
+      hasNext: false,
     });
   });
 
@@ -906,7 +951,7 @@ describe('shared sources behavior', () => {
       return merge([
         pipe(
           ops$,
-          map(op => ({ data: 1, operation: op })),
+          map(op => ({ hasNext: true, data: 1, operation: op })),
           take(1)
         ),
         never,
@@ -938,7 +983,7 @@ describe('shared sources behavior', () => {
     const exchange: Exchange = () => ops$ => {
       return pipe(
         ops$,
-        map(op => ({ stale: true, data: 1, operation: op }))
+        map(op => ({ hasNext: false, stale: true, data: 1, operation: op }))
       );
     };
 
@@ -957,6 +1002,7 @@ describe('shared sources behavior', () => {
       data: 1,
       operation: queryOperation,
       stale: true,
+      hasNext: false,
     });
   });
 });
diff --git a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
index 59cc89f2..f7953616 100644
--- a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
+++ b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
@@ -5,6 +5,7 @@ exports[`on error > returns error data 1`] = `
   "data": undefined,
   "error": [CombinedError: [Network] No Content],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": {
@@ -141,6 +142,7 @@ exports[`on error > returns error data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -149,6 +151,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
   "data": undefined,
   "error": [CombinedError: [Network] No Content],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": [MockFunction spy] {
@@ -295,6 +298,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -452,6 +456,7 @@ exports[`on success > returns response data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
diff --git a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
index 0497d395..8de48e47 100644
--- a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
+++ b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
@@ -118,5 +118,6 @@ exports[`should return response data from forwardSubscription observable 1`] = `
       "user": "colin",
     },
   },
+  "stale": false,
 }
 `;
diff --git a/packages/core/src/exchanges/dedup.test.ts b/packages/core/src/exchanges/dedup.test.ts
deleted file mode 100644
index a8e9c23c..00000000
--- a/packages/core/src/exchanges/dedup.test.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
-  filter,
-  makeSubject,
-  map,
-  pipe,
-  publish,
-  Source,
-  Subject,
-} from 'wonka';
-import { vi, expect, it, beforeEach } from 'vitest';
-
-import {
-  mutationOperation,
-  queryOperation,
-  queryResponse,
-} from '../test-utils';
-import { Operation } from '../types';
-import { dedupExchange } from './dedup';
-import { makeOperation } from '../utils';
-
-const dispatchDebug = vi.fn();
-let shouldRespond = false;
-let exchangeArgs;
-let forwardedOperations: Operation[];
-let input: Subject<Operation>;
-
-beforeEach(() => {
-  shouldRespond = false;
-  forwardedOperations = [];
-  input = makeSubject<Operation>();
-
-  // Collect all forwarded operations
-  const forward = (s: Source<Operation>) => {
-    return pipe(
-      s,
-      map(op => {
-        forwardedOperations.push(op);
-        return queryResponse;
-      }),
-      filter(() => !!shouldRespond)
-    );
-  };
-
-  exchangeArgs = { forward, client: {}, dispatchDebug };
-});
-
-it('forwards query operations correctly', async () => {
-  const { source: ops$, next, complete } = input;
-  const exchange = dedupExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(1);
-});
-
-it('forwards only non-pending query operations', async () => {
-  shouldRespond = false; // We filter out our mock responses
-  const { source: ops$, next, complete } = input;
-  const exchange = dedupExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(queryOperation);
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(1);
-});
-
-it('forwards duplicate query operations as usual after they respond', async () => {
-  shouldRespond = true; // Response will immediately resolve
-  const { source: ops$, next, complete } = input;
-  const exchange = dedupExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(queryOperation);
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(2);
-});
-
-it('forwards duplicate query operations after one was torn down', async () => {
-  shouldRespond = false; // We filter out our mock responses
-  const { source: ops$, next, complete } = input;
-  const exchange = dedupExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(queryOperation);
-  next(makeOperation('teardown', queryOperation, queryOperation.context));
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(3);
-});
-
-it('always forwards mutation operations without deduplicating them', async () => {
-  shouldRespond = false; // We filter out our mock responses
-  const { source: ops$, next, complete } = input;
-  const exchange = dedupExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(mutationOperation);
-  next(mutationOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(2);
-});
diff --git a/packages/core/src/exchanges/map.test.ts b/packages/core/src/exchanges/map.test.ts
index 2a6ec2f0..3df71c93 100644
--- a/packages/core/src/exchanges/map.test.ts
+++ b/packages/core/src/exchanges/map.test.ts
@@ -2,7 +2,7 @@ import { map, tap, pipe, fromValue, toArray, toPromise } from 'wonka';
 import { vi, expect, describe, it } from 'vitest';
 
 import { Client } from '../client';
-import { queryOperation } from '../test-utils';
+import { queryResponse, queryOperation } from '../test-utils';
 import { Operation } from '../types';
 import { mapExchange } from './map';
 
@@ -210,7 +210,7 @@ describe('onError', () => {
       forward: op$ =>
         pipe(
           op$,
-          map((operation: Operation) => ({ operation }))
+          map((operation: Operation) => ({ ...queryResponse, operation }))
         ),
       client: {} as Client,
       dispatchDebug: () => null,
diff --git a/packages/core/src/exchanges/ssr.test.ts b/packages/core/src/exchanges/ssr.test.ts
index fe3d62b2..866a2bd5 100644
--- a/packages/core/src/exchanges/ssr.test.ts
+++ b/packages/core/src/exchanges/ssr.test.ts
@@ -47,12 +47,14 @@ it('caches query results correctly', () => {
     [queryOperation.key]: {
       data: serializedQueryResponse.data,
       error: undefined,
+      hasNext: false,
     },
   });
 });
 
 it('serializes query results quickly', () => {
-  const queryResponse: OperationResult = {
+  const result: OperationResult = {
+    ...queryResponse,
     operation: queryOperation,
     data: {
       user: {
@@ -62,11 +64,11 @@ it('serializes query results quickly', () => {
   };
 
   const serializedQueryResponse = {
-    ...queryResponse,
-    data: JSON.stringify(queryResponse.data),
+    ...result,
+    data: JSON.stringify(result.data),
   };
 
-  output.mockReturnValueOnce(queryResponse);
+  output.mockReturnValueOnce(result);
 
   const ssr = ssrExchange();
   const { source: ops$, next } = input;
@@ -74,7 +76,7 @@ it('serializes query results quickly', () => {
 
   publish(exchange);
   next(queryOperation);
-  queryResponse.data.user.name = 'Not Clive';
+  result.data.user.name = 'Not Clive';
 
   const data = ssr.extractData();
   expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
@@ -83,6 +85,7 @@ it('serializes query results quickly', () => {
     [queryOperation.key]: {
       data: serializedQueryResponse.data,
       error: undefined,
+      hasNext: false,
     },
   });
 });
@@ -119,6 +122,7 @@ it('caches errored query results correctly', () => {
         ],
         networkError: undefined,
       },
+      hasNext: false,
     },
   });
 });
@@ -147,6 +151,7 @@ it('caches extensions when includeExtensions=true', () => {
     [queryOperation.key]: {
       data: '{"user":{"name":"Clive"}}',
       extensions: '{"foo":"bar"}',
+      hasNext: false,
     },
   });
 });
@@ -219,8 +224,8 @@ it('resolves deferred, cached query results correctly', () => {
     isClient: true,
     initialState: {
       [queryOperation.key]: {
-        hasNext: true,
         ...(serializedQueryResponse as any),
+        hasNext: true,
       },
     },
   });
diff --git a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
index 408344e9..8d0418c1 100644
--- a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
+++ b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
@@ -5,6 +5,7 @@ exports[`on error > ignores the error when a result is available 1`] = `
   "data": undefined,
   "error": [CombinedError: [Network] Forbidden],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": {
@@ -141,6 +142,7 @@ exports[`on error > ignores the error when a result is available 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -149,6 +151,7 @@ exports[`on error > returns error data 1`] = `
   "data": undefined,
   "error": [CombinedError: [Network] Forbidden],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": {
@@ -285,6 +288,7 @@ exports[`on error > returns error data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -293,6 +297,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
   "data": undefined,
   "error": [CombinedError: [Network] Forbidden],
   "extensions": undefined,
+  "hasNext": false,
   "operation": {
     "context": {
       "fetchOptions": {
@@ -429,6 +434,7 @@ exports[`on error > returns error data with status 400 and manual redirect mode
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -578,6 +584,7 @@ exports[`on success > returns response data 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
 
@@ -759,5 +766,6 @@ exports[`on success > uses the mock fetch if given 1`] = `
       "name": "Clara",
     },
   },
+  "stale": false,
 }
 `;
diff --git a/packages/core/src/test-utils/samples.ts b/packages/core/src/test-utils/samples.ts
index 599c6585..18873e3c 100644
--- a/packages/core/src/test-utils/samples.ts
+++ b/packages/core/src/test-utils/samples.ts
@@ -103,6 +103,8 @@ export const subscriptionOperation: Operation = makeOperation(
 
 export const undefinedQueryResponse: OperationResult = {
   operation: queryOperation,
+  stale: false,
+  hasNext: false,
 };
 
 export const queryResponse: OperationResult = {
@@ -112,11 +114,15 @@ export const queryResponse: OperationResult = {
       name: 'Clive',
     },
   },
+  stale: false,
+  hasNext: false,
 };
 
 export const mutationResponse: OperationResult = {
   operation: mutationOperation,
   data: {},
+  stale: false,
+  hasNext: false,
 };
 
 export const subscriptionResult: ExecutionResult = {
diff --git a/packages/core/src/utils/result.test.ts b/packages/core/src/utils/result.test.ts
index 86346b64..49697480 100644
--- a/packages/core/src/utils/result.test.ts
+++ b/packages/core/src/utils/result.test.ts
@@ -38,6 +38,8 @@ describe('mergeResultPatch', () => {
           },
         ],
       },
+      stale: false,
+      hasNext: true,
     };
 
     const merged = mergeResultPatch(prevResult, {
@@ -77,6 +79,8 @@ describe('mergeResultPatch', () => {
           },
         ],
       },
+      stale: false,
+      hasNext: true,
     };
 
     const patch = { __typename: 'Child' };
@@ -111,6 +115,8 @@ describe('mergeResultPatch', () => {
         __typename: 'Query',
         item: undefined,
       },
+      stale: false,
+      hasNext: true,
     };
 
     const merged = mergeResultPatch(prevResult, {
@@ -133,6 +139,8 @@ describe('mergeResultPatch', () => {
         __typename: 'Query',
         items: [{ __typename: 'Item' }],
       },
+      stale: false,
+      hasNext: true,
     };
 
     const patch = { __typename: 'Item' };
@@ -162,6 +170,8 @@ describe('mergeResultPatch', () => {
         __typename: 'Query',
         items: [{ __typename: 'Item' }],
       },
+      stale: false,
+      hasNext: true,
     };
 
     const merged = mergeResultPatch(prevResult, {
@@ -190,6 +200,8 @@ describe('mergeResultPatch', () => {
       extensions: {
         base: true,
       },
+      stale: false,
+      hasNext: true,
     };
 
     const merged = mergeResultPatch(prevResult, {
@@ -240,6 +252,8 @@ describe('mergeResultPatch', () => {
       extensions: {
         base: true,
       },
+      stale: false,
+      hasNext: true,
     };
 
     const merged = mergeResultPatch(prevResult, {
@@ -264,6 +278,8 @@ describe('mergeResultPatch', () => {
           },
         ],
       },
+      stale: false,
+      hasNext: true,
     };
 
     const patch = { __typename: 'Child' };
diff --git a/packages/next-urql/src/__tests__/with-urql-client.spec.ts b/packages/next-urql/src/__tests__/with-urql-client.spec.ts
index b79b4c56..bef0d00e 100644
--- a/packages/next-urql/src/__tests__/with-urql-client.spec.ts
+++ b/packages/next-urql/src/__tests__/with-urql-client.spec.ts
@@ -1,7 +1,12 @@
 import React, { createElement as h } from 'react';
 import { shallow, configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
-import { Client } from '@urql/core';
+import {
+  Client,
+  dedupExchange,
+  fetchExchange,
+  cacheExchange,
+} from '@urql/core';
 import { vi, expect, it, beforeEach, describe, beforeAll } from 'vitest';
 
 import { withUrqlClient } from '../with-urql-client';
@@ -26,9 +31,15 @@ describe('withUrqlClient', () => {
 
   describe('with client options', () => {
     beforeEach(() => {
-      Component = withUrqlClient(() => ({ url: 'http://localhost:3000' }), {
-        ssr: true,
-      })(MockApp);
+      Component = withUrqlClient(
+        ssr => ({
+          url: 'http://localhost:3000',
+          exchanges: [dedupExchange, cacheExchange, ssr, fetchExchange],
+        }),
+        {
+          ssr: true,
+        }
+      )(MockApp);
     });
 
     const mockContext: NextUrqlPageContext = {
diff --git a/packages/react-urql/src/test-utils/ssr.test.tsx b/packages/react-urql/src/test-utils/ssr.test.tsx
index 0e9d6f45..75e9cd4c 100644
--- a/packages/react-urql/src/test-utils/ssr.test.tsx
+++ b/packages/react-urql/src/test-utils/ssr.test.tsx
@@ -72,6 +72,8 @@ const queryResponse: OperationResult = {
       name: 'Clive',
     },
   },
+  stale: false,
+  hasNext: false,
 };
 
 const url = 'https://hostname.com';
diff --git a/packages/vue-urql/src/useClient.test.ts b/packages/vue-urql/src/useClient.test.ts
index 9da191b5..0c901e62 100644
--- a/packages/vue-urql/src/useClient.test.ts
+++ b/packages/vue-urql/src/useClient.test.ts
@@ -11,6 +11,7 @@ describe('provideClient', () => {
         provideClient(
           new Client({
             url: 'test',
+            exchanges: [],
           })
         );