Skip to content

Commit ed71098

Browse files
committed
feat!: add websocket server option
BREAKING CHANGE: the subscription server export is now an abstract class
1 parent 5c0f46f commit ed71098

18 files changed

+1640
-210
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
11
# GraphQL Subscription Server
2+
3+
A subscription server for GraphQL subscriptions. Supports streaming over plain web sockets
4+
or Socket.IO, and integrates with Redis or any other Pub/Sub service.
5+
6+
## Setup
7+
8+
### Socket.IO
9+
10+
```js
11+
import http from 'http';
12+
import {
13+
SocketIOSubscriptionServer, // or WebSocketSubscriptionServer
14+
JwtCredentialManager,
15+
RedisSubscriber,
16+
} from '@4c/graphql-subscription-server';
17+
18+
const server = http.createServer();
19+
20+
const subscriptionServer = new SocketIOSubscriptionServer({
21+
schema,
22+
path: '/socket.io/graphql',
23+
subscriber: new RedisSubscriber(),
24+
hasPermission: (message, credentials) => {
25+
authorize(message, credentials);
26+
},
27+
createCredentialsManager: (req) => new JwtCredentialManager(),
28+
createLogger: () => console.debug,
29+
});
30+
31+
subscriptionServer.attach(server);
32+
33+
server.listen(4000, () => {
34+
console.log('server running');
35+
});
36+
```

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"tdd": "jest --watch",
2020
"test": "yarn lint && yarn typecheck && jest",
2121
"testonly": "jest",
22-
"typecheck": "tsc --noEmit && tsc -p test --noEmit"
22+
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
23+
"update-schema": "NODE_ENV=test babel-node ./update-schema.js"
2324
},
2425
"husky": {
2526
"hooks": {
@@ -51,7 +52,7 @@
5152
"express": "^4.17.1",
5253
"graphql-ws": "^4.3.2",
5354
"redis": "^3.0.0",
54-
"ws": "^7.4.4"
55+
"ws": "^7.4.5"
5556
},
5657
"peerDependencies": {
5758
"graphql": ">=0.12.3",
@@ -71,6 +72,7 @@
7172
"@4c/tsconfig": "^0.3.1",
7273
"@babel/cli": "^7.12.10",
7374
"@babel/core": "^7.12.10",
75+
"@babel/node": "^7.14.2",
7476
"@babel/preset-typescript": "^7.12.7",
7577
"@types/express": "^4.17.9",
7678
"@types/jest": "^26.0.19",
@@ -88,13 +90,16 @@
8890
"eslint-plugin-jsx-a11y": "^6.4.1",
8991
"eslint-plugin-prettier": "^3.3.0",
9092
"graphql": "^15.4.0",
93+
"graphql-relay": "^0.6.0",
94+
"graphql-relay-subscription": "^0.3.1",
9195
"husky": "^4.3.6",
9296
"jest": "^26.6.3",
9397
"lint-staged": "^10.5.3",
9498
"prettier": "^2.2.1",
9599
"redis-mock": "^0.55.1",
96100
"semantic-release": "^17.3.0",
97-
"socket.io": "^3.0.4",
101+
"socket.io": "^4.1.2",
102+
"socket.io-client": "^4.1.2",
98103
"travis-deploy-once": "^5.0.11",
99104
"typescript": "^4.1.3",
100105
"utility-types": "^3.10.0"

src/AuthorizedSocketConnection.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import {
99
validate,
1010
} from 'graphql';
1111
import { ExecutionResult } from 'graphql/execution/execute';
12-
import io from 'socket.io';
1312

1413
import * as AsyncUtils from './AsyncUtils';
1514
import { CredentialsManager } from './CredentialsManager';
1615
import { CreateLogger, Logger } from './Logger';
1716
import { Subscriber } from './Subscriber';
1817
import SubscriptionContext from './SubscriptionContext';
18+
import { WebSocket } from './types';
1919

2020
export type CreateValidationRules = ({
2121
query,
@@ -62,7 +62,7 @@ const acknowledge = (cb?: () => void) => {
6262
* - Rudimentary connection constraints (max connections)
6363
*/
6464
export default class AuthorizedSocketConnection<TContext, TCredentials> {
65-
socket: io.Socket;
65+
socket: WebSocket;
6666

6767
config: AuthorizedSocketOptions<TContext, TCredentials>;
6868

@@ -74,7 +74,7 @@ export default class AuthorizedSocketConnection<TContext, TCredentials> {
7474
>;
7575

7676
constructor(
77-
socket: io.Socket,
77+
socket: WebSocket,
7878
config: AuthorizedSocketOptions<TContext, TCredentials>,
7979
) {
8080
this.socket = socket;
@@ -83,12 +83,11 @@ export default class AuthorizedSocketConnection<TContext, TCredentials> {
8383
this.log = config.createLogger('@4c/SubscriptionServer::AuthorizedSocket');
8484
this.subscriptionContexts = new Map();
8585

86-
this.socket
87-
.on('authenticate', this.handleAuthenticate)
88-
.on('subscribe', this.handleSubscribe)
89-
.on('unsubscribe', this.handleUnsubscribe)
90-
.on('connect', this.handleConnect)
91-
.on('disconnect', this.handleDisconnect);
86+
this.socket.on('authenticate', this.handleAuthenticate);
87+
this.socket.on('subscribe', this.handleSubscribe);
88+
this.socket.on('unsubscribe', this.handleUnsubscribe);
89+
this.socket.on('connect', this.handleConnect);
90+
this.socket.on('disconnect', this.handleDisconnect);
9291
}
9392

9493
emitError(error: { code: string; data?: any }) {

src/GraphqlSocketSubscriptionServer.ts

Lines changed: 0 additions & 110 deletions
This file was deleted.

src/SocketIOSubscriptionServer.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { promisify } from 'util';
2+
3+
import express from 'express';
4+
import type io from 'socket.io';
5+
6+
import SubscriptionServer, {
7+
SubscriptionServerConfig,
8+
} from './SubscriptionServer';
9+
10+
export interface SocketIOSubscriptionServerConfig<TContext, TCredentials>
11+
extends SubscriptionServerConfig<TContext, TCredentials> {
12+
socketIoServer?: io.Server;
13+
}
14+
15+
export default class SocketIOSubscriptionServer<
16+
TContext,
17+
TCredentials
18+
> extends SubscriptionServer<TContext, TCredentials> {
19+
io: io.Server;
20+
21+
constructor({
22+
socketIoServer,
23+
...config
24+
}: SocketIOSubscriptionServerConfig<TContext, TCredentials>) {
25+
super(config);
26+
27+
this.io = socketIoServer!;
28+
if (!this.io) {
29+
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
30+
const IoServer = require('socket.io').Server;
31+
this.io = new IoServer({
32+
serveClient: false,
33+
path: this.config.path,
34+
transports: ['websocket'],
35+
allowEIO3: true,
36+
});
37+
}
38+
39+
this.io.on('connection', (socket: io.Socket) => {
40+
const request = Object.create((express as any).request);
41+
Object.assign(request, socket.request);
42+
this.opened(
43+
{
44+
id: socket.id,
45+
protocol: 'socket-io',
46+
on: socket.on.bind(socket),
47+
emit(event: string, data: any) {
48+
socket.emit(event, data);
49+
},
50+
close() {
51+
socket.disconnect();
52+
},
53+
},
54+
request,
55+
);
56+
});
57+
}
58+
59+
attach(httpServer: any) {
60+
this.io.attach(httpServer);
61+
}
62+
63+
async close() {
64+
// @ts-ignore
65+
await promisify((...args) => this.io.close(...args))();
66+
}
67+
}

0 commit comments

Comments
 (0)