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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
306225
34
4782
2057
39