Repository: urql-graphql/urql
Pull request #3106 (2023-03-27 16:24:57)

feat(core): Allow fetch exchange to handle subscriptions
## Summary

This is a proposal which allows the `fetchExchange` to handle subscriptions.
This has been submitted to validate the following two assumptions:
- `OperationContext.fetchSubscriptions` & `ClientOptions.fetchSubscriptions` may or may not be a good idea
- GraphQL API frameworks like `graphql-yoga` will be able to handle subscriptions via `text/event-stream`/`multipart/mixed` response streams

If these two assumptions are fine, we can go ahead and merge this, I reckon.

## Set of changes

- Add `OperationContext.fetchSubscriptions` & `ClientOptions.fetchSubscriptions`
  - Allow `fetchExchange` to handle subscriptions when enabled
- Switch `Accept` header when a subscription is sent



Patch:
diff --git a/.changeset/rotten-planes-heal.md b/.changeset/rotten-planes-heal.md
new file mode 100644
index 00000000..33067e36
--- /dev/null
+++ b/.changeset/rotten-planes-heal.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-auth': patch
+---
+
+Avoid infinite loop when `didAuthError` keeps returning true
diff --git a/.changeset/tricky-poets-clap.md b/.changeset/tricky-poets-clap.md
new file mode 100644
index 00000000..0b7a33fc
--- /dev/null
+++ b/.changeset/tricky-poets-clap.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': minor
+---
+
+Allow subscriptions to be handled by the `fetchExchange` when `fetchSubscriptions` is turned on.
diff --git a/.gitignore b/.gitignore
index 37668852..a05f7d4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,8 +17,13 @@ exchanges/*/LICENSE
 # TODO: Figure out how to remove these:
 tmp/
 dist/
+examples/yarn.lock
+examples/pnpm-lock.yaml
+examples/package-lock.json
 examples/*/public
 examples/*/yarn.lock
+examples/*/pnpm-lock.yaml
+examples/*/package-lock.json
 examples/*/ios/
 examples/*/android/
 examples/*/.watchmanconfig
