Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

fix(core): Fix subscriptions via fetch (#3244)

Changed files
+256 -6
.changeset
examples
with-defer-stream-directives
with-subscriptions-via-fetch
packages
core
src
+5
.changeset/few-snails-burn.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Add missing `fetchSubscriptions` entry to `OperationContext`. The Client’s `fetchSubscriptions` now works properly and can be used to execute subscriptions as multipart/event-stream requests.
+5
.changeset/lazy-needles-give.md
···
+
---
+
'@urql/core': patch
+
---
+
+
Fix `fetchSource` not working for subscriptions since `hasNext` isn’t necessarily set.
+1 -1
examples/with-defer-stream-directives/README.md
···
This example contains:
- The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx)
-
- A local `polka` server set up to test deferred and streamed results in [`server/`](server/).
+
- A local `graphql-yoga` server set up to test deferred and streamed results in [`server/`](server/).
-1
examples/with-defer-stream-directives/package.json
···
"devDependencies": {
"@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": "^4.2.0"
+25
examples/with-subscriptions-via-fetch/README.md
···
+
# With Subscriptions via Fetch
+
+
This example shows `urql` in use with subscriptions running via a plain `fetch`
+
HTTP request to GraphQL Yoga. This uses the [GraphQL Server-Sent
+
Events](https://the-guild.dev/blog/graphql-over-sse) protocol, which means that
+
the request streams in more results via a single HTTP response.
+
+
This example also includes Graphcache ["Cache
+
Updates"](https://formidable.com/open-source/urql/docs/graphcache/cache-updates/)
+
to update a list with incoming items from the subscriptions.
+
+
To run this example install dependencies and run the `start` script:
+
+
```sh
+
yarn install
+
yarn run start
+
# or
+
npm install
+
npm run start
+
```
+
+
This example contains:
+
+
- The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx)
+
- A local `graphql-yoga` server set up to test subscriptions in [`server/`](server/).
+27
examples/with-subscriptions-via-fetch/index.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<title>with-defer-stream-directives</title>
+
<style>
+
body {
+
margin: 0;
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
+
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
+
'Helvetica Neue', sans-serif;
+
-webkit-font-smoothing: antialiased;
+
-moz-osx-font-smoothing: grayscale;
+
}
+
+
code {
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+
monospace;
+
}
+
</style>
+
</head>
+
<body>
+
<div id="root"></div>
+
<script type="module" src="/src/index.jsx"></script>
+
</body>
+
</html>
+24
examples/with-subscriptions-via-fetch/package.json
···
+
{
+
"name": "with-subscriptions-via-fetch",
+
"version": "0.0.0",
+
"private": true,
+
"scripts": {
+
"server": "node server/graphql-yoga.js",
+
"client": "vite",
+
"start": "run-p client server"
+
},
+
"dependencies": {
+
"@urql/core": "^4.0.9",
+
"@urql/exchange-graphcache": "^6.1.1",
+
"graphql": "^16.6.0",
+
"react": "^18.2.0",
+
"react-dom": "^18.2.0",
+
"urql": "^4.0.3"
+
},
+
"devDependencies": {
+
"@vitejs/plugin-react": "^3.1.0",
+
"graphql-yoga": "^3.7.1",
+
"npm-run-all": "^4.1.5",
+
"vite": "^4.2.0"
+
}
+
}
+7
examples/with-subscriptions-via-fetch/server/graphql-yoga.js
···
+
const { createYoga } = require('graphql-yoga');
+
const { createServer } = require('http');
+
const { schema } = require('./schema');
+
+
const yoga = createYoga({ schema });
+
const server = createServer(yoga);
+
server.listen(3004);
+48
examples/with-subscriptions-via-fetch/server/schema.js
···
+
const {
+
GraphQLList,
+
GraphQLObjectType,
+
GraphQLSchema,
+
GraphQLString,
+
} = require('graphql');
+
+
const Alphabet = new GraphQLObjectType({
+
name: 'Alphabet',
+
fields: {
+
char: {
+
type: GraphQLString,
+
},
+
},
+
});
+
+
const schema = new GraphQLSchema({
+
query: new GraphQLObjectType({
+
name: 'Query',
+
fields: () => ({
+
list: {
+
type: new GraphQLList(Alphabet),
+
resolve() {
+
return [{ char: 'Where are my letters?' }];
+
},
+
},
+
}),
+
}),
+
subscription: new GraphQLObjectType({
+
name: 'Subscription',
+
fields: () => ({
+
alphabet: {
+
type: Alphabet,
+
resolve(root) {
+
return root;
+
},
+
subscribe: async function* () {
+
for (let letter = 65; letter <= 90; letter++) {
+
await new Promise(resolve => setTimeout(resolve, 500));
+
yield { char: String.fromCharCode(letter) };
+
}
+
},
+
},
+
}),
+
}),
+
});
+
+
module.exports = { schema };
+37
examples/with-subscriptions-via-fetch/src/App.jsx
···
+
import React from 'react';
+
import { Client, Provider, fetchExchange } from 'urql';
+
+
import { cacheExchange } from '@urql/exchange-graphcache';
+
+
import Songs from './Songs';
+
+
const cache = cacheExchange({
+
keys: {
+
Alphabet: data => data.char,
+
},
+
updates: {
+
Subscription: {
+
alphabet(parent, _args, cache) {
+
const list = cache.resolve('Query', 'list') || [];
+
list.push(parent.alphabet);
+
cache.link('Query', 'list', list);
+
},
+
},
+
},
+
});
+
+
const client = new Client({
+
url: 'http://localhost:3004/graphql',
+
fetchSubscriptions: true,
+
exchanges: [cache, fetchExchange],
+
});
+
+
function App() {
+
return (
+
<Provider value={client}>
+
<Songs />
+
</Provider>
+
);
+
}
+
+
export default App;
+59
examples/with-subscriptions-via-fetch/src/Songs.jsx
···
+
import React from 'react';
+
import { gql, useQuery, useSubscription } from 'urql';
+
+
const LIST_QUERY = gql`
+
query List_Query {
+
list {
+
char
+
}
+
}
+
`;
+
+
const SONG_SUBSCRIPTION = gql`
+
subscription App_Subscription {
+
alphabet {
+
char
+
}
+
}
+
`;
+
+
const ListQuery = () => {
+
const [listResult] = useQuery({
+
query: LIST_QUERY,
+
});
+
return (
+
<div>
+
<h3>List</h3>
+
{listResult?.data?.list.map(i => (
+
<div key={i.char}>{i.char}</div>
+
))}
+
</div>
+
);
+
};
+
+
const SongSubscription = () => {
+
const [songsResult] = useSubscription(
+
{ query: SONG_SUBSCRIPTION },
+
(prev = [], data) => [...prev, data.alphabet]
+
);
+
+
return (
+
<div>
+
<h3>Song</h3>
+
{songsResult?.data?.map(i => (
+
<div key={i.char}>{i.char}</div>
+
))}
+
</div>
+
);
+
};
+
+
const LocationsList = () => {
+
return (
+
<>
+
<ListQuery />
+
<SongSubscription />
+
</>
+
);
+
};
+
+
export default LocationsList;
+6
examples/with-subscriptions-via-fetch/src/index.jsx
···
+
import React from 'react';
+
import { createRoot } from 'react-dom/client';
+
+
import App from './App';
+
+
createRoot(document.getElementById('root')).render(<App />);
+7
examples/with-subscriptions-via-fetch/vite.config.js
···
+
import { defineConfig } from 'vite';
+
import react from '@vitejs/plugin-react';
+
+
// https://vitejs.dev/config/
+
export default defineConfig({
+
plugins: [react()],
+
});
+1
packages/core/src/client.ts
···
const baseOpts = {
url: opts.url,
+
fetchSubscriptions: opts.fetchSubscriptions,
fetchOptions: opts.fetchOptions,
fetch: opts.fetch,
preferGetMethod: !!opts.preferGetMethod,
+4 -4
packages/core/src/internal/fetchSource.ts
···
} catch (error) {
if (!payload) throw error;
}
-
if (payload && !payload.hasNext) break;
+
if (payload && payload.hasNext === false) break;
}
}
-
if (payload && payload.hasNext) {
+
if (payload && payload.hasNext !== false) {
yield { hasNext: false };
}
}
···
} catch (error) {
if (!payload) throw error;
}
-
if (payload && !payload.hasNext) break;
+
if (payload && payload.hasNext === false) break;
}
-
if (payload && payload.hasNext) {
+
if (payload && payload.hasNext !== false) {
yield { hasNext: false };
}
}