Skip to content

Commit 68721ae

Browse files
committed
no full messages in hooks
1 parent f2d7b7a commit 68721ae

File tree

3 files changed

+153
-59
lines changed

3 files changed

+153
-59
lines changed

.changeset/strange-ties-mix.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
'graphql-ws': major
3+
---
4+
5+
`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
6+
7+
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).
8+
9+
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**.
10+
11+
### Migrating from v5 to v6
12+
13+
#### `onSubscribe`
14+
15+
```diff
16+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
17+
18+
const opts: ServerOptions = {
19+
- onSubscribe(ctx, message) {
20+
- const messageId = message.id;
21+
- const messagePayload: SubscribePayload = message.payload;
22+
- },
23+
+ onSubscribe(ctx, id, payload) {
24+
+ const messageId = id;
25+
+ const messagePayload: SubscribePayload = payload;
26+
+ },
27+
};
28+
```
29+
30+
#### `onOperation`
31+
32+
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.
33+
34+
```diff
35+
import { ExecutionArgs } from 'graphql';
36+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
37+
38+
const opts: ServerOptions = {
39+
- onOperation(ctx, message) {
40+
- const messageId = message.id;
41+
- const messagePayload: SubscribePayload = message.payload;
42+
- },
43+
+ onOperation(ctx, id, args) {
44+
+ const messageId = id;
45+
+ const executionArgs: ExecutionArgs = args;
46+
+ },
47+
};
48+
```
49+
50+
#### `onError`
51+
52+
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.
53+
54+
```diff
55+
import { GraphQLError, GraphQLFormattedError } from 'graphql';
56+
import { ServerOptions } from 'graphql-ws';
57+
58+
const opts: ServerOptions = {
59+
- onError(ctx, message, errors) {
60+
- const messageId = message.id;
61+
- const graphqlErrors: readonly GraphQLError[] = errors;
62+
- const messagePayload: readonly GraphQLFormattedError[] = message.payload;
63+
- },
64+
+ onError(ctx, id, errors) {
65+
+ const messageId = id;
66+
+ const graphqlErrors: readonly GraphQLError[] = errors;
67+
+ const messagePayload: readonly GraphQLFormattedError[] = errors.map((e) => e.toJSON());
68+
+ },
69+
};
70+
```
71+
72+
#### `onNext`
73+
74+
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.
75+
76+
```diff
77+
import { ExecutionResult, FormattedExecutionResult } from 'graphql';
78+
import { ServerOptions } from 'graphql-ws';
79+
80+
const opts: ServerOptions = {
81+
- onNext(ctx, message, result) {
82+
- const messageId = message.id;
83+
- const graphqlResult: ExecutionResult = result;
84+
- const messagePayload: FormattedExecutionResult = message.payload;
85+
- },
86+
+ onNext(ctx, id, result) {
87+
+ const messageId = id;
88+
+ const graphqlResult: ExecutionResult = result;
89+
+ const messagePayload: FormattedExecutionResult = { ...result, errors: result.errors?.map((e) => e.toJSON()) };
90+
+ },
91+
};
92+
```
93+
94+
#### `onComplete`
95+
96+
```diff
97+
import { ServerOptions } from 'graphql-ws';
98+
99+
const opts: ServerOptions = {
100+
- onComplete(ctx, message) {
101+
- const messageId = message.id;
102+
- },
103+
+ onComplete(ctx, id) {
104+
+ const messageId = id;
105+
+ },
106+
};
107+
```

src/server.ts

Lines changed: 43 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ import {
2020
} from 'graphql';
2121
import {
2222
CloseCode,
23-
CompleteMessage,
2423
ConnectionInitMessage,
25-
ErrorMessage,
2624
ExecutionPatchResult,
2725
ExecutionResult,
2826
FormattedExecutionPatchResult,
@@ -32,19 +30,14 @@ import {
3230
JSONMessageReviver,
3331
Message,
3432
MessageType,
35-
NextMessage,
3633
parseMessage,
3734
PingMessage,
3835
PongMessage,
3936
stringifyMessage,
4037
SubscribeMessage,
38+
SubscribePayload,
4139
} from './common';
42-
import {
43-
areGraphQLErrors,
44-
isAsyncGenerator,
45-
isAsyncIterable,
46-
isObject,
47-
} from './utils';
40+
import { isAsyncGenerator, isAsyncIterable, isObject } from './utils';
4841

4942
/** @category Server */
5043
export type OperationResult =
@@ -308,7 +301,8 @@ export interface ServerOptions<
308301
| undefined
309302
| ((
310303
ctx: Context<P, E>,
311-
message: SubscribeMessage,
304+
id: string,
305+
payload: SubscribePayload,
312306
) =>
313307
| Promise<ExecutionArgs | readonly GraphQLError[] | void>
314308
| ExecutionArgs
@@ -338,7 +332,7 @@ export interface ServerOptions<
338332
| undefined
339333
| ((
340334
ctx: Context<P, E>,
341-
message: SubscribeMessage,
335+
id: string,
342336
args: ExecutionArgs,
343337
result: OperationResult,
344338
) => Promise<OperationResult | void> | OperationResult | void);
@@ -359,7 +353,7 @@ export interface ServerOptions<
359353
| undefined
360354
| ((
361355
ctx: Context<P, E>,
362-
message: ErrorMessage,
356+
id: string,
363357
errors: readonly GraphQLError[],
364358
) =>
365359
| Promise<readonly GraphQLFormattedError[] | void>
@@ -383,7 +377,7 @@ export interface ServerOptions<
383377
| undefined
384378
| ((
385379
ctx: Context<P, E>,
386-
message: NextMessage,
380+
id: string,
387381
args: ExecutionArgs,
388382
result: ExecutionResult | ExecutionPatchResult,
389383
) =>
@@ -408,7 +402,7 @@ export interface ServerOptions<
408402
*/
409403
onComplete?:
410404
| undefined
411-
| ((ctx: Context<P, E>, message: CompleteMessage) => Promise<void> | void);
405+
| ((ctx: Context<P, E>, id: string) => Promise<void> | void);
412406
/**
413407
* An optional override for the JSON.parse function used to hydrate
414408
* incoming messages to this server. Useful for parsing custom datatypes
@@ -699,57 +693,46 @@ export function makeServer<
699693
args: ExecutionArgs,
700694
) => {
701695
const { errors, ...resultWithoutErrors } = result;
702-
let nextMessage: NextMessage = {
703-
id,
704-
type: MessageType.Next,
705-
payload: {
706-
...resultWithoutErrors,
707-
...(errors
708-
? { errors: errors.map((e) => e.toJSON()) }
709-
: {}),
710-
},
711-
};
712-
const maybeResult = await onNext?.(
713-
ctx,
714-
nextMessage,
715-
args,
716-
result,
717-
);
718-
if (maybeResult)
719-
nextMessage = {
720-
...nextMessage,
721-
payload: maybeResult,
722-
};
696+
const maybeResult = await onNext?.(ctx, id, args, result);
723697
await socket.send(
724-
stringifyMessage<MessageType.Next>(nextMessage, replacer),
698+
stringifyMessage<MessageType.Next>(
699+
{
700+
id,
701+
type: MessageType.Next,
702+
payload: maybeResult || {
703+
...resultWithoutErrors,
704+
// omit errors completely if not defined
705+
...(errors
706+
? { errors: errors.map((e) => e.toJSON()) }
707+
: {}),
708+
},
709+
},
710+
replacer,
711+
),
725712
);
726713
},
727714
error: async (errors: readonly GraphQLError[]) => {
728-
let errorMessage: ErrorMessage = {
729-
id,
730-
type: MessageType.Error,
731-
payload: errors.map((e) => e.toJSON()),
732-
};
733-
const maybeErrors = await onError?.(ctx, errorMessage, errors);
734-
if (maybeErrors)
735-
errorMessage = {
736-
...errorMessage,
737-
payload: maybeErrors,
738-
};
715+
const maybeErrors = await onError?.(ctx, id, errors);
739716
await socket.send(
740-
stringifyMessage<MessageType.Error>(errorMessage, replacer),
717+
stringifyMessage<MessageType.Error>(
718+
{
719+
id,
720+
type: MessageType.Error,
721+
payload: maybeErrors || errors.map((e) => e.toJSON()),
722+
},
723+
replacer,
724+
),
741725
);
742726
},
743727
complete: async (notifyClient: boolean) => {
744-
const completeMessage: CompleteMessage = {
745-
id,
746-
type: MessageType.Complete,
747-
};
748-
await onComplete?.(ctx, completeMessage);
728+
await onComplete?.(ctx, id);
749729
if (notifyClient)
750730
await socket.send(
751731
stringifyMessage<MessageType.Complete>(
752-
completeMessage,
732+
{
733+
id,
734+
type: MessageType.Complete,
735+
},
753736
replacer,
754737
),
755738
);
@@ -758,7 +741,11 @@ export function makeServer<
758741

759742
try {
760743
let execArgs: ExecutionArgs;
761-
const maybeExecArgsOrErrors = await onSubscribe?.(ctx, message);
744+
const maybeExecArgsOrErrors = await onSubscribe?.(
745+
ctx,
746+
message.id,
747+
message.payload,
748+
);
762749
if (maybeExecArgsOrErrors) {
763750
if (areGraphQLErrors(maybeExecArgsOrErrors))
764751
return id in ctx.subscriptions
@@ -833,7 +820,7 @@ export function makeServer<
833820

834821
const maybeResult = await onOperation?.(
835822
ctx,
836-
message,
823+
message.id,
837824
execArgs,
838825
operationResult,
839826
);

tests/server.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,13 +1629,13 @@ describe.concurrent('Subscribe', () => {
16291629
};
16301630

16311631
const { url } = await startTServer({
1632-
onSubscribe: (_ctx, msg) => {
1632+
onSubscribe: (_ctx, _id, payload) => {
16331633
// search using `SubscriptionPayload.query` as QueryID
16341634
// check the client example below for better understanding
1635-
const hit = queriesStore[msg.payload.query as string]!;
1635+
const hit = queriesStore[payload.query as string]!;
16361636
return {
16371637
...hit,
1638-
variableValues: msg.payload.variables, // use the variables from the client
1638+
variableValues: payload.variables, // use the variables from the client
16391639
};
16401640
},
16411641
});

0 commit comments

Comments
 (0)