graphql-ws
Advanced tools
Changelog
6.0.0
b668b30
Thanks @enisdenjo! - @fastify/websocket WebSocket in the context extra has been renamed from connection
to socket
import { makeHandler } from 'graphql-ws/use/@fastify/websocket';
makeHandler({
schema(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
context(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onConnect(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onDisconnect(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onClose(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onSubscribe(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onOperation(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onError(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onNext(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
onComplete(ctx) {
- const websocket = ctx.connection;
+ const websocket = ctx.socket;
},
});
#613 3f11aba
Thanks @enisdenjo! - Drop support for ws
v7
ws
v7 has been deprecated. Please upgrade and use v8.
#613 3f11aba
Thanks @enisdenjo! - Drop support for deprecated fastify-websocket
fastify-websocket
has been deprecated since v4.3.0.. Please upgrade and use @fastify/websocket
.
#613 3f11aba
Thanks @enisdenjo! - The /lib/
part from imports has been removed, for example graphql-ws/lib/use/ws
becomes graphql-ws/use/ws
Simply remove the /lib/
part from your graphql-ws imports that use a handler.
- import { useServer } from 'graphql-ws/lib/use/ws';
+ import { useServer } from 'graphql-ws/use/ws';
- import { makeBehavior } from 'graphql-ws/lib/use/uWebSockets';
+ import { makeBehavior } from 'graphql-ws/use/uWebSockets';
- import { makeHandler } from 'graphql-ws/lib/use/@fastify/websocket';
+ import { makeHandler } from 'graphql-ws/use/@fastify/websocket';
- import { handleProtocols, makeHandler } from 'graphql-ws/lib/use/bun';
+ import { handleProtocols, makeHandler } from 'graphql-ws/use/bun';
- import { makeHandler } from 'https://esm.sh/graphql-ws/lib/use/deno';
+ import { makeHandler } from 'https://esm.sh/graphql-ws/use/deno';
#613 3f11aba
Thanks @enisdenjo! - ErrorMessage
uses and onError
returns GraphQLFormattedError
(instead of GraphQLError
)
Thanks @benjie for working on this in #599
#613 3f11aba
Thanks @enisdenjo! - Least supported Node version is v20
Node v10 has been deprecated for years now. There is no reason to support it. Bumping the engine to the current LTS (v20) also allows the code to be leaner and use less polyfills.
#613 3f11aba
Thanks @enisdenjo! - Least supported graphql
peer dependency is ^15.10.1 and ^16
Users are advised to use the latest of graphql
because of various improvements in performance and security.
#613 3f11aba
Thanks @enisdenjo! - NextMessage
uses and onNext
returns FormattedExecutionResult
(instead of ExecutionResult
)
#613 3f11aba
Thanks @enisdenjo! - schema
, context
, onSubscribe
, onOperation
, onError
, onNext
and onComplete
hooks don't have the full accompanying message anymore, only the ID and the relevant part from the message
There is really no need to pass the full SubscribeMessage
to the onSubscribe
hook. The only relevant parts from the message are the id
and the payload
, the type
is useless since the hook inherently has it (onNext
is next
type, onError
is error
type, etc).
The actual techincal reason for not having the full message is to avoid serialising results and errors twice. Both onNext
and onError
allow the user to augment the result and return it to be used instead. onNext
originally had the NextMessage
argument which already has the FormattedExecutionResult
, and onError
originally had the ErrorMessage
argument which already has the GraphQLFormattedError
, and they both also returned FormattedExecutionResult
and GraphQLFormattedError
respectivelly - meaning, if the user serialised the results - the serialisation would happen twice.
Additionally, the onOperation
, onError
, onNext
and onComplete
now have the payload
which is the SubscribeMessage.payload
(SubscribePayload
) for easier access to the original query as well as execution params extensions.
schema
import { ExecutionArgs } from 'graphql';
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- schema(ctx, message, argsWithoutSchema: Omit<ExecutionArgs, 'schema'>) {
- const messageId = message.id;
- const messagePayload: SubscribePayload = message.payload;
- },
+ schema(ctx, id, payload) {
+ const messageId = id;
+ const messagePayload: SubscribePayload = payload;
+ },
};
context
import { ExecutionArgs } from 'graphql';
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- context(ctx, message, args: ExecutionArgs) {
- const messageId = message.id;
- const messagePayload: SubscribePayload = message.payload;
- },
+ context(ctx, id, payload, args: ExecutionArgs) {
+ const messageId = id;
+ const messagePayload: SubscribePayload = payload;
+ },
};
onSubscribe
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- onSubscribe(ctx, message) {
- const messageId = message.id;
- const messagePayload: SubscribePayload = message.payload;
- },
+ onSubscribe(ctx, id, payload) {
+ const messageId = id;
+ const messagePayload: SubscribePayload = payload;
+ },
};
onOperation
The SubscribeMessage.payload
is not useful here at all, the payload
has been parsed to ready-to-use graphql execution args and should be used instead.
import { ExecutionArgs } from 'graphql';
import { ServerOptions, SubscribePayload, OperationResult } from 'graphql-ws';
const opts: ServerOptions = {
- onOperation(ctx, message, args: ExecutionArgs, result: OperationResult) {
- const messageId = message.id;
- const messagePayload: SubscribePayload = message.payload;
- },
+ onOperation(ctx, id, payload, args: ExecutionArgs, result: OperationResult) {
+ const messageId = id;
+ const messagePayload: SubscribePayload = payload;
+ },
};
onError
The ErrorMessage.payload
(GraphQLFormattedError[]
) is not useful here at all, the user has access to GraphQLError[]
that are true instances of the error containing object references to originalError
s and other properties. The user can always convert and return GraphQLFormattedError[]
by using the .toJSON()
method.
import { GraphQLError, GraphQLFormattedError } from 'graphql';
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- onError(ctx, message, errors) {
- const messageId = message.id;
- const graphqlErrors: readonly GraphQLError[] = errors;
- const errorMessagePayload: readonly GraphQLFormattedError[] = message.payload;
- },
+ onError(ctx, id, payload, errors) {
+ const messageId = id;
+ const graphqlErrors: readonly GraphQLError[] = errors;
+ const subscribeMessagePayload: SubscribePayload = payload;
+ const errorMessagePayload: readonly GraphQLFormattedError[] = errors.map((e) => e.toJSON());
+ },
};
onNext
The NextMessage.payload
(FormattedExecutionResult
) is not useful here at all, the user has access to ExecutionResult
that contains actual object references to error instances. The user can always convert and return FormattedExecutionResult
by serialising the errors with GraphQLError.toJSON()
method.
import { ExecutionArgs, ExecutionResult, FormattedExecutionResult } from 'graphql';
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- onNext(ctx, message, args: ExecutionArgs, result: ExecutionResult) {
- const messageId = message.id;
- const nextMessagePayload: FormattedExecutionResult = message.payload;
- },
+ onNext(ctx, id, payload, args: ExecutionArgs, result: ExecutionResult) {
+ const messageId = id;
+ const subscribeMessagePayload: SubscribePayload = payload;
+ const nextMessagePayload: FormattedExecutionResult = { ...result, errors: result.errors?.map((e) => e.toJSON()) };
+ },
};
onComplete
import { ServerOptions, SubscribePayload } from 'graphql-ws';
const opts: ServerOptions = {
- onComplete(ctx, message) {
- const messageId = message.id;
- },
+ onComplete(ctx, id, payload) {
+ const messageId = id;
+ const subscribeMessagePayload: SubscribePayload = payload;
+ },
};
#613 3f11aba
Thanks @enisdenjo! - Errors thrown from subscription iterables will be caught and reported through the ErrorMessage
Compared to the behaviour before, which terminated the whole WebSocket connection - those errors are now gracefully reported and terminate only the specific subscription that threw the error.
There's been an editorial change in the GraphQL Spec suggesting this being the correct approach.
Also, if you'd like to get involved and ideally drop your opinion about whether iterable errors should be reported as errors or ExecutionResult
s with errors
field set, please read more here.
If you had used the suggested "ws server usage with custom subscribe method that gracefully handles thrown errors" recipe, you can simply remove it since this behaviour is now baked in.
import { subscribe } from 'graphql';
import { useServer } from 'graphql-ws/use/ws';
import { WebSocketServer } from 'ws'; // yarn add ws
const wsServer = new WebSocketServer({
port: 4000,
path: '/graphql',
});
useServer(
{
schema,
- async subscribe(...args) {
- const result = await subscribe(...args);
- if ('next' in result) {
- // is an async iterable, augment the next method to handle thrown errors
- const originalNext = result.next;
- result.next = async () => {
- try {
- return await originalNext();
- } catch (err) {
- // gracefully handle the error thrown from the next method
- return { value: { errors: [err] } };
- }
- };
- }
- return result;
- },
},
wsServer,
);
#613 3f11aba
Thanks @enisdenjo! - Remove deprecated isMessage
, use validateMessage
instead
Replace all ocurrances of isMessage
with validateMessage
. Note that validateMessage
throws if the message is not valid, compared with isMessage
that simply returned true/false.
- import { isMessage } from 'graphql-ws';
+ import { validateMessage } from 'graphql-ws';
function isGraphQLWSMessage(val) {
- return isMessage(val);
+ try {
+ validateMessage(val);
+ return true;
+ } catch {
+ return false;
+ }
}
#613 3f11aba
Thanks @enisdenjo! - Removed deprecated isFatalConnectionProblem
, use shouldRetry
instead
Replace all ocurrances of isFatalConnectionProblem
with shouldRetry
. Note that the result is inverted, where you returned false
in isFatalConnectionProblem
you should return true
in shouldRetry
.
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://localhost:4000/graphql',
- isFatalConnectionProblem: () => false,
+ shouldRetry: () => true,
});
#613 3f11aba
Thanks @enisdenjo! - Client is truly zero-dependency, not even a peer dependency on graphql
In non-browser environments, you can use only the client and not even depend on graphql
by importing from graphql-ws/client
.
import { createClient } from 'graphql-ws/client';
const client = createClient({
url: 'ws://localhost:4000/graphql',
});
Note that, in browser envirments (and of course having your bundler use the browser
package.json field), you don't have to import from graphql-ws/client
- simply importing from graphql-ws
will only have the createClient
available.
#615 29dd26a
Thanks @enisdenjo! - Define optional peer dependencies and least supported versions
Using the peerDependencies
in combination with peerDependenciesMeta
configuration in package.json
.