graphql-ws
Advanced tools
Comparing version 5.9.1 to 5.10.0
@@ -8,2 +8,4 @@ import type { FastifyRequest } from 'fastify'; | ||
* | ||
* @deprecated Use `@fastify/websocket` instead. | ||
* | ||
* @category Server/fastify-websocket | ||
@@ -27,2 +29,4 @@ */ | ||
* | ||
* @deprecated Use `@fastify/websocket` instead. | ||
* | ||
* @category Server/fastify-websocket | ||
@@ -29,0 +33,0 @@ */ |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.makeHandler = void 0; | ||
const server_1 = require("../server"); | ||
const common_1 = require("../common"); | ||
const utils_1 = require("../utils"); | ||
const websocket_1 = require("./@fastify/websocket"); | ||
/** | ||
@@ -11,2 +9,4 @@ * Make a handler to use on a [fastify-websocket](https://github.com/fastify/fastify-websocket) route. | ||
* | ||
* @deprecated Use `@fastify/websocket` instead. | ||
* | ||
* @category Server/fastify-websocket | ||
@@ -23,106 +23,5 @@ */ | ||
keepAlive = 12000) { | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
const server = (0, server_1.makeServer)(options); | ||
// we dont have access to the fastify-websocket server instance yet, | ||
// register an error handler on first connection ONCE only | ||
let handlingServerEmittedErrors = false; | ||
return function handler(connection, request) { | ||
const { socket } = connection; | ||
// might be too late, but meh | ||
this.websocketServer.options.handleProtocols = server_1.handleProtocols; | ||
// handle server emitted errors only if not already handling | ||
if (!handlingServerEmittedErrors) { | ||
handlingServerEmittedErrors = true; | ||
this.websocketServer.once('error', (err) => { | ||
console.error('Internal error emitted on the WebSocket server. ' + | ||
'Please check your implementation.', err); | ||
// catch the first thrown error and re-throw it once all clients have been notified | ||
let firstErr = null; | ||
// report server errors by erroring out all clients with the same error | ||
for (const client of this.websocketServer.clients) { | ||
try { | ||
client.close(common_1.CloseCode.InternalServerError, isProd | ||
? 'Internal server error' | ||
: (0, utils_1.limitCloseReason)(err.message, 'Internal server error')); | ||
} | ||
catch (err) { | ||
firstErr = firstErr !== null && firstErr !== void 0 ? firstErr : err; | ||
} | ||
} | ||
if (firstErr) | ||
throw firstErr; | ||
}); | ||
} | ||
// used as listener on two streams, prevent superfluous calls on close | ||
let emittedErrorHandled = false; | ||
function handleEmittedError(err) { | ||
if (emittedErrorHandled) | ||
return; | ||
emittedErrorHandled = true; | ||
console.error('Internal error emitted on a WebSocket socket. ' + | ||
'Please check your implementation.', err); | ||
socket.close(common_1.CloseCode.InternalServerError, isProd | ||
? 'Internal server error' | ||
: (0, utils_1.limitCloseReason)(err.message, 'Internal server error')); | ||
} | ||
// fastify-websocket uses the WebSocket.createWebSocketStream, | ||
// therefore errors get emitted on both the connection and the socket | ||
connection.once('error', handleEmittedError); | ||
socket.once('error', handleEmittedError); | ||
// keep alive through ping-pong messages | ||
let pongWait = null; | ||
const pingInterval = keepAlive > 0 && isFinite(keepAlive) | ||
? setInterval(() => { | ||
// ping pong on open sockets only | ||
if (socket.readyState === socket.OPEN) { | ||
// terminate the connection after pong wait has passed because the client is idle | ||
pongWait = setTimeout(() => { | ||
socket.terminate(); | ||
}, keepAlive); | ||
// listen for client's pong and stop socket termination | ||
socket.once('pong', () => { | ||
if (pongWait) { | ||
clearTimeout(pongWait); | ||
pongWait = null; | ||
} | ||
}); | ||
socket.ping(); | ||
} | ||
}, keepAlive) | ||
: null; | ||
const closed = server.opened({ | ||
protocol: socket.protocol, | ||
send: (data) => new Promise((resolve, reject) => { | ||
if (socket.readyState !== socket.OPEN) | ||
return resolve(); | ||
socket.send(data, (err) => (err ? reject(err) : resolve())); | ||
}), | ||
close: (code, reason) => socket.close(code, reason), | ||
onMessage: (cb) => socket.on('message', async (event) => { | ||
try { | ||
await cb(String(event)); | ||
} | ||
catch (err) { | ||
console.error('Internal error occurred during message handling. ' + | ||
'Please check your implementation.', err); | ||
socket.close(common_1.CloseCode.InternalServerError, isProd | ||
? 'Internal server error' | ||
: (0, utils_1.limitCloseReason)(err.message, 'Internal server error')); | ||
} | ||
}), | ||
}, { connection, request }); | ||
socket.once('close', (code, reason) => { | ||
if (pongWait) | ||
clearTimeout(pongWait); | ||
if (pingInterval) | ||
clearInterval(pingInterval); | ||
if (!isProd && | ||
code === common_1.CloseCode.SubprotocolNotAcceptable && | ||
socket.protocol === common_1.DEPRECATED_GRAPHQL_WS_PROTOCOL) | ||
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` + | ||
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.'); | ||
closed(code, String(reason)); | ||
}); | ||
}; | ||
// new handler can be reused, the semantics stayed the same | ||
return (0, websocket_1.makeHandler)(options, keepAlive); | ||
} | ||
exports.makeHandler = makeHandler; |
{ | ||
"name": "graphql-ws", | ||
"version": "5.9.1", | ||
"version": "5.10.0", | ||
"description": "Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client", | ||
@@ -51,2 +51,7 @@ "keywords": [ | ||
}, | ||
"./lib/use/@fastify/websocket": { | ||
"require": "./lib/use/@fastify/websocket.js", | ||
"import": "./lib/use/@fastify/websocket.mjs", | ||
"types": "./lib/use/@fastify/websocket.d.ts" | ||
}, | ||
"./lib/use/fastify-websocket": { | ||
@@ -90,30 +95,31 @@ "require": "./lib/use/fastify-websocket.js", | ||
"devDependencies": { | ||
"@babel/core": "^7.18.6", | ||
"@babel/core": "^7.18.10", | ||
"@babel/plugin-proposal-class-properties": "^7.18.6", | ||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.18.6", | ||
"@babel/plugin-proposal-optional-chaining": "^7.18.6", | ||
"@babel/preset-env": "^7.18.6", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.18.9", | ||
"@babel/plugin-proposal-optional-chaining": "^7.18.9", | ||
"@babel/preset-env": "^7.18.10", | ||
"@babel/preset-typescript": "^7.18.6", | ||
"@rollup/plugin-typescript": "^8.3.3", | ||
"@fastify/websocket": "^7.0.0", | ||
"@rollup/plugin-typescript": "^8.3.4", | ||
"@semantic-release/changelog": "^6.0.1", | ||
"@semantic-release/git": "^10.0.1", | ||
"@types/jest": "^28.1.4", | ||
"@types/jest": "^28.1.6", | ||
"@types/ws": "^8.5.3", | ||
"@typescript-eslint/eslint-plugin": "^5.30.0", | ||
"@typescript-eslint/parser": "^5.30.0", | ||
"babel-jest": "^28.1.2", | ||
"eslint": "^8.18.0", | ||
"@typescript-eslint/eslint-plugin": "^5.33.0", | ||
"@typescript-eslint/parser": "^5.33.0", | ||
"babel-jest": "^28.1.3", | ||
"eslint": "^8.21.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"fastify": "^3.29.0", | ||
"fastify": "^4.4.0", | ||
"fastify-websocket": "4.2.2", | ||
"glob": "^8.0.3", | ||
"graphql": "^16.5.0", | ||
"jest": "^28.1.2", | ||
"jest-environment-jsdom": "^28.1.2", | ||
"jest-jasmine2": "^28.1.2", | ||
"jest": "^28.1.3", | ||
"jest-environment-jsdom": "^28.1.3", | ||
"jest-jasmine2": "^28.1.3", | ||
"prettier": "^2.7.1", | ||
"replacestream": "^4.0.3", | ||
"rollup": "^2.75.7", | ||
"rollup": "^2.77.2", | ||
"rollup-plugin-terser": "^7.0.2", | ||
@@ -123,8 +129,8 @@ "semantic-release": "^19.0.3", | ||
"tslib": "^2.4.0", | ||
"typedoc": "^0.23.2", | ||
"typedoc-plugin-markdown": "^3.13.2", | ||
"typedoc": "^0.23.10", | ||
"typedoc-plugin-markdown": "^3.13.4", | ||
"typescript": "^4.7.4", | ||
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.10.0", | ||
"ws": "^8.8.0", | ||
"ws7": "npm:ws@^7.5.8" | ||
"ws": "^8.8.1", | ||
"ws7": "npm:ws@^7.5.9" | ||
}, | ||
@@ -131,0 +137,0 @@ "resolutions": { |
258
README.md
@@ -102,8 +102,8 @@ <div align="center"> | ||
##### With [fastify-websocket](https://github.com/fastify/fastify-websocket) | ||
##### With [@fastify/websocket](https://github.com/fastify/fastify-websocket) | ||
```ts | ||
import Fastify from 'fastify'; // yarn add fastify | ||
import fastifyWebsocket from 'fastify-websocket'; // yarn add fastify-websocket | ||
import { makeHandler } from 'graphql-ws/lib/use/fastify-websocket'; | ||
import fastifyWebsocket from '@fastify/websocket'; // yarn add @fastify/websocket | ||
import { makeHandler } from 'graphql-ws/lib/use/@fastify/websocket'; | ||
import { schema } from './previous-step'; | ||
@@ -114,3 +114,5 @@ | ||
fastify.get('/graphql', { websocket: true }, makeHandler({ schema })); | ||
fastify.register(async (fastify) => { | ||
fastify.get('/graphql', { websocket: true }, makeHandler({ schema })); | ||
}); | ||
@@ -1173,2 +1175,74 @@ fastify.listen(4000, (err) => { | ||
<details id="yoga"> | ||
<summary><a href="#yoga">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server usage with <a href="https://www.graphql-yoga.com">GraphQL Yoga</a></summary> | ||
```typescript | ||
import { ExecutionArgs, execute, subscribe } from 'graphql'; | ||
import { createServer } from '@graphql-yoga/node'; | ||
import { WebSocketServer } from 'ws'; | ||
import { useServer } from 'graphql-ws/lib/use/ws'; | ||
import { schema } from './my-graphql-schema'; | ||
async function main() { | ||
const yogaApp = createServer({ | ||
schema, | ||
graphiql: { | ||
subscriptionsProtocol: 'WS', // use WebSockets instead of SSE | ||
}, | ||
}); | ||
const httpServer = await yogaApp.start(); | ||
const wsServer = new WebSocketServer({ | ||
server: httpServer, | ||
path: yogaApp.getAddressInfo().endpoint, | ||
}); | ||
// yoga's envelop may augment the `execute` and `subscribe` operations | ||
// so we need to make sure we always use the freshest instance | ||
type EnvelopedExecutionArgs = ExecutionArgs & { | ||
rootValue: { | ||
execute: typeof execute; | ||
subscribe: typeof subscribe; | ||
}; | ||
}; | ||
useServer( | ||
{ | ||
execute: (args) => | ||
(args as EnvelopedExecutionArgs).rootValue.execute(args), | ||
subscribe: (args) => | ||
(args as EnvelopedExecutionArgs).rootValue.subscribe(args), | ||
onSubscribe: async (ctx, msg) => { | ||
const { schema, execute, subscribe, contextFactory, parse, validate } = | ||
yogaApp.getEnveloped(ctx); | ||
const args: EnvelopedExecutionArgs = { | ||
schema, | ||
operationName: msg.payload.operationName, | ||
document: parse(msg.payload.query), | ||
variableValues: msg.payload.variables, | ||
contextValue: await contextFactory(), | ||
rootValue: { | ||
execute, | ||
subscribe, | ||
}, | ||
}; | ||
const errors = validate(args.schema, args.document); | ||
if (errors.length) return errors; | ||
return args; | ||
}, | ||
}, | ||
wsServer, | ||
); | ||
} | ||
main().catch((e) => { | ||
console.error(e); | ||
process.exit(1); | ||
}); | ||
``` | ||
</details> | ||
<details id="express"> | ||
@@ -1256,2 +1330,27 @@ <summary><a href="#express">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server usage with <a href="https://github.com/graphql/express-graphql">Express GraphQL</a></summary> | ||
<details id="deprecated-fastify-websocket"> | ||
<summary><a href="#deprecated-fastify-websocket">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server usage with <a href="https://www.npmjs.com/package/fastify-websocket">deprecated fastify-websocket</a></summary> | ||
```typescript | ||
import Fastify from 'fastify'; // yarn add fastify@^3 | ||
import fastifyWebsocket from 'fastify-websocket'; // yarn add fastify-websocket@4.2.2 | ||
import { makeHandler } from 'graphql-ws/lib/use/fastify-websocket'; | ||
import { schema } from './previous-step'; | ||
const fastify = Fastify(); | ||
fastify.register(fastifyWebsocket); | ||
fastify.get('/graphql', { websocket: true }, makeHandler({ schema })); | ||
fastify.listen(4000, (err) => { | ||
if (err) { | ||
fastify.log.error(err); | ||
return process.exit(1); | ||
} | ||
console.log('Listening to port 4000'); | ||
}); | ||
``` | ||
</details> | ||
<details id="ws-backwards-compat"> | ||
@@ -1794,2 +1893,153 @@ <summary><a href="#ws-backwards-compat">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server usage with <a href="https://github.com/apollographql/subscriptions-transport-ws">subscriptions-transport-ws</a> backwards compatibility</summary> | ||
<details id="subscribe-ack"> | ||
<summary><a href="#subscribe-ack">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server and client usage with subscription acknowledgment</summary> | ||
```ts | ||
// 🛸 server | ||
import { WebSocketServer } from 'ws'; | ||
// import ws from 'ws'; yarn add ws@7 | ||
// const WebSocketServer = ws.Server; | ||
import { MessageType, stringifyMessage } from 'graphql-ws'; | ||
import { useServer } from 'graphql-ws/lib/use/ws'; | ||
import { schema } from './my-graphql-schema'; | ||
const wsServer = new WebSocketServer({ | ||
port: 4000, | ||
path: '/graphql', | ||
}); | ||
useServer<undefined, { ackWaiters: Record<string, () => void> }>( | ||
{ | ||
schema, | ||
onConnect: (ctx) => { | ||
// listeners waiting for operation acknowledgment. if subscription, this means the graphql.subscribe was successful | ||
// intentionally in context extra to avoid memory leaks when clients disconnect | ||
ctx.extra.ackWaiters = {}; | ||
}, | ||
onSubscribe: (ctx, msg) => { | ||
const ackId = msg.payload.extensions?.ackId; | ||
if (typeof ackId === 'string') { | ||
// if acknowledgment ID is present, create an acknowledger that will be executed when operation succeeds | ||
ctx.extra.ackWaiters![msg.id] = () => { | ||
ctx.extra.socket.send( | ||
stringifyMessage({ | ||
type: MessageType.Ping, | ||
payload: { | ||
ackId, | ||
}, | ||
}), | ||
); | ||
}; | ||
} | ||
}, | ||
onOperation: (ctx, msg) => { | ||
// acknowledge operation success and remove waiter | ||
ctx.extra.ackWaiters![msg.id]?.(); | ||
delete ctx.extra.ackWaiters![msg.id]; | ||
}, | ||
}, | ||
wsServer, | ||
); | ||
console.log('Listening to port 4000'); | ||
``` | ||
```ts | ||
// 📺 client | ||
import { | ||
Client, | ||
ClientOptions, | ||
createClient, | ||
ExecutionResult, | ||
Sink, | ||
SubscribePayload, | ||
} from 'graphql-ws'; | ||
// client with augmented subscribe method accepting the `onAck` callback for operation acknowledgement | ||
type ClientWithSubscribeAck = Omit<Client, 'subscribe'> & { | ||
subscribe<Data = Record<string, unknown>, Extensions = unknown>( | ||
payload: SubscribePayload, | ||
sink: Sink<ExecutionResult<Data, Extensions>>, | ||
onAck: () => void, | ||
): () => void; | ||
}; | ||
function createClientWithSubscribeAck( | ||
options: ClientOptions, | ||
): ClientWithSubscribeAck { | ||
const client = createClient(options); | ||
const ackListeners: Record<string, () => void> = {}; | ||
client.on('ping', (_received, payload) => { | ||
const ackId = payload?.ackId; | ||
if (typeof ackId === 'string') { | ||
ackListeners[ackId]?.(); | ||
delete ackListeners[ackId]; | ||
} | ||
}); | ||
return { | ||
...client, | ||
subscribe: (payload, sink, onAck) => { | ||
const ackId = Math.random().toString(); // be wary of uniqueness | ||
ackListeners[ackId] = onAck; | ||
return client.subscribe( | ||
{ | ||
...payload, | ||
extensions: { | ||
...payload.extensions, | ||
ackId, | ||
}, | ||
}, | ||
sink, | ||
); | ||
}, | ||
}; | ||
} | ||
``` | ||
Using the augmented client would be as simple as: | ||
```ts | ||
const client = createClientWithSubscribeAck({ | ||
url: 'ws://i.want.ack:4000/graphql', | ||
}); | ||
(async () => { | ||
const onNext = () => { | ||
/* handle incoming values */ | ||
}; | ||
let unsubscribe = () => { | ||
/* complete the subscription */ | ||
}; | ||
let subscriptionAcknowledged = () => { | ||
/* server successfully subscribed */ | ||
}; | ||
await new Promise((resolve, reject) => { | ||
unsubscribe = client.subscribe( | ||
{ | ||
query: 'subscription { greetings }', | ||
}, | ||
{ | ||
next: onNext, | ||
error: reject, | ||
complete: resolve, | ||
}, | ||
subscriptionAcknowledged, | ||
); | ||
}); | ||
expect(subscriptionAcknowledged).toBeCalledFirst(); | ||
expect(onNext).then.toBeCalledTimes(5); // we say "Hi" in 5 languages | ||
})(); | ||
``` | ||
</details> | ||
## [Documentation](docs/) | ||
@@ -1796,0 +2046,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
306225
34
4782
2057
39