From d052336c1987996f8ce42be4dfb4372d3a792538 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 11 Sep 2020 12:28:13 +0200 Subject: [PATCH 01/18] Add GraphQL over WebSocket RFC --- rfcs/GraphQLOverWebSocket.md | 215 +++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 rfcs/GraphQLOverWebSocket.md diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md new file mode 100644 index 00000000..1766b12a --- /dev/null +++ b/rfcs/GraphQLOverWebSocket.md @@ -0,0 +1,215 @@ +# GraphQL over WebSocket Protocol + +## Nomenclature + +- **Socket** is the main WebSocket communication channel between the _server_ and the _client_ +- **Connection** is a connection **within the established socket** describing a "connection" through which the operation requests will be communicated + +## Communication + +The WebSocket sub-protocol for this specification is: `graphql-transport-ws` + +Messages are represented through the JSON structure and are stringified before being sent over the network. They are bidirectional, meaning both the server and the client conform to the specified message structure. + +**All** messages contain the `type` field outlining the type or action this message describes. Depending on the type, the message can contain two more _optional_ fields: + +- `id` used for uniquely identifying server responses and connecting them with the client requests +- `payload` holding the extra "payload" information to go with the specific message type + +The server can terminate the socket (kick the client off) at any time. The close event dispatched by the server is used to describe the fatal error to the client. + +The client terminates the socket and closes the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. + +## Keep-Alive + +The server will occasionally check if the client is still "alive", available and listening. In order to perform this check, implementation leverages the standardized [Pings and Pongs: The Heartbeat of WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets). + +Keep-Alive interval and the "pong wait" timeout can be tuned by using the accompanying configuration parameter on the server. + +Ping and Pong feature is a mandatory requirement by [The WebSocket Protocol](https://tools.ietf.org/html/rfc6455#section-5.5.2). All clients that don't support it are **not** RFC6455 compliant and will simply have their socket terminated after the pong wait has passed. + +## Message types + +### `ConnectionInit` + +Direction: **Client -> Server** + +Indicates that the client wants to establish a connection within the existing socket. This connection is **not** the actual WebSocket communication channel, but is rather a frame within it asking the server to allow future subscription operation requests. + +The client can specify additional `connectionParams` which are sent through the `payload` field in the outgoing message. + +The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will terminate the socket with the close event: `4408: Connection initialisation timeout`. + +```typescript +interface ConnectionInitMessage { + type: "connection_init"; + payload?: Record; // connectionParams +} +``` + +The server will respond by either: + +- Dispatching a `ConnectionAck` message acknowledging that the connection has been successfully established. The server does not implement the `onConnect` callback or the implemented callback has returned `true`. +- Closing the socket with a close event `4403: Forbidden` indicating that the connection request has been denied because of access control. The server has returned `false` in the `onConnect` callback. +- Closing the socket with a close event `4400: ` indicating that the connection request has been denied because of an implementation specific error. The server has thrown an error in the `onConnect` callback, the thrown error's message is the `` in the close event. + +### `ConnectionAck` + +Direction: **Server -> Client** + +Potential response to the `ConnectionInit` message from the client acknowledging a successful connection with the server. + +```typescript +interface ConnectionAckMessage { + type: "connection_ack"; +} +``` + +The client is now **ready** to request subscription operations. + +### `Subscribe` + +Direction: **Client -> Server** + +Requests a operation specified in the message `payload`. This message leverages the unique ID field to connect future server messages to the operation started by this message. + +```typescript +import { DocumentNode } from "graphql"; + +interface SubscribeMessage { + id: ""; + type: "subscribe"; + payload: { + operationName?: string | null; + query: string | DocumentNode; + variables?: Record | null; + }; +} +``` + +Executing operations is allowed **only** after the server has acknowledged the connection through the `ConnectionAck` message, if the connection is not acknowledged, the socket will be terminated immediately with a close event `4401: Unauthorized`. + +### `Next` + +Direction: **Server -> Client** + +Operation execution result message. + +- If the operation is a `query` or `mutation`, the message can be seen as the final execution result. This message is followed by the `Complete` message indicating the completion of the operation. +- If the operation is a `subscription`, the message can be seen as an event in the source stream requested by the `Subscribe` message. + +```typescript +import { ExecutionResult } from "graphql"; + +interface NextMessage { + id: ""; + type: "next"; + payload: ExecutionResult; +} +``` + +### `Error` + +Direction: **Server -> Client** + +Operation execution error(s) triggered by the `Next` message happening before the actual execution, usually due to validation errors. + +```typescript +import { GraphQLError } from "graphql"; + +interface ErrorMessage { + id: ""; + type: "error"; + payload: GraphQLError[]; +} +``` + +### `Complete` + +Direction: **bidirectional** + +- **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, **no `Complete` message will be emitted**. + +- **Client -> Server** (for `subscription` operations only) indicating that the client has stopped listening to the events and wants to complete the source stream. No further data events, relevant to the original subscription, should be sent through. + +```typescript +interface CompleteMessage { + id: ""; + type: "complete"; +} +``` + +### Invalid message + +Direction: **bidirectional** + +Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket termination with a close event `4400: `. The `` can be vagouly descriptive on why the received message is invalid. + +## Examples + +For the sake of clarity, the following examples demonstrate the communication protocol. + +

Successful connection initialisation

