Skip to content

Commit 47cae71

Browse files
committed
add sse
1 parent 1a561b3 commit 47cae71

File tree

11 files changed

+166
-64
lines changed

11 files changed

+166
-64
lines changed

packages/graphiql-toolkit/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,22 @@
3232
"devDependencies": {
3333
"graphql": "^17.0.0-alpha.7",
3434
"graphql-ws": "^5.5.5",
35+
"graphql-sse": "^2.5.3",
3536
"isomorphic-fetch": "^3.0.0",
3637
"subscriptions-transport-ws": "0.11.0",
3738
"tsup": "^8.2.4"
3839
},
3940
"peerDependencies": {
4041
"graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2",
41-
"graphql-ws": ">= 4.5.0"
42+
"graphql-ws": ">= 4.5.0",
43+
"graphql-sse": "^2"
4244
},
4345
"peerDependenciesMeta": {
4446
"graphql-ws": {
4547
"optional": true
48+
},
49+
"graphql-sse": {
50+
"optional": true
4651
}
4752
},
4853
"keywords": [
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type {
2+
ClientOptions,
3+
createClient as createClientType,
4+
ExecutionResult,
5+
} from 'graphql-sse';
6+
import { Fetcher, FetcherParams } from './types';
7+
8+
export async function createSseFetcher(opts: ClientOptions): Promise<Fetcher> {
9+
const { createClient } =
10+
process.env.USE_IMPORT === 'false'
11+
? (require('graphql-sse') as { createClient: typeof createClientType })
12+
: await import('graphql-sse');
13+
14+
const sseClient = createClient({
15+
retryAttempts: 0,
16+
// @ts-expect-error
17+
singleConnection: true, // or use false if you have an HTTP/2 server
18+
// @ts-expect-error
19+
lazy: false, // connect as soon as the page opens
20+
...opts,
21+
});
22+
23+
function subscribe(payload: FetcherParams) {
24+
let deferred: {
25+
resolve: (arg: boolean) => void;
26+
reject: (arg: unknown) => void;
27+
};
28+
29+
const pending: ExecutionResult<Record<string, unknown>, unknown>[] = [];
30+
let throwMe: unknown;
31+
let done = false;
32+
33+
const dispose = sseClient.subscribe(
34+
{
35+
...payload,
36+
// types are different with FetcherParams
37+
operationName: payload.operationName ?? undefined,
38+
},
39+
{
40+
next(data) {
41+
pending.push(data);
42+
deferred?.resolve(false);
43+
},
44+
error(err) {
45+
throwMe = err;
46+
deferred?.reject(throwMe);
47+
},
48+
complete() {
49+
done = true;
50+
deferred?.resolve(true);
51+
},
52+
},
53+
);
54+
55+
return {
56+
[Symbol.asyncIterator]() {
57+
return this;
58+
},
59+
async next() {
60+
if (done) {
61+
return { done: true, value: undefined };
62+
}
63+
if (throwMe) {
64+
throw throwMe;
65+
}
66+
if (pending.length) {
67+
return { value: pending.shift() };
68+
}
69+
return (await new Promise((resolve, reject) => {
70+
deferred = { resolve, reject };
71+
}))
72+
? { done: true, value: undefined }
73+
: { value: pending.shift() };
74+
},
75+
async return() {
76+
dispose();
77+
return { done: true, value: undefined };
78+
},
79+
};
80+
}
81+
82+
// @ts-expect-error todo: fix type
83+
return subscribe;
84+
}

packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {
66
isSubscriptionWithName,
77
getWsFetcher,
88
} from './lib';
9+
import { createSseFetcher } from './create-sse-fetcher';
910

1011
/**
11-
* build a GraphiQL fetcher that is:
12+
* Build a GraphiQL fetcher that is:
1213
* - backwards compatible
13-
* - optionally supports graphql-ws or `
14+
* - optionally supports graphql-ws or graphql-sse
1415
*/
1516
export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
1617
const httpFetch =
@@ -40,7 +41,13 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
4041
graphQLParams.operationName || undefined,
4142
)
4243
: false;
44+
4345
if (isSubscription) {
46+
if (options.sseUrl) {
47+
const sseFetcher = await createSseFetcher({ url: options.sseUrl });
48+
return sseFetcher(graphQLParams);
49+
}
50+
4451
const wsFetcher = await getWsFetcher(options, fetcherOpts);
4552

4653
if (!wsFetcher) {

packages/graphiql-toolkit/src/create-fetcher/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ export interface CreateFetcherOptions {
8484
* url for websocket subscription requests
8585
*/
8686
subscriptionUrl?: string;
87+
/**
88+
* url for graphql-sse
89+
*/
90+
sseUrl?: string;
8791
/**
8892
* `wsClient` implementation that matches `ws-graphql` signature,
8993
* whether via `createClient()` itself or another client.

packages/graphiql-toolkit/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const opts: Options = {
44
entry: ['src/**/*.ts', '!**/__tests__'],
55
bundle: false,
66
clean: true,
7-
dts: true,
87
minifySyntax: true,
98
};
109

@@ -17,6 +16,7 @@ export default defineConfig([
1716
env: {
1817
USE_IMPORT: 'true',
1918
},
19+
dts: true,
2020
},
2121
{
2222
...opts,

packages/graphiql/cypress/e2e/incremental-delivery.cy.ts

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,16 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => {
1919
const mockStreamSuccess = {
2020
data: {
2121
streamable: [
22-
{
23-
text: 'Hi',
24-
},
25-
{
26-
text: '你好',
27-
},
28-
{
29-
text: 'Hola',
30-
},
31-
{
32-
text: 'أهلاً',
33-
},
34-
{
35-
text: 'Bonjour',
36-
},
37-
{
38-
text: 'سلام',
39-
},
40-
{
41-
text: '안녕',
42-
},
43-
{
44-
text: 'Ciao',
45-
},
46-
{
47-
text: 'हेलो',
48-
},
49-
{
50-
text: 'Здорово',
51-
},
22+
{ text: 'Hi' },
23+
{ text: '你好' },
24+
{ text: 'Hola' },
25+
{ text: 'أهلاً' },
26+
{ text: 'Bonjour' },
27+
{ text: 'سلام' },
28+
{ text: '안녕' },
29+
{ text: 'Ciao' },
30+
{ text: 'हेलो' },
31+
{ text: 'Здорово' },
5232
],
5333
},
5434
};
@@ -141,22 +121,10 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => {
141121
person: {
142122
name: 'Mark',
143123
friends: [
144-
{
145-
name: 'James',
146-
age: 1000,
147-
},
148-
{
149-
name: 'Mary',
150-
age: 1000,
151-
},
152-
{
153-
name: 'John',
154-
age: 1000,
155-
},
156-
{
157-
name: 'Patrica',
158-
age: 1000,
159-
},
124+
{ name: 'James', age: 1000 },
125+
{ name: 'Mary', age: 1000 },
126+
{ name: 'John', age: 1000 },
127+
{ name: 'Patrica', age: 1000 },
160128
],
161129
age: 1000,
162130
},

packages/graphiql/resources/renderExample.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ root.render(
7878
fetcher: GraphiQL.createFetcher({
7979
url: getSchemaUrl(),
8080
subscriptionUrl: 'ws://localhost:8081/subscriptions',
81+
sseUrl: parameters.sseUrl
8182
}),
8283
query: parameters.query,
8384
variables: parameters.variables,

packages/graphiql/test/beforeDevServer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const express = require('express');
99
const path = require('node:path');
1010
// eslint-disable-next-line import-x/no-extraneous-dependencies
1111
const { createHandler } = require('graphql-http/lib/use/express');
12-
const schema = require('./schema');
12+
const { schema } = require('./schema');
1313
const badSchema = require('../cypress/fixtures/bad-schema.json');
1414

1515
module.exports = function beforeDevServer(app, _server, _compiler) {

packages/graphiql/test/e2e-server.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ const {
1515
sendResult,
1616
} = require('graphql-helix'); // update when `graphql-http` is upgraded to support multipart requests for incremental delivery https://github.com/graphql/graphiql/pull/3682#discussion_r1715545279
1717
const WebSocketsServer = require('./afterDevServer');
18-
const schema = require('./schema');
18+
const { schema, sseSchema } = require('./schema');
1919
const { customExecute } = require('./execute');
20+
const { createHandler } = require('graphql-sse/lib/use/express');
2021

2122
const app = express();
2223

@@ -42,6 +43,33 @@ async function handler(req, res) {
4243
sendResult(result, res);
4344
}
4445

46+
app.use('/graphql/stream', (req, res, next) => {
47+
// Fixes
48+
// Access to fetch at 'http://localhost:8080/graphql/stream' from origin 'http://localhost:5173' has been blocked by
49+
// CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin'
50+
// header is present on the requested resource. If an opaque response serves your needs, set the request's mode to
51+
// 'no-cors' to fetch the resource with CORS disabled.
52+
53+
// CORS headers
54+
res.header('Access-Control-Allow-Origin', '*'); // restrict it to the required domain
55+
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST');
56+
// Set custom headers for CORS
57+
res.header(
58+
'Access-Control-Allow-Headers',
59+
'content-type,x-graphql-event-stream-token',
60+
);
61+
62+
if (req.method === 'OPTIONS') {
63+
return res.status(200).end();
64+
}
65+
next();
66+
});
67+
68+
// Create the GraphQL over SSE handler
69+
const sseHandler = createHandler({ schema: sseSchema });
70+
// Serve all methods on `/graphql/stream`
71+
app.use('/graphql/stream', sseHandler);
72+
4573
// Server
4674
app.use(express.json());
4775

packages/graphiql/test/schema.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* This source code is licensed under the MIT license found in the
66
* LICENSE file in the root directory of this source tree.
77
*/
8-
9-
const graphql = require('graphql');
10-
118
const {
129
GraphQLSchema,
1310
GraphQLObjectType,
@@ -25,12 +22,7 @@ const {
2522
GraphQLStreamDirective,
2623
specifiedDirectives,
2724
version,
28-
} = graphql;
29-
30-
const directives =
31-
parseInt(version, 10) > 16
32-
? [...specifiedDirectives, GraphQLDeferDirective, GraphQLStreamDirective]
33-
: specifiedDirectives;
25+
} = require('graphql');
3426

3527
// Test Schema
3628
const TestEnum = new GraphQLEnumType({
@@ -392,12 +384,20 @@ const TestSubscriptionType = new GraphQLObjectType({
392384
},
393385
});
394386

395-
const myTestSchema = new GraphQLSchema({
387+
const schemaConfig = {
396388
query: TestType,
397389
mutation: TestMutationType,
398390
subscription: TestSubscriptionType,
399391
description: 'This is a test schema for GraphiQL',
400-
directives,
392+
};
393+
394+
exports.schema = new GraphQLSchema({
395+
...schemaConfig,
396+
directives:
397+
parseInt(version, 10) > 16
398+
? [...specifiedDirectives, GraphQLDeferDirective, GraphQLStreamDirective]
399+
: specifiedDirectives,
401400
});
402401

403-
module.exports = myTestSchema;
402+
// Same schema but without defer/stream directives
403+
exports.sseSchema = new GraphQLSchema(schemaConfig);

0 commit comments

Comments
 (0)