diff --git a/examples/pnpm-workspace.yaml b/examples/pnpm-workspace.yaml
new file mode 100644
index 00000000..a2dc0713
--- /dev/null
+++ b/examples/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+packages:
+  - '*'
diff --git a/examples/with-apq/package.json b/examples/with-apq/package.json
index 57346811..4dff0931 100644
--- a/examples/with-apq/package.json
+++ b/examples/with-apq/package.json
@@ -6,14 +6,15 @@
     "start": "vite"
   },
   "dependencies": {
+    "@urql/core": "^3.2.2",
     "@urql/exchange-persisted-fetch": "^1.3.0",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-apq/src/App.jsx b/examples/with-apq/src/App.jsx
index eb7adb16..3a3994be 100644
--- a/examples/with-apq/src/App.jsx
+++ b/examples/with-apq/src/App.jsx
@@ -1,13 +1,13 @@
 import React from 'react';
-import { createClient, Provider, fetchExchange } from 'urql';
-import { persistedFetchExchange } from '@urql/exchange-persisted-fetch';
+import { Client, Provider, fetchExchange } from 'urql';
+import { persistedExchange } from '@urql/exchange-persisted';
 
 import LocationsList from './LocationsList';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/apq-weather',
   exchanges: [
-    persistedFetchExchange({
+    persistedExchange({
       preferGetForPersistedQueries: true,
     }),
     fetchExchange,
diff --git a/examples/with-apq/src/index.jsx b/examples/with-apq/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-apq/src/index.jsx
+++ b/examples/with-apq/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-apq/vite.config.js b/examples/with-apq/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-apq/vite.config.js
+++ b/examples/with-apq/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-defer-stream-directives/package.json b/examples/with-defer-stream-directives/package.json
index cf58c143..70ca028e 100644
--- a/examples/with-defer-stream-directives/package.json
+++ b/examples/with-defer-stream-directives/package.json
@@ -8,21 +8,28 @@
     "client": "vite",
     "start": "run-p client server:yoga"
   },
+  "pnpm": {
+    "peerDependencyRules": {
+      "allowedVersions": {
+        "graphql": "17"
+      }
+    }
+  },
   "dependencies": {
-    "@apollo/server": "^4.4.1",
     "@graphql-yoga/plugin-defer-stream": "^1.7.1",
-    "@urql/core": "^3.1.1",
-    "@urql/exchange-graphcache": "^5.0.9",
+    "@urql/core": "^3.2.2",
+    "@urql/exchange-graphcache": "^5.2.0",
     "graphql": "17.0.0-alpha.2",
-    "graphql-yoga": "^3.7.1",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^3.0.3"
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.6",
+    "@apollo/server": "^4.4.1",
+    "@vitejs/plugin-react": "^3.1.0",
     "graphql-helix": "^1.13.0",
+    "graphql-yoga": "^3.7.1",
     "npm-run-all": "^4.1.5",
-    "vite": "^2.9.15"
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-defer-stream-directives/src/App.jsx b/examples/with-defer-stream-directives/src/App.jsx
index 5037b1e0..4e45e605 100644
--- a/examples/with-defer-stream-directives/src/App.jsx
+++ b/examples/with-defer-stream-directives/src/App.jsx
@@ -1,11 +1,5 @@
 import React from 'react';
-import {
-  createClient,
-  Provider,
-  dedupExchange,
-  debugExchange,
-  fetchExchange,
-} from 'urql';
+import { Client, Provider, fetchExchange } from 'urql';
 
 import { cacheExchange } from '@urql/exchange-graphcache';
 
@@ -18,9 +12,9 @@ const cache = cacheExchange({
   },
 });
 
-const client = createClient({
+const client = new Client({
   url: 'http://localhost:3004/graphql',
-  exchanges: [dedupExchange, cache, debugExchange, fetchExchange],
+  exchanges: [cache, fetchExchange],
 });
 
 function App() {
diff --git a/examples/with-defer-stream-directives/src/index.jsx b/examples/with-defer-stream-directives/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-defer-stream-directives/src/index.jsx
+++ b/examples/with-defer-stream-directives/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-defer-stream-directives/vite.config.js b/examples/with-defer-stream-directives/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-defer-stream-directives/vite.config.js
+++ b/examples/with-defer-stream-directives/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-graphcache-pagination/package.json b/examples/with-graphcache-pagination/package.json
index 84a94140..b8b4efd4 100644
--- a/examples/with-graphcache-pagination/package.json
+++ b/examples/with-graphcache-pagination/package.json
@@ -6,14 +6,15 @@
     "start": "vite"
   },
   "dependencies": {
-    "@urql/exchange-graphcache": "^4.0.0",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "@urql/exchange-graphcache": "^5.2.0",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-graphcache-pagination/src/App.jsx b/examples/with-graphcache-pagination/src/App.jsx
index 264947f6..6f7c0f5a 100644
--- a/examples/with-graphcache-pagination/src/App.jsx
+++ b/examples/with-graphcache-pagination/src/App.jsx
@@ -1,14 +1,13 @@
 import React from 'react';
-import { createClient, Provider, dedupExchange, fetchExchange } from 'urql';
+import { Client, Provider, fetchExchange } from 'urql';
 import { cacheExchange } from '@urql/exchange-graphcache';
 import { relayPagination } from '@urql/exchange-graphcache/extras';
 
 import PaginatedNpmSearch from './PaginatedNpmSearch';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/relay-npm',
   exchanges: [
-    dedupExchange,
     cacheExchange({
       resolvers: {
         Query: {
diff --git a/examples/with-graphcache-pagination/src/index.jsx b/examples/with-graphcache-pagination/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-graphcache-pagination/src/index.jsx
+++ b/examples/with-graphcache-pagination/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-graphcache-pagination/vite.config.js b/examples/with-graphcache-pagination/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-graphcache-pagination/vite.config.js
+++ b/examples/with-graphcache-pagination/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-graphcache-updates/package.json b/examples/with-graphcache-updates/package.json
index cb281a97..eb4d4f62 100644
--- a/examples/with-graphcache-updates/package.json
+++ b/examples/with-graphcache-updates/package.json
@@ -6,15 +6,16 @@
     "start": "vite"
   },
   "dependencies": {
-    "@urql/exchange-auth": "^0.1.2",
-    "@urql/exchange-graphcache": "^4.0.0",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "@urql/exchange-auth": "^2.0.0",
+    "@urql/exchange-graphcache": "^5.2.0",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-graphcache-updates/src/client.js b/examples/with-graphcache-updates/src/client.js
index d6f7b121..ea5cf6ee 100644
--- a/examples/with-graphcache-updates/src/client.js
+++ b/examples/with-graphcache-updates/src/client.js
@@ -1,5 +1,4 @@
-import { createClient, dedupExchange, fetchExchange, gql } from 'urql';
-import { makeOperation } from '@urql/core';
+import { Client, fetchExchange, gql } from 'urql';
 import { authExchange } from '@urql/exchange-auth';
 import { cacheExchange } from '@urql/exchange-graphcache';
 
@@ -40,78 +39,56 @@ const cache = cacheExchange({
   },
 });
 
-const client = createClient({
-  url: 'https://trygql.formidable.dev/graphql/web-collections',
-  exchanges: [
-    dedupExchange,
-    cache,
-    authExchange({
-      getAuth: async ({ authState }) => {
-        if (!authState) {
-          const token = localStorage.getItem('authToken');
-
-          if (token) {
-            return { token };
-          }
-
-          return null;
-        }
+const auth = authExchange(async utilities => {
+  let token = localStorage.getItem('authToken');
 
+  return {
+    addAuthToOperation(operation) {
+      if (!token) return operation;
+      return token
+        ? utilities.appendHeaders(operation, {
+            Authorization: `Bearer ${token}`,
+          })
+        : operation;
+    },
+    didAuthError(error) {
+      return error.graphQLErrors.some(
+        e => e.extensions?.code === 'UNAUTHORIZED'
+      );
+    },
+    willAuthError(operation) {
+      if (!token) {
+        // Detect our login mutation and let this operation through:
+        return (
+          operation.kind !== 'mutation' ||
+          // Here we find any mutation definition with the "signin" field
+          !operation.query.definitions.some(definition => {
+            return (
+              definition.kind === 'OperationDefinition' &&
+              definition.selectionSet.selections.some(node => {
+                // The field name is just an example, since register may also be an exception
+                return node.kind === 'Field' && node.name.value === 'signin';
+              })
+            );
+          })
+        );
+      }
+      return false;
+    },
+    async refreshAuth() {
+      token = localStorage.getItem('authToken');
+      if (!token) {
         // This is where auth has gone wrong and we need to clean up and redirect to a login page
         localStorage.clear();
         window.location.reload();
+      }
+    },
+  };
+});
 
-        return null;
-      },
-      addAuthToOperation: ({ authState, operation }) => {
-        if (!authState || !authState.token) {
-          return operation;
-        }
-
-        const fetchOptions =
-          typeof operation.context.fetchOptions === 'function'
-            ? operation.context.fetchOptions()
-            : operation.context.fetchOptions || {};
-
-        return makeOperation(operation.kind, operation, {
-          ...operation.context,
-          fetchOptions: {
-            ...fetchOptions,
-            headers: {
-              ...fetchOptions.headers,
-              Authorization: `Bearer ${authState.token}`,
-            },
-          },
-        });
-      },
-      didAuthError: ({ error }) => {
-        return error.graphQLErrors.some(
-          e => e.extensions?.code === 'UNAUTHORIZED'
-        );
-      },
-      willAuthError: ({ operation, authState }) => {
-        if (!authState) {
-          // Detect our login mutation and let this operation through:
-          return (
-            operation.kind !== 'mutation' ||
-            // Here we find any mutation definition with the "signin" field
-            !operation.query.definitions.some(definition => {
-              return (
-                definition.kind === 'OperationDefinition' &&
-                definition.selectionSet.selections.some(node => {
-                  // The field name is just an example, since register may also be an exception
-                  return node.kind === 'Field' && node.name.value === 'signin';
-                })
-              );
-            })
-          );
-        }
-
-        return false;
-      },
-    }),
-    fetchExchange,
-  ],
+const client = new Client({
+  url: 'https://trygql.formidable.dev/graphql/web-collections',
+  exchanges: [cache, auth, fetchExchange],
 });
 
 export default client;
diff --git a/examples/with-graphcache-updates/src/index.jsx b/examples/with-graphcache-updates/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-graphcache-updates/src/index.jsx
+++ b/examples/with-graphcache-updates/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-graphcache-updates/vite.config.js b/examples/with-graphcache-updates/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-graphcache-updates/vite.config.js
+++ b/examples/with-graphcache-updates/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-multipart/package.json b/examples/with-multipart/package.json
index 24f682e6..06a457ff 100644
--- a/examples/with-multipart/package.json
+++ b/examples/with-multipart/package.json
@@ -6,14 +6,14 @@
     "start": "vite"
   },
   "dependencies": {
-    "@urql/exchange-multipart-fetch": "^0.1.11",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-multipart/src/App.jsx b/examples/with-multipart/src/App.jsx
index bb88aeaf..6c45911c 100644
--- a/examples/with-multipart/src/App.jsx
+++ b/examples/with-multipart/src/App.jsx
@@ -1,12 +1,11 @@
 import React from 'react';
-import { createClient, Provider } from 'urql';
-import { multipartFetchExchange } from '@urql/exchange-multipart-fetch';
+import { Client, Provider, fetchExchange } from 'urql';
 
 import FileUpload from './FileUpload';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/uploads-mock',
-  exchanges: [multipartFetchExchange],
+  exchanges: [fetchExchange],
 });
 
 function App() {
diff --git a/examples/with-multipart/src/index.jsx b/examples/with-multipart/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-multipart/src/index.jsx
+++ b/examples/with-multipart/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-multipart/vite.config.js b/examples/with-multipart/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-multipart/vite.config.js
+++ b/examples/with-multipart/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-next/package.json b/examples/with-next/package.json
index 79e5d3bc..ddb7d899 100644
--- a/examples/with-next/package.json
+++ b/examples/with-next/package.json
@@ -3,12 +3,13 @@
   "version": "0.0.0",
   "private": true,
   "dependencies": {
-    "graphql": "^15.5.0",
-    "next": "10.1.2",
-    "next-urql": "^3.0.1",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "graphql": "^16.6.0",
+    "next": "13.2.4",
+    "next-urql": "^4.0.3",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "scripts": {
     "dev": "next dev",
diff --git a/examples/with-next/pages/_app.js b/examples/with-next/pages/_app.js
index 25d5d9ff..828487cf 100644
--- a/examples/with-next/pages/_app.js
+++ b/examples/with-next/pages/_app.js
@@ -1,10 +1,12 @@
 import { withUrqlClient } from 'next-urql';
+import { cacheExchange, fetchExchange } from 'urql';
 
 const App = ({ Component, pageProps }) => <Component {...pageProps} />;
 
 export default withUrqlClient(
-  () => ({
+  ssrExchange => ({
     url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
+    exchanges: [cacheExchange, ssrExchange, fetchExchange],
   }),
   { ssr: false }
 )(App);
diff --git a/examples/with-next/pages/index.js b/examples/with-next/pages/index.js
index f8209410..57e9dead 100644
--- a/examples/with-next/pages/index.js
+++ b/examples/with-next/pages/index.js
@@ -1,5 +1,5 @@
 import { withUrqlClient } from 'next-urql';
-import { useQuery, gql } from 'urql';
+import { useQuery, cacheExchange, fetchExchange, gql } from 'urql';
 
 const POKEMONS_QUERY = gql`
   query {
@@ -26,8 +26,9 @@ function Index() {
 }
 
 export default withUrqlClient(
-  () => ({
+  ssrExchange => ({
     url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
+    exchanges: [cacheExchange, ssrExchange, fetchExchange],
   }),
   { ssr: true }
 )(Index);
diff --git a/examples/with-next/pages/server.js b/examples/with-next/pages/server.js
index 7351f3ab..819d2c49 100644
--- a/examples/with-next/pages/server.js
+++ b/examples/with-next/pages/server.js
@@ -1,12 +1,6 @@
 import { initUrqlClient } from 'next-urql';
-import {
-  ssrExchange,
-  dedupExchange,
-  cacheExchange,
-  fetchExchange,
-  useQuery,
-  gql,
-} from 'urql';
+
+import { ssrExchange, cacheExchange, fetchExchange, useQuery, gql } from 'urql';
 
 const POKEMONS_QUERY = gql`
   query {
@@ -37,7 +31,7 @@ export async function getServerSideProps() {
   const client = initUrqlClient(
     {
       url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
-      exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
+      exchanges: [cacheExchange, ssrCache, fetchExchange],
     },
     false
   );
diff --git a/examples/with-next/pages/static.js b/examples/with-next/pages/static.js
index 793ca103..7832a670 100644
--- a/examples/with-next/pages/static.js
+++ b/examples/with-next/pages/static.js
@@ -1,12 +1,6 @@
 import { initUrqlClient } from 'next-urql';
-import {
-  ssrExchange,
-  dedupExchange,
-  cacheExchange,
-  fetchExchange,
-  useQuery,
-  gql,
-} from 'urql';
+
+import { ssrExchange, cacheExchange, fetchExchange, useQuery, gql } from 'urql';
 
 const POKEMONS_QUERY = gql`
   query {
@@ -37,7 +31,7 @@ export async function getStaticProps() {
   const client = initUrqlClient(
     {
       url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
-      exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
+      exchanges: [cacheExchange, ssrCache, fetchExchange],
     },
     false
   );
diff --git a/examples/with-pagination/package.json b/examples/with-pagination/package.json
index bcafd737..33ee35bb 100644
--- a/examples/with-pagination/package.json
+++ b/examples/with-pagination/package.json
@@ -6,13 +6,14 @@
     "start": "vite"
   },
   "dependencies": {
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-pagination/src/App.jsx b/examples/with-pagination/src/App.jsx
index 07e38aa3..f1d02127 100644
--- a/examples/with-pagination/src/App.jsx
+++ b/examples/with-pagination/src/App.jsx
@@ -1,10 +1,11 @@
 import React from 'react';
-import { createClient, Provider } from 'urql';
+import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
 
 import PaginatedNpmSearch from './PaginatedNpmSearch';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/relay-npm',
+  exchanges: [cacheExchange, fetchExchange],
 });
 
 function App() {
diff --git a/examples/with-pagination/src/index.jsx b/examples/with-pagination/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-pagination/src/index.jsx
+++ b/examples/with-pagination/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-pagination/vite.config.js b/examples/with-pagination/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-pagination/vite.config.js
+++ b/examples/with-pagination/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-react-native/App.js b/examples/with-react-native/App.js
index 3a9d5dac..2ed5d010 100644
--- a/examples/with-react-native/App.js
+++ b/examples/with-react-native/App.js
@@ -1,10 +1,11 @@
 import React from 'react';
-import { createClient, Provider } from 'urql';
+import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
 
 import PokemonList from './src/screens/PokemonList';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
+  exchanges: [cacheExchange, fetchExchange],
 });
 
 const App = () => {
diff --git a/examples/with-react-native/package.json b/examples/with-react-native/package.json
index f5568dad..b9efb2d9 100644
--- a/examples/with-react-native/package.json
+++ b/examples/with-react-native/package.json
@@ -8,15 +8,16 @@
     "start": "react-native start"
   },
   "dependencies": {
-    "graphql": "^15.5.0",
-    "react": "17.0.1",
-    "react-native": "0.64.0",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "graphql": "^16.6.0",
+    "react": "18.2.0",
+    "react-native": "0.71.4",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
     "@babel/core": "^7.12.9",
+    "@babel/preset-env": "^7.20.2",
     "@babel/runtime": "^7.12.5",
-    "babel-jest": "^26.6.3",
-    "metro-react-native-babel-preset": "^0.64.0"
+    "metro-react-native-babel-preset": "^0.76.0"
   }
 }
diff --git a/examples/with-react/package.json b/examples/with-react/package.json
index b52642ba..0f7bb365 100644
--- a/examples/with-react/package.json
+++ b/examples/with-react/package.json
@@ -6,13 +6,14 @@
     "start": "vite"
   },
   "dependencies": {
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-react/src/App.jsx b/examples/with-react/src/App.jsx
index cc972792..c993b525 100644
--- a/examples/with-react/src/App.jsx
+++ b/examples/with-react/src/App.jsx
@@ -1,10 +1,11 @@
 import React from 'react';
-import { createClient, Provider } from 'urql';
+import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
 
 import PokemonList from './PokemonList';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
+  exchanges: [cacheExchange, fetchExchange],
 });
 
 function App() {
diff --git a/examples/with-react/src/index.jsx b/examples/with-react/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-react/src/index.jsx
+++ b/examples/with-react/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-react/vite.config.js b/examples/with-react/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-react/vite.config.js
+++ b/examples/with-react/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-refresh-auth/package.json b/examples/with-refresh-auth/package.json
index 4ab49277..cef4da25 100644
--- a/examples/with-refresh-auth/package.json
+++ b/examples/with-refresh-auth/package.json
@@ -6,14 +6,15 @@
     "start": "vite"
   },
   "dependencies": {
-    "@urql/exchange-auth": "^0.1.2",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "@urql/exchange-auth": "^2.0.0",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-refresh-auth/src/client.js b/examples/with-refresh-auth/src/client.js
index 03213ac2..8a41c6eb 100644
--- a/examples/with-refresh-auth/src/client.js
+++ b/examples/with-refresh-auth/src/client.js
@@ -1,12 +1,4 @@
-import {
-  makeOperation,
-  createClient,
-  dedupExchange,
-  fetchExchange,
-  cacheExchange,
-  gql,
-} from 'urql';
-
+import { Client, fetchExchange, cacheExchange, gql } from 'urql';
 import { authExchange } from '@urql/exchange-auth';
 
 import {
@@ -25,92 +17,70 @@ const REFRESH_TOKEN_MUTATION = gql`
   }
 `;
 
-const client = createClient({
-  url: 'https://trygql.formidable.dev/graphql/web-collections',
-  exchanges: [
-    dedupExchange,
-    cacheExchange,
-    authExchange({
-      async getAuth({ authState, mutate }) {
-        if (!authState) {
-          const token = getToken();
-          const refreshToken = getRefreshToken();
-
-          if (token && refreshToken) {
-            return { token, refreshToken };
-          }
-
-          return null;
-        }
-
-        const result = await mutate(REFRESH_TOKEN_MUTATION, {
-          refreshToken: authState.refreshToken,
+const auth = authExchange(async utilities => {
+  let token = getToken();
+  let refreshToken = getRefreshToken();
+
+  return {
+    addAuthToOperation(operation) {
+      return token
+        ? utilities.appendHeaders(operation, {
+            Authorization: `Bearer ${token}`,
+          })
+        : operation;
+    },
+    didAuthError(error) {
+      return error.graphQLErrors.some(
+        e => e.extensions?.code === 'UNAUTHORIZED'
+      );
+    },
+    willAuthError(operation) {
+      // Sync tokens on every operation
+      token = getToken();
+      refreshToken = getRefreshToken();
+
+      if (!token) {
+        // Detect our login mutation and let this operation through:
+        return (
+          operation.kind !== 'mutation' ||
+          // Here we find any mutation definition with the "signin" field
+          !operation.query.definitions.some(definition => {
+            return (
+              definition.kind === 'OperationDefinition' &&
+              definition.selectionSet.selections.some(node => {
+                // The field name is just an example, since register may also be an exception
+                return node.kind === 'Field' && node.name.value === 'signin';
+              })
+            );
+          })
+        );
+      }
+      return false;
+    },
+    async refreshAuth() {
+      if (refreshToken) {
+        const result = await utilities.mutate(REFRESH_TOKEN_MUTATION, {
+          refreshToken,
         });
 
         if (result.data?.refreshCredentials) {
-          saveAuthData(result.data.refreshCredentials);
-
-          return result.data.refreshCredentials;
+          token = result.data.refreshCredentials.token;
+          refreshToken = result.data.refreshCredentials.refreshToken;
+          saveAuthData({ token, refreshToken });
+          return;
         }
+      }
 
-        // This is where auth has gone wrong and we need to clean up and redirect to a login page
-        clearStorage();
-        window.location.reload();
-
-        return null;
-      },
-
-      addAuthToOperation({ authState, operation }) {
-        if (!authState || !authState.token) {
-          return operation;
-        }
-
-        const fetchOptions =
-          typeof operation.context.fetchOptions === 'function'
-            ? operation.context.fetchOptions()
-            : operation.context.fetchOptions || {};
-
-        return makeOperation(operation.kind, operation, {
-          ...operation.context,
-          fetchOptions: {
-            ...fetchOptions,
-            headers: {
-              ...fetchOptions.headers,
-              Authorization: `Bearer ${authState.token}`,
-            },
-          },
-        });
-      },
-
-      didAuthError({ error }) {
-        return error.graphQLErrors.some(
-          e => e.extensions?.code === 'UNAUTHORIZED'
-        );
-      },
-
-      willAuthError({ operation, authState }) {
-        if (!authState) {
-          // Detect our login mutation and let this operation through:
-          return (
-            operation.kind !== 'mutation' ||
-            // Here we find any mutation definition with the "signin" field
-            !operation.query.definitions.some(definition => {
-              return (
-                definition.kind === 'OperationDefinition' &&
-                definition.selectionSet.selections.some(node => {
-                  // The field name is just an example, since register may also be an exception
-                  return node.kind === 'Field' && node.name.value === 'signin';
-                })
-              );
-            })
-          );
-        }
+      // This is where auth has gone wrong and we need to clean up and redirect to a login page
+      clearStorage();
+      window.location.reload();
+    },
+  };
+});
 
-        return false;
-      },
-    }),
-    fetchExchange,
-  ],
+const client = new Client({
+  url: 'https://trygql.formidable.dev/graphql/web-collections',
+  exchanges: [cacheExchange, auth, fetchExchange],
 });
 
 export default client;
diff --git a/examples/with-refresh-auth/src/index.jsx b/examples/with-refresh-auth/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-refresh-auth/src/index.jsx
+++ b/examples/with-refresh-auth/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-refresh-auth/vite.config.js b/examples/with-refresh-auth/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-refresh-auth/vite.config.js
+++ b/examples/with-refresh-auth/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-retry/package.json b/examples/with-retry/package.json
index a9755bd3..48712f7a 100644
--- a/examples/with-retry/package.json
+++ b/examples/with-retry/package.json
@@ -6,14 +6,15 @@
     "start": "vite"
   },
   "dependencies": {
-    "@urql/exchange-retry": "^0.2.0",
-    "graphql": "^15.5.0",
-    "react": "^17.0.2",
-    "react-dom": "^17.0.2",
-    "urql": "^2.0.2"
+    "@urql/core": "^3.2.2",
+    "@urql/exchange-retry": "^1.0.0",
+    "graphql": "^16.6.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "urql": "^3.0.4"
   },
   "devDependencies": {
-    "@vitejs/plugin-react-refresh": "^1.3.3",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-react": "^3.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-retry/src/App.jsx b/examples/with-retry/src/App.jsx
index 4ba224f3..97eca32a 100644
--- a/examples/with-retry/src/App.jsx
+++ b/examples/with-retry/src/App.jsx
@@ -1,14 +1,15 @@
 import React from 'react';
-import { createClient, fetchExchange, Provider } from 'urql';
+import { Client, fetchExchange, Provider } from 'urql';
 import { retryExchange } from '@urql/exchange-retry';
 
 import Color from './Color';
 
-const client = createClient({
+const client = new Client({
   url: 'https://trygql.formidable.dev/graphql/intermittent-colors',
   exchanges: [
     retryExchange({
-      maxNumberAttempts: 5,
+      maxNumberAttempts: 10,
+      maxDelayMs: 500,
       retryIf: error => {
         // NOTE: With this deemo schema we have a specific random error to look out for:
         return (
diff --git a/examples/with-retry/src/Color.jsx b/examples/with-retry/src/Color.jsx
index 96b990a6..a92a0e74 100644
--- a/examples/with-retry/src/Color.jsx
+++ b/examples/with-retry/src/Color.jsx
@@ -29,8 +29,8 @@ const RandomColorDisplay = () => {
 
       {result.operation && (
         <p>
-          We retried {result.operation.context.retryCount} times to get a result
-          without an error.
+          To get a result, the retry exchange retried:{' '}
+          {result.operation.context.retryCount || 0} times.
         </p>
       )}
     </div>
diff --git a/examples/with-retry/src/index.jsx b/examples/with-retry/src/index.jsx
index f16bd3d1..7b6e0272 100644
--- a/examples/with-retry/src/index.jsx
+++ b/examples/with-retry/src/index.jsx
@@ -1,11 +1,6 @@
 import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
 
 import App from './App';
 
-render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
-);
+createRoot(document.getElementById('root')).render(<App />);
diff --git a/examples/with-retry/vite.config.js b/examples/with-retry/vite.config.js
index 75f50ede..627a3196 100644
--- a/examples/with-retry/vite.config.js
+++ b/examples/with-retry/vite.config.js
@@ -1,7 +1,7 @@
 import { defineConfig } from 'vite';
-import reactRefresh from '@vitejs/plugin-react-refresh';
+import react from '@vitejs/plugin-react';
 
 // https://vitejs.dev/config/
 export default defineConfig({
-  plugins: [reactRefresh()],
+  plugins: [react()],
 });
diff --git a/examples/with-svelte/package.json b/examples/with-svelte/package.json
index c8583ff1..38f93e8e 100644
--- a/examples/with-svelte/package.json
+++ b/examples/with-svelte/package.json
@@ -7,12 +7,14 @@
     "build": "vite build",
     "serve": "vite preview"
   },
-  "devDependencies": {
-    "@sveltejs/vite-plugin-svelte": "^1.0.0-next.44",
-    "vite": "^2.2.4"
-  },
   "dependencies": {
-    "@urql/svelte": "^2.0.0",
-    "svelte": "^3.48.0"
+    "@urql/core": "^3.2.2",
+    "@urql/svelte": "^3.0.4",
+    "graphql": "^16.6.0",
+    "svelte": "^3.57.0"
+  },
+  "devDependencies": {
+    "@sveltejs/vite-plugin-svelte": "^2.0.3",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-svelte/src/App.svelte b/examples/with-svelte/src/App.svelte
index fa4da8b7..073dcf0c 100644
--- a/examples/with-svelte/src/App.svelte
+++ b/examples/with-svelte/src/App.svelte
@@ -1,9 +1,10 @@
 <script>
-  import { setContextClient, createClient } from "@urql/svelte";
+  import { setContextClient, Client, cacheExchange, fetchExchange } from "@urql/svelte";
   import PokemonList from "./PokemonList.svelte";
 
-  setContextClient(createClient({
-    url: "https://trygql.formidable.dev/graphql/basic-pokedex"
+  setContextClient(new Client({
+    url: "https://trygql.formidable.dev/graphql/basic-pokedex",
+    exchanges: [cacheExchange, fetchExchange],
   }));
 </script>
 
diff --git a/examples/with-vue3/package.json b/examples/with-vue3/package.json
index 7c4479d5..bbebe86b 100644
--- a/examples/with-vue3/package.json
+++ b/examples/with-vue3/package.json
@@ -8,12 +8,13 @@
     "serve": "vite preview"
   },
   "dependencies": {
-    "@urql/vue": "0.3.0",
-    "vue": "^3.0.11"
+    "@urql/core": "^3.2.2",
+    "@urql/vue": "^1.0.5",
+    "graphql": "^16.6.0",
+    "vue": "^3.2.47"
   },
   "devDependencies": {
-    "@vitejs/plugin-vue": "^1.2.2",
-    "@vue/compiler-sfc": "^3.0.11",
-    "vite": "^2.2.4"
+    "@vitejs/plugin-vue": "^4.1.0",
+    "vite": "^4.2.0"
   }
 }
diff --git a/examples/with-vue3/src/App.vue b/examples/with-vue3/src/App.vue
index edac1caf..8179f640 100644
--- a/examples/with-vue3/src/App.vue
+++ b/examples/with-vue3/src/App.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import { provideClient } from '@urql/vue';
+import { provideClient, cacheExchange, fetchExchange } from '@urql/vue';
 import PokemonList from './PokemonList.vue'
 
 export default {
@@ -18,6 +18,7 @@ export default {
   setup() {
     provideClient({
       url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
+      exchanges: [cacheExchange, fetchExchange],
     });
   },
   components: {
diff --git a/exchanges/auth/src/authExchange.ts b/exchanges/auth/src/authExchange.ts
index 3fdfa520..d5a878de 100644
--- a/exchanges/auth/src/authExchange.ts
+++ b/exchanges/auth/src/authExchange.ts
@@ -270,6 +270,7 @@ export function authExchange(
           operation.key,
           addAuthAttemptToOperation(operation, true)
         );
+
         // check that another operation isn't already doing refresh
         if (config && !authPromise) {
           authPromise = config.refreshAuth().finally(flushQueue);
@@ -310,7 +311,9 @@ export function authExchange(
       const opsWithAuth$ = pipe(
         merge([retries.source, pendingOps$]),
         map(operation => {
-          if (bypassQueue.has(operation)) {
+          if (operation.context.authAttempt) {
+            return addAuthToOperation(operation);
+          } else if (bypassQueue.has(operation)) {
             return operation;
           } else if (authPromise) {
             if (!retryQueue.has(operation.key)) {
diff --git a/package.json b/package.json
index e7d7b0c7..45866590 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
     "build": "node ./scripts/actions/build-all.mjs",
     "postinstall": "node ./scripts/prepare/postinstall.js",
     "pack": "node ./scripts/actions/pack-all.mjs",
-    "changeset:version": "changeset version && pnpm install --lockfile-only",
+    "changeset:version": "node ./scripts/changesets/version.mjs",
     "changeset:publish": "changeset publish"
   },
   "eslintConfig": {
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 9790e5fc..c076e3a2 100755
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -100,6 +100,16 @@ export interface ClientOptions {
    * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec.
    */
   fetch?: typeof fetch;
+  /** Allows a subscription to be executed using a `fetch` API request.
+   *
+   * @remarks
+   * If your API supports the `text/event-stream` and/or `multipart/mixed` response protocol, and you use
+   * this protocol to handle subscriptions, then you may switch this flag to `true`.
+   *
+   * This means you won’t have to create a {@link subscriptionExchange} to handle subscriptions with an
+   * external transport, and will instead be able to use GraphQL over HTTP transports.
+   */
+  fetchSubscriptions?: boolean;
   /** A list of `Exchange`s that will be used to create the `Client`'s execution pipeline.
    *
    * @remarks
@@ -631,20 +641,18 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
         // Add `stale: true` flag when a new operation is sent for queries
         switchMap(result => {
           const value$ = fromValue(result);
-          return result.stale
+          return result.stale || result.hasNext
             ? value$
             : merge([
                 value$,
                 pipe(
                   operations.source,
-                  filter(
-                    op =>
-                      op.kind === 'query' &&
-                      op.key === operation.key &&
-                      op.context.requestPolicy !== 'cache-only'
-                  ),
+                  filter(op => op.key === operation.key),
                   take(1),
-                  map(() => ({ ...result, stale: true }))
+                  map(() => {
+                    result.stale = true;
+                    return result;
+                  })
                 ),
               ]);
         })
@@ -750,37 +758,29 @@ export const Client: new (opts: ClientOptions) => Client = function Client(
             active.set(operation.key, (source = makeResultSource(operation)));
           }
 
-          const isNetworkOperation =
-            operation.context.requestPolicy === 'cache-and-network' ||
-            operation.context.requestPolicy === 'network-only';
-          const replay = replays.get(operation.key);
+          source = pipe(
+            source,
+            onStart(() => {
+              dispatchOperation(operation);
+            })
+          );
 
-          if (operation.kind !== 'query' || !replay || isNetworkOperation) {
-            source = pipe(
-              source,
-              onStart(() => {
-                dispatchOperation(operation);
-              })
+          const replay = replays.get(operation.key);
+          if (
+            operation.kind === 'query' &&
+            replay &&
+            (replay.stale || replay.hasNext)
+          ) {
+            return pipe(
+              merge([
+                source,
+                pipe(
+                  fromValue(replay),
+                  filter(replay => replay === replays.get(operation.key))
+                ),
+              ]),
+              switchMap(fromValue)
             );
-          }
-
-          if (operation.kind === 'query' && replay) {
-            return merge([
-              source,
-              pipe(
-                fromValue(replay),
-                filter(replay => {
-                  if (replay === replays.get(operation.key)) {
-                    if (isNetworkOperation && !replay.hasNext)
-                      replay.stale = true;
-                    return true;
-                  } else {
-                    if (!isNetworkOperation) dispatchOperation(operation);
-                    return false;
-                  }
-                })
-              ),
-            ]);
           } else {
             return source;
           }
diff --git a/packages/core/src/exchanges/fetch.ts b/packages/core/src/exchanges/fetch.ts
index 39edbdeb..07c914bb 100755
--- a/packages/core/src/exchanges/fetch.ts
+++ b/packages/core/src/exchanges/fetch.ts
@@ -31,7 +31,11 @@ export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => {
     const fetchResults$ = pipe(
       ops$,
       filter(operation => {
-        return operation.kind === 'query' || operation.kind === 'mutation';
+        return (
+          operation.kind !== 'teardown' &&
+          (operation.kind !== 'subscription' ||
+            !!operation.context.fetchSubscriptions)
+        );
       }),
       mergeMap(operation => {
         const body = makeFetchBody(operation);
@@ -87,7 +91,11 @@ export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => {
     const forward$ = pipe(
       ops$,
       filter(operation => {
-        return operation.kind !== 'query' && operation.kind !== 'mutation';
+        return (
+          operation.kind === 'teardown' ||
+          (operation.kind === 'subscription' &&
+            !operation.context.fetchSubscriptions)
+        );
       }),
       forward
     );
diff --git a/packages/core/src/internal/fetchOptions.ts b/packages/core/src/internal/fetchOptions.ts
index cc698e81..7aa6513f 100644
--- a/packages/core/src/internal/fetchOptions.ts
+++ b/packages/core/src/internal/fetchOptions.ts
@@ -121,7 +121,9 @@ export const makeFetchOptions = (
 ): RequestInit => {
   const headers: HeadersInit = {
     accept:
-      'application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed',
+      operation.kind === 'subscription'
+        ? 'text/event-stream, multipart/mixed'
+        : 'application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed',
   };
   const extraOptions =
     (typeof operation.context.fetchOptions === 'function'
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b41c7e9b..1614a922 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -473,6 +473,16 @@ export interface OperationContext {
    * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} for a description of this object.
    */
   fetchOptions?: RequestInit | (() => RequestInit);
+  /** Allows the `fetchExchange` to handle subscriptions.
+   *
+   * @remarks
+   * When set to `true`, subscriptions are allowed to be handled by the {@link fetchExchange} and will
+   * be sent using a `fetch` call as GraphQL over HTTP requests.
+   * This may be enabled on {@link ClientOptions.fetchSubscriptions} when your API supports the
+   * `text/event-stream` and `multipart/mixed` response protocols and is able to use them to
+   * respond with subscription results.
+   */
+  fetchSubscriptions?: boolean;
   /** The request and caching strategy instructing cache exchanges how to treat cached results.
    *
    * @remarks
diff --git a/packages/svelte-urql/src/subscriptionStore.ts b/packages/svelte-urql/src/subscriptionStore.ts
index 8bb15d72..dd80c70a 100644
--- a/packages/svelte-urql/src/subscriptionStore.ts
+++ b/packages/svelte-urql/src/subscriptionStore.ts
@@ -127,10 +127,11 @@ export type SubscriptionArgs<
  *       newNotification { id, text }
  *     }
  *   `,
+ *   },
  *   function combineNotifications(notifications = [], data) {
  *     return [...notifications, data.newNotification];
  *   },
- * });
+ * );
  * ```
  */
 export function subscriptionStore<
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index dbda806b..cca7fbe7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,3 +1,4 @@
 packages:
   - 'packages/*'
   - 'exchanges/*'
+  - '!examples/*'
diff --git a/scripts/actions/lib/packages.mjs b/scripts/actions/lib/packages.mjs
index 969cfdc0..3a890527 100644
--- a/scripts/actions/lib/packages.mjs
+++ b/scripts/actions/lib/packages.mjs
@@ -1,4 +1,5 @@
 import * as path from 'path';
+import * as fs from 'fs/promises';
 import glob from 'glob';
 
 import { workspaceRoot, workspaces, require } from './constants.mjs';
@@ -6,6 +7,30 @@ import { workspaceRoot, workspaces, require } from './constants.mjs';
 const getPackageManifest = (cwd) =>
   require(path.resolve(cwd, 'package.json'));
 
+const updatePackageManifest = async (cwd, manifest) => {
+  const sortDependencies = (dependencies) => {
+    if (dependencies == null)
+      return undefined;
+    return Object.keys(dependencies).sort().reduce((acc, key) => {
+      acc[key] = dependencies[key];
+      return acc;
+    }, {});
+  };
+
+  try {
+    if (!!getPackageManifest(cwd)) {
+      manifest.dependencies = sortDependencies(manifest.dependencies);
+      manifest.devDependencies = sortDependencies(manifest.devDependencies);
+      await fs.writeFile(
+        path.resolve(cwd, 'package.json'),
+        JSON.stringify(manifest, null, 2) + '\n',
+      );
+    }
+  } catch (_error) {
+    throw new Error('package.json does not exist in: ' + cwd);
+  }
+};
+
 const getPackageArtifact = (cwd) => {
   const pkg = getPackageManifest(cwd);
   const name =
@@ -48,6 +73,7 @@ const listArtifacts = async () => {
 
 export {
   getPackageManifest,
+  updatePackageManifest,
   getPackageArtifact,
   listPackages,
   listArtifacts,
diff --git a/scripts/changesets/version.mjs b/scripts/changesets/version.mjs
new file mode 100644
index 00000000..a14a775d
--- /dev/null
+++ b/scripts/changesets/version.mjs
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+import glob from 'glob';
+import { execa } from 'execa';
+
+import {
+  getPackageManifest,
+  updatePackageManifest,
+  listPackages
+} from '../actions/lib/packages.mjs';
+
+const versionRe = /^\d+\.\d+\.\d+/i;
+const execaOpts = { stdio: 'inherit' };
+
+await execa('changeset', ['version'], execaOpts);
+await execa('pnpm', ['install', '--lockfile-only'], execaOpts);
+
+const packages = (await listPackages()).reduce((map, dir) => {
+  const manifest = getPackageManifest(dir);
+  const versionMatch = manifest.version.match(versionRe);
+  if (versionMatch) {
+    const { name } = manifest;
+    const version = `^${versionMatch[0]}`;
+    map[name] = version;
+  }
+  return map;
+}, {});
+
+const examples = (await glob('./examples/*/')).filter(x => !/node_modules$/.test(x));
+console.log(`Scope: updating ${examples.length} examples`)
+for (const example of examples) {
+  let hadMatch = false;
+
+  const manifest = getPackageManifest(example);
+
+  if (manifest.dependencies) {
+    for (const name in manifest.dependencies) {
+      hadMatch = hadMatch || !!packages[name];
+      if (packages[name] && packages[name] !== manifest.dependencies)
+        manifest.dependencies[name] = packages[name];
+    }
+  }
+
+  if (manifest.devDependencies) {
+    for (const name in manifest.devDependencies) {
+      hadMatch = hadMatch || !!packages[name];
+      if (packages[name] && packages[name] !== manifest.devDependencies)
+        manifest.devDependencies[name] = packages[name];
+    }
+  }
+
+  if (
+    hadMatch &&
+    !(manifest.devDependencies || {})['@urql/core'] &&
+    !(manifest.dependencies || {})['@urql/core']
+  ) {
+    (manifest.dependencies || manifest.devDependencies || {})['@urql/core']
+      = packages['@urql/core'];
+  }
+
+  await updatePackageManifest(example, manifest);
+}


Test patch:
diff --git a/exchanges/auth/src/authExchange.test.ts b/exchanges/auth/src/authExchange.test.ts
index 7795e342..b9045a87 100644
--- a/exchanges/auth/src/authExchange.test.ts
+++ b/exchanges/auth/src/authExchange.test.ts
@@ -158,7 +158,7 @@ it('supports calls to the mutate() method in refreshAuth()', async () => {
     },
   });
 
-  expect(res.operation.context.authAttempt).toBe(false);
+  expect(res.operation.context.authAttempt).toBe(true);
   expect(res.operation.context.fetchOptions).toEqual({
     method: 'POST',
     headers: {
@@ -385,3 +385,60 @@ it('calls willAuthError on queued operations', async () => {
     'final-token'
   );
 });
+
+it('does not infinitely retry authentication when an operation did error', async () => {
+  const { exchangeArgs, result, operations } = makeExchangeArgs();
+  const { source, next } = makeSubject<any>();
+
+  const didAuthError = vi.fn().mockReturnValue(true);
+
+  pipe(
+    source,
+    authExchange(async utils => {
+      let token = 'initial-token';
+      return {
+        addAuthToOperation(operation) {
+          return utils.appendHeaders(operation, {
+            Authorization: token,
+          });
+        },
+        didAuthError,
+        async refreshAuth() {
+          token = 'final-token';
+        },
+      };
+    })(exchangeArgs),
+    publish
+  );
+
+  await new Promise(resolve => setTimeout(resolve));
+
+  result.mockImplementation(x => ({
+    ...queryResponse,
+    operation: {
+      ...queryResponse.operation,
+      ...x,
+    },
+    data: undefined,
+    error: new CombinedError({
+      graphQLErrors: [{ message: 'Oops' }],
+    }),
+  }));
+
+  next(queryOperation);
+  expect(result).toHaveBeenCalledTimes(1);
+  expect(didAuthError).toHaveBeenCalledTimes(1);
+
+  await new Promise(resolve => setTimeout(resolve));
+
+  expect(result).toHaveBeenCalledTimes(2);
+  expect(operations.length).toBe(2);
+  expect(operations[0]).toHaveProperty(
+    'context.fetchOptions.headers.Authorization',
+    'initial-token'
+  );
+  expect(operations[1]).toHaveProperty(
+    'context.fetchOptions.headers.Authorization',
+    'final-token'
+  );
+});
diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts
index 847cd671..46e35ca8 100755
--- a/packages/core/src/client.test.ts
+++ b/packages/core/src/client.test.ts
@@ -709,14 +709,14 @@ describe('shared sources behavior', () => {
     expect(resultTwo).toHaveBeenCalledWith({
       data: 1,
       operation: queryOperation,
-      stale: false,
+      stale: true,
       hasNext: false,
     });
 
     vi.advanceTimersByTime(1);
 
     // With cache-first we don't expect a new operation to be issued
-    expect(resultTwo).toHaveBeenCalledTimes(1);
+    expect(resultTwo).toHaveBeenCalledTimes(2);
   });
 
   it('dispatches the correct request policy on subsequent sources', async () => {
@@ -834,10 +834,21 @@ describe('shared sources behavior', () => {
       hasNext: false,
     });
 
+    expect(resultOne).toHaveBeenCalledWith({
+      data: 1,
+      operation,
+      stale: true,
+      hasNext: false,
+    });
+
+    expect(resultTwo).toHaveBeenCalledTimes(1);
+    expect(resultOne).toHaveBeenCalledTimes(2);
+
     vi.advanceTimersByTime(1);
 
     // With network-only we expect a new operation to be issued, hence a new result
     expect(resultTwo).toHaveBeenCalledTimes(2);
+    expect(resultOne).toHaveBeenCalledTimes(3);
 
     expect(resultTwo).toHaveBeenCalledWith({
       data: 2,
@@ -845,6 +856,13 @@ describe('shared sources behavior', () => {
       stale: false,
       hasNext: false,
     });
+
+    expect(resultOne).toHaveBeenCalledWith({
+      data: 2,
+      operation,
+      stale: false,
+      hasNext: false,
+    });
   });
 
   it('does not replay values from a past subscription', async () => {