+ +1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` +1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") +1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation +1. _Server_ validates the connection initialisation request and dispatches a `ConnectionAck` message to the client on successful connection +1. _Client_ has received the acknowledgement message and is now ready to request operation executions + +### Forbidden connection initialisation + +1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` +1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") +1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation +1. _Server_ validates the connection initialisation request and decides that the client is not allowed to establish a connection +1. _Server_ terminates the socket by dispatching the close event `4403: Forbidden` +1. _Client_ reports an error using the close event reason (which is `Forbidden`) + +### Erroneous connection initialisation + +1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` +1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") +1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation +1. _Server_ tries validating the connection initialisation request but an error `I'm a teapot` is thrown +1. _Server_ terminates the socket by dispatching the close event `4400: I'm a teapot` +1. _Client_ reports an error using the close event reason (which is `I'm a teapot`) + +### Connection initialisation timeout + +1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` +1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") +1. _Client_ does not dispatch a `ConnectionInit` message +1. _Server_ waits for the `ConnectionInit` message for the duration specified in the `connectionInitWaitTimeout` parameter +1. _Server_ waiting time has passed +1. _Server_ terminates the socket by dispatching the close event `4408: Connection initialisation timeout` +1. _Client_ reports an error using the close event reason (which is `Connection initialisation timeout`) + +### Query/Mutation operation + +_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ + +1. _Client_ generates a unique ID for the following operation +1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested `query`/`mutation` operation passed through the `payload` field +1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation +1. _Server_ validates the request and executes the GraphQL operation +1. _Server_ dispatches a `Next` message with the execution result matching the client's unique ID +1. _Server_ dispatches the `Complete` message with the matching unique ID indicating that the execution has completed +1. _Server_ triggers the `onComplete` callback, if specified + +### Subscribe operation + +_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ + +1. _Client_ generates a unique ID for the following operation +1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested subscription operation passed through the `payload` field +1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation +1. _Server_ validates the request, establishes a GraphQL subscription and listens for events in the source stream +1. _Server_ dispatches `Next` messages for every event in the underlying subscription source stream matching the client's unique ID +1. _Client_ stops the subscription by dispatching a `Complete` message with the matching unique ID +1. _Server_ effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources +1. _Server_ triggers the `onComplete` callback, if specified + +## Implementations + +- [graphql-transport-ws](https://github.com/enisdenjo/graphql-transport-ws) From 3f75a34c128fae53490ed4ad253788724902a387 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 16 Sep 2020 21:37:39 +0200 Subject: [PATCH 02/18] Typo --- rfcs/GraphQLOverWebSocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 1766b12a..ba76d77d 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -143,7 +143,7 @@ interface CompleteMessage { Direction: **bidirectional** -Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket termination with a close event `4400: `. The `` can be vagouly descriptive on why the received message is invalid. +Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket termination with a close event `4400: `. The `` can be vaguely descriptive on why the received message is invalid. ## Examples From 423f81f006536355a7d4ce948531e90f0cf50345 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 16 Sep 2020 21:37:59 +0200 Subject: [PATCH 03/18] Multiple `ConnectionInit` messages result in closure --- rfcs/GraphQLOverWebSocket.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index ba76d77d..53c1c3f0 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -40,6 +40,8 @@ The client can specify additional `connectionParams` which are sent through the The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will terminate the socket with the close event: `4408: Connection initialisation timeout`. +If the server receives more than one `ConnectionInit` message at any given time, the server will terminate the socket with the close event `4429: Too many initialisation requests`. + ```typescript interface ConnectionInitMessage { type: "connection_init"; From dab5819195d9c4fcdcc17d7ce9a375d9f5c73158 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 2 Oct 2020 00:08:05 +0200 Subject: [PATCH 04/18] `subscription` operations are distinct on the message ID --- rfcs/GraphQLOverWebSocket.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 53c1c3f0..61eca8be 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -73,7 +73,9 @@ The client is now **ready** to request subscription operations. Direction: **Client -> Server** -Requests a operation specified in the message `payload`. This message leverages the unique ID field to connect future server messages to the operation started by this message. +Requests a operation specified in the message `payload`. This message provides a unique ID field to connect future server messages to the operation started by this message. + +If there is already an active subscriber for a `subscription` operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. Since `query` and `mutation` resolve to a single emitted value, their subscription does not require reservations for additional future events. Having this in mind, the server may not assert this rule for these operations. ```typescript import { DocumentNode } from "graphql"; From 7302a0da26ca2241054602cd3611aacee92179ce Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 2 Oct 2020 00:09:13 +0200 Subject: [PATCH 05/18] Close is not the same as terminate and small refinements --- rfcs/GraphQLOverWebSocket.md | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 61eca8be..2c4c55e8 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -7,18 +7,18 @@ ## Communication -The WebSocket sub-protocol for this specification is: `graphql-transport-ws` +The WebSocket sub-protocol for this specification is: `graphql-transport-ws`. -Messages are represented through the JSON structure and are stringified before being sent over the network. They are bidirectional, meaning both the server and the client conform to the specified message structure. +Messages are represented through the JSON structure and are stringified before being sent over the network. They are bidirectional, meaning both the server and the client must conform to the specified message structure. -**All** messages contain the `type` field outlining the type or action this message describes. Depending on the type, the message can contain two more _optional_ fields: +**All** messages contain the `type` field outlining the action this message describes. Depending on the type, the message can contain two more _optional_ fields: -- `id` used for uniquely identifying server responses and connecting them with the client requests +- `id` used for uniquely identifying server responses and connecting them with the client's requests - `payload` holding the extra "payload" information to go with the specific message type -The server can terminate the socket (kick the client off) at any time. The close event dispatched by the server is used to describe the fatal error to the client. +The server can close the socket (kick the client off) at any time. The close event dispatched by the server is used to describe the fatal error to the client. -The client terminates the socket and closes the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. +The client closes the socket and the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. ## Keep-Alive @@ -34,17 +34,17 @@ Ping and Pong feature is a mandatory requirement by [The WebSocket Protocol](htt Direction: **Client -> Server** -Indicates that the client wants to establish a connection within the existing socket. This connection is **not** the actual WebSocket communication channel, but is rather a frame within it asking the server to allow future subscription operation requests. +Indicates that the client wants to establish a connection within the existing socket. This connection is **not** the actual WebSocket communication channel, but is rather a frame within it asking the server to allow future operation requests. The client can specify additional `connectionParams` which are sent through the `payload` field in the outgoing message. -The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will terminate the socket with the close event: `4408: Connection initialisation timeout`. +The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will close the socket with the event: `4408: Connection initialisation timeout`. -If the server receives more than one `ConnectionInit` message at any given time, the server will terminate the socket with the close event `4429: Too many initialisation requests`. +If the server receives more than one `ConnectionInit` message at any given time, the server will close the socket with the event `4429: Too many initialisation requests`. ```typescript interface ConnectionInitMessage { - type: "connection_init"; + type: 'connection_init'; payload?: Record; // connectionParams } ``` @@ -63,7 +63,7 @@ Potential response to the `ConnectionInit` message from the client acknowledging ```typescript interface ConnectionAckMessage { - type: "connection_ack"; + type: 'connection_ack'; } ``` @@ -73,16 +73,16 @@ The client is now **ready** to request subscription operations. Direction: **Client -> Server** -Requests a operation specified in the message `payload`. This message provides a unique ID field to connect future server messages to the operation started by this message. +Requests an operation specified in the message `payload`. This message provides a unique ID field to connect future server messages to the operation started by this message. If there is already an active subscriber for a `subscription` operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. Since `query` and `mutation` resolve to a single emitted value, their subscription does not require reservations for additional future events. Having this in mind, the server may not assert this rule for these operations. ```typescript -import { DocumentNode } from "graphql"; +import { DocumentNode } from 'graphql'; interface SubscribeMessage { - id: ""; - type: "subscribe"; + id: ''; + type: 'subscribe'; payload: { operationName?: string | null; query: string | DocumentNode; @@ -91,7 +91,7 @@ interface SubscribeMessage { } ``` -Executing operations is allowed **only** after the server has acknowledged the connection through the `ConnectionAck` message, if the connection is not acknowledged, the socket will be terminated immediately with a close event `4401: Unauthorized`. +Executing operations is allowed **only** after the server has acknowledged the connection through the `ConnectionAck` message, if the connection is not acknowledged, the socket will be closed immediately with the event `4401: Unauthorized`. ### `Next` @@ -103,11 +103,11 @@ Operation execution result message. - If the operation is a `subscription`, the message can be seen as an event in the source stream requested by the `Subscribe` message. ```typescript -import { ExecutionResult } from "graphql"; +import { ExecutionResult } from 'graphql'; interface NextMessage { - id: ""; - type: "next"; + id: ''; + type: 'next'; payload: ExecutionResult; } ``` @@ -119,11 +119,11 @@ Direction: **Server -> Client** Operation execution error(s) triggered by the `Next` message happening before the actual execution, usually due to validation errors. ```typescript -import { GraphQLError } from "graphql"; +import { GraphQLError } from 'graphql'; interface ErrorMessage { - id: ""; - type: "error"; + id: ''; + type: 'error'; payload: GraphQLError[]; } ``` @@ -132,14 +132,14 @@ interface ErrorMessage { Direction: **bidirectional** -- **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, **no `Complete` message will be emitted**. +- **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, no `Complete` message will be emitted. - **Client -> Server** (for `subscription` operations only) indicating that the client has stopped listening to the events and wants to complete the source stream. No further data events, relevant to the original subscription, should be sent through. ```typescript interface CompleteMessage { - id: ""; - type: "complete"; + id: ''; + type: 'complete'; } ``` @@ -147,7 +147,7 @@ interface CompleteMessage { Direction: **bidirectional** -Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket termination with a close event `4400: `. The `` can be vaguely descriptive on why the received message is invalid. +Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket closure with the event `4400: `. The `` can be vaguely descriptive on why the received message is invalid. ## Examples @@ -167,7 +167,7 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") 1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation 1. _Server_ validates the connection initialisation request and decides that the client is not allowed to establish a connection -1. _Server_ terminates the socket by dispatching the close event `4403: Forbidden` +1. _Server_ closes the socket by dispatching the event `4403: Forbidden` 1. _Client_ reports an error using the close event reason (which is `Forbidden`) ### Erroneous connection initialisation @@ -176,7 +176,7 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") 1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation 1. _Server_ tries validating the connection initialisation request but an error `I'm a teapot` is thrown -1. _Server_ terminates the socket by dispatching the close event `4400: I'm a teapot` +1. _Server_ closes the socket by dispatching the event `4400: I'm a teapot` 1. _Client_ reports an error using the close event reason (which is `I'm a teapot`) ### Connection initialisation timeout @@ -186,7 +186,7 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Client_ does not dispatch a `ConnectionInit` message 1. _Server_ waits for the `ConnectionInit` message for the duration specified in the `connectionInitWaitTimeout` parameter 1. _Server_ waiting time has passed -1. _Server_ terminates the socket by dispatching the close event `4408: Connection initialisation timeout` +1. _Server_ closes the socket by dispatching the event `4408: Connection initialisation timeout` 1. _Client_ reports an error using the close event reason (which is `Connection initialisation timeout`) ### Query/Mutation operation From 88858d0ce024df369574906fed36b257b24fbe82 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 19 Oct 2020 14:45:31 +0200 Subject: [PATCH 06/18] Support returning multiple results from `execute` --- rfcs/GraphQLOverWebSocket.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 2c4c55e8..0a1cf837 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -75,7 +75,7 @@ Direction: **Client -> Server** Requests an operation specified in the message `payload`. This message provides a unique ID field to connect future server messages to the operation started by this message. -If there is already an active subscriber for a `subscription` operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. Since `query` and `mutation` resolve to a single emitted value, their subscription does not require reservations for additional future events. Having this in mind, the server may not assert this rule for these operations. +If there is already an active subscriber for a live operation (any operation that emits **multiple** results) matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. Operations resolving to a **single** emitted result do not require reservations for additional future events - having this in mind, the server may not assert this rule for such cases. ```typescript import { DocumentNode } from 'graphql'; @@ -97,10 +97,7 @@ Executing operations is allowed **only** after the server has acknowledged the c Direction: **Server -> Client** -Operation execution result message. - -- If the operation is a `query` or `mutation`, the message can be seen as the final execution result. This message is followed by the `Complete` message indicating the completion of the operation. -- If the operation is a `subscription`, the message can be seen as an event in the source stream requested by the `Subscribe` message. +Operation execution result(s) from the source stream created by the binding `Subscribe` message. After all results have been emitted, the `Complete` message will follow indicating stream completion. ```typescript import { ExecutionResult } from 'graphql'; @@ -134,7 +131,7 @@ Direction: **bidirectional** - **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, no `Complete` message will be emitted. -- **Client -> Server** (for `subscription` operations only) indicating that the client has stopped listening to the events and wants to complete the source stream. No further data events, relevant to the original subscription, should be sent through. +- **Client -> Server** indicates that the client has stopped listening and wants to complete the source stream. No further events, relevant to the original subscription, should be sent through. ```typescript interface CompleteMessage { @@ -201,6 +198,19 @@ _The client and the server has already gone through [successful connection initi 1. _Server_ dispatches the `Complete` message with the matching unique ID indicating that the execution has completed 1. _Server_ triggers the `onComplete` callback, if specified +### Live Query operation + +_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ + +1. _Client_ generates a unique ID for the following operation +1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested `live query` operation passed through the `payload` field +1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation +1. _Server_ validates the request, establishes a GraphQL subscription on the `live query` and listens for data events in the source stream +1. _Server_ dispatches `Next` messages for every data event in the underlying `live query` source stream matching the client's unique ID +1. _Client_ stops the `live query` by dispatching a `Complete` message with the matching unique ID +1. _Server_ effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources +1. _Server_ triggers the `onComplete` callback, if specified + ### Subscribe operation _The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ From 2f3de95fec1907d263695e9e2ac694949eed39e3 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 22 Oct 2020 21:02:19 +0200 Subject: [PATCH 07/18] Further wording improvements --- rfcs/GraphQLOverWebSocket.md | 49 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 0a1cf837..05cbd2f7 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -73,9 +73,9 @@ The client is now **ready** to request subscription operations. Direction: **Client -> Server** -Requests an operation specified in the message `payload`. This message provides a unique ID field to connect future server messages to the operation started by this message. +Requests an operation specified in the message `payload`. This message provides a unique ID field to connect published messages to the operation requested by this message. -If there is already an active subscriber for a live operation (any operation that emits **multiple** results) matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. Operations resolving to a **single** emitted result do not require reservations for additional future events - having this in mind, the server may not assert this rule for such cases. +If there is already an active subscriber for a streaming operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. The server may not assert this rule for operations returning a single result as they do not require reservations for additional future events. ```typescript import { DocumentNode } from 'graphql'; @@ -186,42 +186,41 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Server_ closes the socket by dispatching the event `4408: Connection initialisation timeout` 1. _Client_ reports an error using the close event reason (which is `Connection initialisation timeout`) -### Query/Mutation operation +### Single result operation -_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ - -1. _Client_ generates a unique ID for the following operation -1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested `query`/`mutation` operation passed through the `payload` field -1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation -1. _Server_ validates the request and executes the GraphQL operation -1. _Server_ dispatches a `Next` message with the execution result matching the client's unique ID -1. _Server_ dispatches the `Complete` message with the matching unique ID indicating that the execution has completed -1. _Server_ triggers the `onComplete` callback, if specified - -### Live Query operation +#### `query` and `mutation` operations without streaming directives _The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ 1. _Client_ generates a unique ID for the following operation -1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested `live query` operation passed through the `payload` field +1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field +
_All future communication is linked through this unique ID_ 1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation -1. _Server_ validates the request, establishes a GraphQL subscription on the `live query` and listens for data events in the source stream -1. _Server_ dispatches `Next` messages for every data event in the underlying `live query` source stream matching the client's unique ID -1. _Client_ stops the `live query` by dispatching a `Complete` message with the matching unique ID -1. _Server_ effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources +1. _Server_ validates the request and executes the single result GraphQL operation +1. _Server_ dispatches the `Next` message with the execution result +1. _Server_ dispatches the `Complete` message indicating that the execution has completed 1. _Server_ triggers the `onComplete` callback, if specified -### Subscribe operation +### Streaming operation + +#### `subscription` operation and queries with streaming directives _The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ 1. _Client_ generates a unique ID for the following operation -1. _Client_ dispatches the `Subscribe` message with the, previously generated, unique ID through the `id` field and the requested subscription operation passed through the `payload` field +1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested streaming operation passed through the `payload` field +
_All future communication is linked through this unique ID_ 1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation -1. _Server_ validates the request, establishes a GraphQL subscription and listens for events in the source stream -1. _Server_ dispatches `Next` messages for every event in the underlying subscription source stream matching the client's unique ID -1. _Client_ stops the subscription by dispatching a `Complete` message with the matching unique ID -1. _Server_ effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources +1. _Server_ validates the request and executes the streaming GraphQL operation +1. _Server_ checks if the generated ID is unique across active streaming subscriptions + - If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for already exists` + - If unique, continue... +1. _Server_ dispatches `Next` messages for every event in the source stream +1. - _Client_ stops the subscription by dispatching a `Complete` message + - _Server_ completes the source stream +
_or_ + - _Server_ dispatches the `Complete` message indicating that the source stream has completed + - _Client_ completes the stream observer 1. _Server_ triggers the `onComplete` callback, if specified ## Implementations From 7fa10074dd201dab4cab6f5842e0fd3441f4ab27 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Sun, 25 Oct 2020 02:01:00 +0200 Subject: [PATCH 08/18] Example improvements --- rfcs/GraphQLOverWebSocket.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 05cbd2f7..3ca31432 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -195,11 +195,19 @@ _The client and the server has already gone through [successful connection initi 1. _Client_ generates a unique ID for the following operation 1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
_All future communication is linked through this unique ID_ -1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation -1. _Server_ validates the request and executes the single result GraphQL operation -1. _Server_ dispatches the `Next` message with the execution result +1. _Server_ triggers the `onSubscribe` callback + + - If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload + - If `ExecutionArgs` are returned, they will be used directly + +1. _Server_ executes the single result GraphQL operation using the arguments provided above +1. _Server_ triggers the `onNext` callback + + - If `ExecutionResult` is **not** returned, the direct result from the operation will be dispatched with the `Next` message + - If `ExecutionResult` is returned, it will be dispatched with the `Next` message + +1. _Server_ triggers the `onComplete` callback 1. _Server_ dispatches the `Complete` message indicating that the execution has completed -1. _Server_ triggers the `onComplete` callback, if specified ### Streaming operation @@ -208,14 +216,24 @@ _The client and the server has already gone through [successful connection initi _The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ 1. _Client_ generates a unique ID for the following operation -1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested streaming operation passed through the `payload` field +1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
_All future communication is linked through this unique ID_ -1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation -1. _Server_ validates the request and executes the streaming GraphQL operation +1. _Server_ triggers the `onSubscribe` callback + + - If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload + - If `ExecutionArgs` are returned, they will be used directly + +1. _Server_ executes the streaming GraphQL operation using the arguments provided above 1. _Server_ checks if the generated ID is unique across active streaming subscriptions + - If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for already exists` - If unique, continue... -1. _Server_ dispatches `Next` messages for every event in the source stream + +1. _Server_ triggers the `onNext` callback + + - If `ExecutionResult` is **not** returned, the direct events from the source stream will be dispatched with the `Next` message + - If `ExecutionResult` is returned, it will be dispatched with the `Next` message instead of every event from the source stram + 1. - _Client_ stops the subscription by dispatching a `Complete` message - _Server_ completes the source stream
_or_ From 4854c40513b2ac43176c777f009d62fb21dccdce Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Sun, 25 Oct 2020 16:33:57 +0100 Subject: [PATCH 09/18] Repo rename --- rfcs/GraphQLOverWebSocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 3ca31432..0168f9b3 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -243,4 +243,4 @@ _The client and the server has already gone through [successful connection initi ## Implementations -- [graphql-transport-ws](https://github.com/enisdenjo/graphql-transport-ws) +- [graphql-ws](https://github.com/enisdenjo/graphql-ws) From e4b7e6a06b99c41a8702fdf5d8b23ae825dab911 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Sat, 31 Oct 2020 21:43:14 +0100 Subject: [PATCH 10/18] Further refinements --- rfcs/GraphQLOverWebSocket.md | 69 ++++-------------------------------- 1 file changed, 7 insertions(+), 62 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 0168f9b3..ffdf4ede 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -20,14 +20,6 @@ The server can close the socket (kick the client off) at any time. The close eve The client closes the socket and the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. -## Keep-Alive - -The server will occasionally check if the client is still "alive", available and listening. In order to perform this check, implementation leverages the standardized [Pings and Pongs: The Heartbeat of WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets). - -Keep-Alive interval and the "pong wait" timeout can be tuned by using the accompanying configuration parameter on the server. - -Ping and Pong feature is a mandatory requirement by [The WebSocket Protocol](https://tools.ietf.org/html/rfc6455#section-5.5.2). All clients that don't support it are **not** RFC6455 compliant and will simply have their socket terminated after the pong wait has passed. - ## Message types ### `ConnectionInit` @@ -36,8 +28,6 @@ Direction: **Client -> Server** Indicates that the client wants to establish a connection within the existing socket. This connection is **not** the actual WebSocket communication channel, but is rather a frame within it asking the server to allow future operation requests. -The client can specify additional `connectionParams` which are sent through the `payload` field in the outgoing message. - The server must receive the connection initialisation message within the allowed waiting time specified in the `connectionInitWaitTimeout` parameter during the server setup. If the client does not request a connection within the allowed timeout, the server will close the socket with the event: `4408: Connection initialisation timeout`. If the server receives more than one `ConnectionInit` message at any given time, the server will close the socket with the event `4429: Too many initialisation requests`. @@ -45,21 +35,15 @@ If the server receives more than one `ConnectionInit` message at any given time, ```typescript interface ConnectionInitMessage { type: 'connection_init'; - payload?: Record; // connectionParams + payload?: Record; } ``` -The server will respond by either: - -- Dispatching a `ConnectionAck` message acknowledging that the connection has been successfully established. The server does not implement the `onConnect` callback or the implemented callback has returned `true`. -- Closing the socket with a close event `4403: Forbidden` indicating that the connection request has been denied because of access control. The server has returned `false` in the `onConnect` callback. -- Closing the socket with a close event `4400: ` indicating that the connection request has been denied because of an implementation specific error. The server has thrown an error in the `onConnect` callback, the thrown error's message is the `` in the close event. - ### `ConnectionAck` Direction: **Server -> Client** -Potential response to the `ConnectionInit` message from the client acknowledging a successful connection with the server. +Expected response to the `ConnectionInit` message from the client acknowledging a successful connection with the server. ```typescript interface ConnectionAckMessage { @@ -154,28 +138,10 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` 1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") -1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation +1. _Client_ immediately dispatches a `ConnectionInit` message optionally providing a payload as agreed with the server 1. _Server_ validates the connection initialisation request and dispatches a `ConnectionAck` message to the client on successful connection 1. _Client_ has received the acknowledgement message and is now ready to request operation executions -### Forbidden connection initialisation - -1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` -1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") -1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation -1. _Server_ validates the connection initialisation request and decides that the client is not allowed to establish a connection -1. _Server_ closes the socket by dispatching the event `4403: Forbidden` -1. _Client_ reports an error using the close event reason (which is `Forbidden`) - -### Erroneous connection initialisation - -1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` -1. _Server_ accepts the handshake and establishes a WebSocket communication channel (which we call "socket") -1. _Client_ immediately dispatches a `ConnectionInit` message setting the `connectionParams` according to the server implementation -1. _Server_ tries validating the connection initialisation request but an error `I'm a teapot` is thrown -1. _Server_ closes the socket by dispatching the event `4400: I'm a teapot` -1. _Client_ reports an error using the close event reason (which is `I'm a teapot`) - ### Connection initialisation timeout 1. _Client_ sends a WebSocket handshake request with the sub-protocol: `graphql-transport-ws` @@ -184,7 +150,6 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Server_ waits for the `ConnectionInit` message for the duration specified in the `connectionInitWaitTimeout` parameter 1. _Server_ waiting time has passed 1. _Server_ closes the socket by dispatching the event `4408: Connection initialisation timeout` -1. _Client_ reports an error using the close event reason (which is `Connection initialisation timeout`) ### Single result operation @@ -195,18 +160,8 @@ _The client and the server has already gone through [successful connection initi 1. _Client_ generates a unique ID for the following operation 1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
_All future communication is linked through this unique ID_ -1. _Server_ triggers the `onSubscribe` callback - - - If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload - - If `ExecutionArgs` are returned, they will be used directly - -1. _Server_ executes the single result GraphQL operation using the arguments provided above -1. _Server_ triggers the `onNext` callback - - - If `ExecutionResult` is **not** returned, the direct result from the operation will be dispatched with the `Next` message - - If `ExecutionResult` is returned, it will be dispatched with the `Next` message - -1. _Server_ triggers the `onComplete` callback +1. _Server_ executes the single result GraphQL operation +1. _Server_ dispatches the result with the `Next` message 1. _Server_ dispatches the `Complete` message indicating that the execution has completed ### Streaming operation @@ -218,28 +173,18 @@ _The client and the server has already gone through [successful connection initi 1. _Client_ generates a unique ID for the following operation 1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
_All future communication is linked through this unique ID_ -1. _Server_ triggers the `onSubscribe` callback - - - If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload - - If `ExecutionArgs` are returned, they will be used directly - -1. _Server_ executes the streaming GraphQL operation using the arguments provided above +1. _Server_ executes the streaming GraphQL operation 1. _Server_ checks if the generated ID is unique across active streaming subscriptions - If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for already exists` - If unique, continue... -1. _Server_ triggers the `onNext` callback - - - If `ExecutionResult` is **not** returned, the direct events from the source stream will be dispatched with the `Next` message - - If `ExecutionResult` is returned, it will be dispatched with the `Next` message instead of every event from the source stram - +1. _Server_ dispatches results over time with the `Next` message 1. - _Client_ stops the subscription by dispatching a `Complete` message - _Server_ completes the source stream
_or_ - _Server_ dispatches the `Complete` message indicating that the source stream has completed - _Client_ completes the stream observer -1. _Server_ triggers the `onComplete` callback, if specified ## Implementations From 7d900fc2aa364de3b74d68b0b185ae82df8b5697 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Tue, 3 Nov 2020 16:23:58 +0100 Subject: [PATCH 11/18] query must be a string --- rfcs/GraphQLOverWebSocket.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index ffdf4ede..7e47a5be 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -62,14 +62,12 @@ Requests an operation specified in the message `payload`. This message provides If there is already an active subscriber for a streaming operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. The server may not assert this rule for operations returning a single result as they do not require reservations for additional future events. ```typescript -import { DocumentNode } from 'graphql'; - interface SubscribeMessage { id: ''; type: 'subscribe'; payload: { operationName?: string | null; - query: string | DocumentNode; + query: string; variables?: Record | null; }; } From 2859ed224fdac7b7a804e8c0a9376d7413043fb4 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Sat, 7 Nov 2020 10:14:28 +0100 Subject: [PATCH 12/18] Optional payload with connection ack --- rfcs/GraphQLOverWebSocket.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 7e47a5be..2c17b78a 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -45,9 +45,12 @@ Direction: **Server -> Client** Expected response to the `ConnectionInit` message from the client acknowledging a successful connection with the server. +The server can use the optional `payload` field to transfer additional details about the connection. + ```typescript interface ConnectionAckMessage { type: 'connection_ack'; + payload?: Record; } ``` From edc4ab51ea206d2052cf108915d8e04ccdcabe0b Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 13 Jan 2021 01:06:16 +0100 Subject: [PATCH 13/18] Enforce ID uniqueness and all operations are cancelable --- rfcs/GraphQLOverWebSocket.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 2c17b78a..7bd8c548 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -62,7 +62,7 @@ Direction: **Client -> Server** Requests an operation specified in the message `payload`. This message provides a unique ID field to connect published messages to the operation requested by this message. -If there is already an active subscriber for a streaming operation matching the provided ID, the server will close the socket immediately with the event `4409: Subscriber for already exists`. The server may not assert this rule for operations returning a single result as they do not require reservations for additional future events. +If there is already an active subscriber for an operation matching the provided ID, regardless of the operation type, the server must close the socket immediately with the event `4409: Subscriber for already exists`. ```typescript interface SubscribeMessage { @@ -116,7 +116,7 @@ Direction: **bidirectional** - **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, no `Complete` message will be emitted. -- **Client -> Server** indicates that the client has stopped listening and wants to complete the source stream. No further events, relevant to the original subscription, should be sent through. +- **Client -> Server** indicates that the client has stopped listening and wants to complete the subscription. No further events, relevant to the original subscription, should be sent through. Even if the client completed a single result operation before it resolved, the result should not be sent through once it does. ```typescript interface CompleteMessage { From 478e46f00f5904e3f66c59d99ab475e76dc38879 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 31 May 2021 22:29:41 +0200 Subject: [PATCH 14/18] add extensions field to subscibe message payload --- rfcs/GraphQLOverWebSocket.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 7bd8c548..2193aa8a 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -72,6 +72,7 @@ interface SubscribeMessage { operationName?: string | null; query: string; variables?: Record | null; + extensions?: Record | null; }; } ``` From cdf369575df28df9a5944848f1ad698661a3bb48 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Mon, 31 May 2021 22:30:30 +0200 Subject: [PATCH 15/18] unnecessary implementations section --- rfcs/GraphQLOverWebSocket.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 2193aa8a..323a1f8e 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -187,7 +187,3 @@ _The client and the server has already gone through [successful connection initi
_or_ - _Server_ dispatches the `Complete` message indicating that the source stream has completed - _Client_ completes the stream observer - -## Implementations - -- [graphql-ws](https://github.com/enisdenjo/graphql-ws) From 574a0cf3a0cab520bbc44df4351beb6c9e4912cb Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Tue, 8 Jun 2021 22:38:28 +0200 Subject: [PATCH 16/18] ping and pong messages --- rfcs/GraphQLOverWebSocket.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 323a1f8e..3af2e020 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -56,6 +56,36 @@ interface ConnectionAckMessage { The client is now **ready** to request subscription operations. +### `Ping` + +Direction: **bidirectional** + +Useful for detecting failed connections, displaying latency metrics or other types of network probing. + +A `Pong` must be sent in response from the receiving party as soon as possible. + +The `Ping` message can be sent at any time within the established socket. + +```typescript +interface PingMessage { + type: 'ping'; +} +``` + +### `Pong` + +Direction: **bidirectional** + +The response to the `Ping` message. Must be sent as soon as the `Ping` message is received. + +The `Pong` message can be sent at any time within the established socket. Furthermore, the `Pong` message may even be sent unsolicited as an unidirectional heartbeat. + +```typescript +interface PongMessage { + type: 'pong'; +} +``` + ### `Subscribe` Direction: **Client -> Server** From 2e687b76ea184f1a098b00a9e14b0792e126759f Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 9 Jun 2021 13:16:48 +0200 Subject: [PATCH 17/18] optional payload for ping and pong messages --- rfcs/GraphQLOverWebSocket.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 3af2e020..28f70036 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -66,9 +66,12 @@ A `Pong` must be sent in response from the receiving party as soon as possible. The `Ping` message can be sent at any time within the established socket. +The optional `payload` field can be used to transfer additional details about the ping. + ```typescript interface PingMessage { type: 'ping'; + payload?: Record; } ``` @@ -80,9 +83,12 @@ The response to the `Ping` message. Must be sent as soon as the `Ping` message i The `Pong` message can be sent at any time within the established socket. Furthermore, the `Pong` message may even be sent unsolicited as an unidirectional heartbeat. +The optional `payload` field can be used to transfer additional details about the pong. + ```typescript interface PongMessage { type: 'pong'; + payload?: Record; } ``` From 3afac4a440ca5cdce41f11cd83b62c123ed235b6 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 25 Mar 2022 14:19:30 +0100 Subject: [PATCH 18/18] smaller improvements and clarifications As discussed in https://github.com/enisdenjo/graphql-ws/discussions/340 --- rfcs/GraphQLOverWebSocket.md | 65 +++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/rfcs/GraphQLOverWebSocket.md b/rfcs/GraphQLOverWebSocket.md index 28f70036..c0196761 100644 --- a/rfcs/GraphQLOverWebSocket.md +++ b/rfcs/GraphQLOverWebSocket.md @@ -16,6 +16,8 @@ Messages are represented through the JSON structure and are stringified before b - `id` used for uniquely identifying server responses and connecting them with the client's requests - `payload` holding the extra "payload" information to go with the specific message type +Multiple operations identified with separate IDs can be active at any time and their messages can be interleaved on the connection. + The server can close the socket (kick the client off) at any time. The close event dispatched by the server is used to describe the fatal error to the client. The client closes the socket and the connection by dispatching a `1000: Normal Closure` close event to the server indicating a normal closure. @@ -98,7 +100,9 @@ Direction: **Client -> Server** Requests an operation specified in the message `payload`. This message provides a unique ID field to connect published messages to the operation requested by this message. -If there is already an active subscriber for an operation matching the provided ID, regardless of the operation type, the server must close the socket immediately with the event `4409: Subscriber for already exists`. +If there is already an active subscriber for an operation matching the provided ID, regardless of the operation type, the server **must** close the socket immediately with the event `4409: Subscriber for already exists`. + +The server needs only keep track of IDs for as long as the subscription is active. Once a client completes an operation, it is free to re-use that ID. ```typescript interface SubscribeMessage { @@ -135,7 +139,7 @@ interface NextMessage { Direction: **Server -> Client** -Operation execution error(s) triggered by the `Next` message happening before the actual execution, usually due to validation errors. +Operation execution error(s) in response to the `Subscribe` message. This can occur _before_ execution starts, usually due to validation errors, or _during_ the execution of the request. This message terminates the operation and no further messages will be sent. ```typescript import { GraphQLError } from 'graphql'; @@ -153,7 +157,9 @@ Direction: **bidirectional** - **Server -> Client** indicates that the requested operation execution has completed. If the server dispatched the `Error` message relative to the original `Subscribe` message, no `Complete` message will be emitted. -- **Client -> Server** indicates that the client has stopped listening and wants to complete the subscription. No further events, relevant to the original subscription, should be sent through. Even if the client completed a single result operation before it resolved, the result should not be sent through once it does. +- **Client -> Server** indicates that the client has stopped listening and wants to complete the subscription. No further events, relevant to the original subscription, should be sent through. Even if the client sent a `Complete` message for a _single-result-operation_ before it resolved, the result should not be sent through once it does. + +Note: The asynchronous nature of the full-duplex connection means that a client can send a `Complete` message to the server even when messages are in-flight to the client, or when the server has itself completed the operation (via a `Error` or `Complete` message). Both client and server must therefore be prepared to receive (and ignore) messages for operations that they consider already completed. ```typescript interface CompleteMessage { @@ -168,6 +174,8 @@ Direction: **bidirectional** Receiving a message of a type or format which is not specified in this document will result in an **immediate** socket closure with the event `4400: `. The `` can be vaguely descriptive on why the received message is invalid. +Receiving a message (other than `Subscribe`) with an ID that belongs to an operation that has been previously completed does not constitute an error. It is permissable to simply ignore all _unknown_ IDs without closing the connection. + ## Examples For the sake of clarity, the following examples demonstrate the communication protocol. @@ -189,19 +197,6 @@ For the sake of clarity, the following examples demonstrate the communication pr 1. _Server_ waiting time has passed 1. _Server_ closes the socket by dispatching the event `4408: Connection initialisation timeout` -### Single result operation - -#### `query` and `mutation` operations without streaming directives - -_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ - -1. _Client_ generates a unique ID for the following operation -1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field -
_All future communication is linked through this unique ID_ -1. _Server_ executes the single result GraphQL operation -1. _Server_ dispatches the result with the `Next` message -1. _Server_ dispatches the `Complete` message indicating that the execution has completed - ### Streaming operation #### `subscription` operation and queries with streaming directives @@ -217,9 +212,39 @@ _The client and the server has already gone through [successful connection initi - If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for already exists` - If unique, continue... +1. _Server_ _optionally_ checks if the operation is valid before starting executing it, e.g. checking permissions + + - If **not** valid, the _server_ sends an `Error` message and deems the operation complete. + - If valid, continue... + 1. _Server_ dispatches results over time with the `Next` message -1. - _Client_ stops the subscription by dispatching a `Complete` message - - _Server_ completes the source stream -
_or_ - - _Server_ dispatches the `Complete` message indicating that the source stream has completed +1. - _Server_ dispatches the `Complete` message indicating that the source stream has completed - _Client_ completes the stream observer +
**or** + - _Client_ stops the subscription by dispatching a `Complete` message + - _Server_ receives `Complete` message and completes the source stream + - _Client_ ignores all further messages that it recives with this ID +
**or** + - _Server_ dispatches the `Complete` message indicating that the source stream has completed + - **Simultaneously** _client_ stops the subscription by dispatching a `Complete` message + - _Client_ ignores all further messages that it recives with this ID + - _Server_ ignores the `Complete` message from the client + +### Single result operation + +#### `query` and `mutation` operations without streaming directives + +A single result operation is identical to a streaming operation except that _at most one_ `Next` message is sent. + +It shares the same name-space for IDs as streaming operations and can be multiplexed with other operations on the connection. + +_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._ + +1. _Client_ generates a unique ID for the following operation +1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field +
_All future communication is linked through this unique ID_ +1. _Server_ executes the single result GraphQL operation +1. _Server_ dispatches the result with the `Next` message +1. _Server_ dispatches the `Complete` message indicating that the execution has completed + +The _client_ may dispatch a `Complete` message at any time, just as shown in the streaming operations examples above, and the same interactions ensue.