Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL over WebSocket Protocol #2

Merged
merged 15 commits into from
Aug 17, 2020
Merged

GraphQL over WebSocket Protocol #2

merged 15 commits into from
Aug 17, 2020

Conversation

enisdenjo
Copy link
Owner

@enisdenjo enisdenjo commented Aug 6, 2020

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.

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.

interface ConnectionInitMessage {
  type: 'connection_init';
  payload?: Record<string, unknown>; // 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: <error-message> 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 <error-message> in the close event.

ConnectionAck

Direction: Server -> Client

Potential response to the ConnectionInit message from the client acknowledging a successful connection with the server.

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.

import { DocumentNode } from 'graphql';

interface SubscribeMessage {
  id: '<unique-operation-id>';
  type: 'subscribe';
  payload: {
    operationName: string;
    query: string | DocumentNode;
    variables: Record<string, unknown>;
  };
}

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.
import { ExecutionResult } from 'graphql';

interface NextMessage {
  id: '<unique-operation-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.

import { GraphQLError } from 'graphql';

interface ErrorMessage {
  id: '<unique-operation-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.

interface CompleteMessage {
  id: '<unique-operation-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: <error-message>. The <error-message> 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
  2. Server accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
  3. Client immediately dispatches a ConnectionInit message setting the connectionParams according to the server implementation
  4. Server validates the connection initialisation request and dispatches a ConnectionAck message to the client on successful connection
  5. 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
  2. Server accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
  3. Client immediately dispatches a ConnectionInit message setting the connectionParams according to the server implementation
  4. Server validates the connection initialisation request and decides that the client is not allowed to establish a connection
  5. Server terminates the socket by dispatching the close event 4403: Forbidden
  6. 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
  2. Server accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
  3. Client immediately dispatches a ConnectionInit message setting the connectionParams according to the server implementation
  4. Server tries validating the connection initialisation request but an error I'm a teapot is thrown
  5. Server terminates the socket by dispatching the close event 4400: I'm a teapot
  6. 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
  2. Server accepts the handshake and establishes a WebSocket communication channel (which we call "socket")
  3. Client does not dispatch a ConnectionInit message
  4. Server waits for the ConnectionInit message for the duration specified in the connectionInitWaitTimeout parameter
  5. Server waiting time has passed
  6. Server terminates the socket by dispatching the close event 4408: Connection initialisation timeout
  7. 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.

  1. Client generates a unique ID for the following operation
  2. 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
  3. Server triggers the onSubscribe callback, if specified, and uses the returned ExecutionArgs for the operation
  4. Server validates the request and executes the GraphQL operation
  5. Server dispatches a Next message with the execution result matching the client's unique ID
  6. Server dispatches the Complete message with the matching unique ID indicating that the execution has completed
  7. Server triggers the onComplete callback, if specified

Subscribe operation

The client and the server has already gone through successful connection initialisation.

  1. Client generates a unique ID for the following operation
  2. 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
  3. Server triggers the onSubscribe callback, if specified, and uses the returned ExecutionArgs for the operation
  4. Server validates the request, establishes a GraphQL subscription and listens for events in the source stream
  5. Server dispatches Next messages for every event in the underlying subscription source stream matching the client's unique ID
  6. Client stops the subscription by dispatching a Complete message with the matching unique ID
  7. Server effectively stops the GraphQL subscription by completing/disposing the underlying source stream and cleaning up related resources
  8. Server triggers the onComplete callback, if specified

@enisdenjo enisdenjo marked this pull request as ready for review August 7, 2020 11:52
@enisdenjo enisdenjo changed the title [RFC] GraphQL subscriptions over WebSocket Protocol GraphQL subscriptions over WebSocket Protocol Aug 7, 2020
@enisdenjo enisdenjo force-pushed the protocol branch 4 times, most recently from b8245c1 to ef1c979 Compare August 8, 2020 11:34
@enisdenjo enisdenjo changed the title GraphQL subscriptions over WebSocket Protocol GraphQL over WebSocket Protocol Aug 8, 2020
@enisdenjo enisdenjo force-pushed the protocol branch 3 times, most recently from 9b3aa2e to 3d7c075 Compare August 8, 2020 13:19
@enisdenjo enisdenjo merged commit 42045c5 into master Aug 17, 2020
@enisdenjo enisdenjo deleted the protocol branch August 17, 2020 20:10
enisdenjo pushed a commit that referenced this pull request Aug 17, 2020
# [1.0.0](v0.0.2...v1.0.0) (2020-08-17)

### Features

* **client:** Re-implement following the new transport protocol ([#6](#6)) ([5191a35](5191a35))
* **server:** Implement following the new transport protocol ([#1](#1)) ([a412d25](a412d25))
* Rewrite GraphQL over WebSocket Protocol ([#2](#2)) ([42045c5](42045c5))

### BREAKING CHANGES

* This lib is no longer compatible with [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws). It follows a redesigned transport protocol aiming to improve security, stability and reduce ambiguity.
@enisdenjo
Copy link
Owner Author

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Aug 17, 2020
github-actions bot referenced this pull request in ilijaNL/graphql-transport-ws Mar 16, 2023
# 1.0.0 (2023-03-16)

### Bug Fixes

* Add `browser` export map ([ea306db](ea306db))
* Add `package.json` to exports map ([enisdenjo#119](https://github.com/ilijaNL/graphql-ws/issues/119)) ([1f09863](1f09863)), closes [enisdenjo#118](https://github.com/ilijaNL/graphql-ws/issues/118)
* Add `uWebSockets` exports path ([36247cb](36247cb)), closes [enisdenjo#155](https://github.com/ilijaNL/graphql-ws/issues/155)
* Add support for `graphql@v16` ([ad5aea2](ad5aea2))
* add the sink to the subscribed map AFTER emitting a subscribe message ([814f46c](814f46c))
* Add types path to package.json `exports` ([enisdenjo#375](https://github.com/ilijaNL/graphql-ws/issues/375)) ([9f394d7](9f394d7))
* **client:** `complete` should not be called after subscription `error` ([1fba419](1fba419))
* **client:** `ConnectionInit` payload is absent if `connectionParams` returns nothing ([98f8265](98f8265))
* **client:** `isFatalConnectionProblem` defaults to undefined for using `shouldRetry` ([9d5c573](9d5c573))
* **client:** Accept nullish values for `operationName` and `variables` ([2d60dda](2d60dda))
* **client:** cant read the `CloseEvent.reason` after bundling so just pass the whole event to the sink error and let the user handle it ([9ccb46b](9ccb46b))
* **client:** Close event's `wasClean` is not necessary ([2c65f0e](2c65f0e)), closes [enisdenjo#81](https://github.com/ilijaNL/graphql-ws/issues/81)
* **client:** Close with error message during connecting issues ([f8ecdd7](f8ecdd7))
* **client:** Connection locks dont increment on retries ([1e7bd97](1e7bd97)), closes [enisdenjo#153](https://github.com/ilijaNL/graphql-ws/issues/153)
* **client:** Debounce close by `lazyCloseTimeout` ([c332837](c332837)), closes [enisdenjo#388](https://github.com/ilijaNL/graphql-ws/issues/388)
* **client:** Dispose of subscription on complete or error messages ([enisdenjo#23](https://github.com/ilijaNL/graphql-ws/issues/23)) ([fb4d8e9](fb4d8e9))
* **client:** Distinguish client connection closes ([ed4d9db](ed4d9db))
* **client:** Don't complete after connection error ([5f829c3](5f829c3))
* **client:** Export relevant elements from the browser bundle ([b106dbe](b106dbe)), closes [enisdenjo#97](https://github.com/ilijaNL/graphql-ws/issues/97)
* **client:** Lazy connects after successful reconnects are not retries ([99b85a3](99b85a3))
* **client:** Limit client emitted error close message size ([2d959f6](2d959f6))
* **client:** New `error` event listener for handling connection errors ([enisdenjo#136](https://github.com/ilijaNL/graphql-ws/issues/136)) ([127b69f](127b69f)), closes [enisdenjo#135](https://github.com/ilijaNL/graphql-ws/issues/135)
* **client:** No retries when disposed ([0d5e6c2](0d5e6c2))
* **client:** One cleanup per subscription ([enisdenjo#67](https://github.com/ilijaNL/graphql-ws/issues/67)) ([5a5ae4d](5a5ae4d))
* **client:** Only `query` is required in the subscribe payload ([e892530](e892530))
* **client:** Reduce WebSocket event listeners and add new client `message` event ([enisdenjo#104](https://github.com/ilijaNL/graphql-ws/issues/104)) ([68d0e20](68d0e20)), closes [enisdenjo#102](https://github.com/ilijaNL/graphql-ws/issues/102)
* **client:** Report close causing internal errors to error listeners ([4e7e389](4e7e389))
* **client:** Report close error even if `Complete` message followed ([27754b2](27754b2)), closes [enisdenjo#245](https://github.com/ilijaNL/graphql-ws/issues/245)
* **client:** Return ping's payload through the response pong ([ee6193a](ee6193a))
* **client:** send complete message and close only if socket is still open ([49b75ce](49b75ce))
* **client:** Should emit `closed` event when disposing ([5800de8](5800de8)), closes [enisdenjo#108](https://github.com/ilijaNL/graphql-ws/issues/108)
* **client:** Shouldn't reconnect if all subscriptions complete while waiting for retry ([2826c10](2826c10)), closes [enisdenjo#163](https://github.com/ilijaNL/graphql-ws/issues/163)
* **client:** Shouldn’t send the `Complete` message if socket is not open ([cd12024](cd12024))
* **client:** Some close events are not worth retrying ([4d9134b](4d9134b))
* **client:** Specify and fail on fatal internal WebSocket close codes ([a720125](a720125))
* **client:** Stabilize and simplify internals ([enisdenjo#100](https://github.com/ilijaNL/graphql-ws/issues/100)) ([5ff8f1d](5ff8f1d)), closes [enisdenjo#99](https://github.com/ilijaNL/graphql-ws/issues/99) [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Stop execution if `connectionParams` took too long and the server kicked the client off ([1e94e45](1e94e45)), closes [enisdenjo#331](https://github.com/ilijaNL/graphql-ws/issues/331)
* **client:** Subscribes even if socket is in CLOSING state due to all subscriptions being completed ([3e3b8b7](3e3b8b7)), closes [enisdenjo#173](https://github.com/ilijaNL/graphql-ws/issues/173) [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscription can be disposed only once ([abd9c28](abd9c28)), closes [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscriptions acquire locks ([eb6cb2a](eb6cb2a))
* **client:** Time retries and socket change waits ([7c707db](7c707db)), closes [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Wait for server acknowledgement indefinitely ([a4bd602](a4bd602)), closes [enisdenjo#98](https://github.com/ilijaNL/graphql-ws/issues/98)
* Close the details tag in the README ([84144c4](84144c4))
* correctly detect WebSocket server ([eab29dc](eab29dc))
* Define entry points through the `exports`  field and use `.mjs` suffixed ESM imports ([enisdenjo#110](https://github.com/ilijaNL/graphql-ws/issues/110)) ([4196238](4196238))
* Define graphql execution results ([a64c91b](a64c91b))
* Drop TypeScript DOM lib dependency ([a81e8c1](a81e8c1))
* export both the client and the server from index ([29923b1](29923b1))
* Export useful types ([e4cc4d4](e4cc4d4))
* **fastify-websocket:** Handle connection and socket emitted errors ([71e9586](71e9586))
* **fastify-websocket:** Handle server emitted errors ([3fa17a7](3fa17a7))
* http and ws have no default exports ([5c01ed9](5c01ed9))
* include `types` file holding important types ([f3e4edf](f3e4edf))
* Main entrypoint in `exports` is just `"."` ([8f70b02](8f70b02))
* **message:** Allow `data` field to be of any type ([533248e](533248e)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* **message:** Allow `payload` field to be of any type for `NextMessage` ([7cebbfe](7cebbfe)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* Node 10 is the min supported version ([19844d7](19844d7))
* notify only relevant sinks about errors or completions ([62155ba](62155ba))
* Only UMD build has side effects ([66ed43f](66ed43f))
* Reorder types paths in package.json for better import resolution ([enisdenjo#406](https://github.com/ilijaNL/graphql-ws/issues/406)) ([37263c5](37263c5))
* reset connected/connecting state when disconnecting and disposing ([2eb3cd5](2eb3cd5))
* **server:** `handleProtocols` accepts arrays too and gracefully rejects other types ([98dec1a](98dec1a)), closes [enisdenjo#318](https://github.com/ilijaNL/graphql-ws/issues/318)
* **server:** `onDisconnect` is called exclusively if the connection is acknowledged ([33ed5f2](33ed5f2))
* **server:** `return` instead of `break` at switch case ends ([e9447e4](e9447e4)), closes [enisdenjo#140](https://github.com/ilijaNL/graphql-ws/issues/140)
* **server:** `subscription` operations are distinct on the message ID ([enisdenjo#24](https://github.com/ilijaNL/graphql-ws/issues/24)) ([dfffb05](dfffb05))
* **server:** allow skipping init message wait with zero values ([a7419df](a7419df))
* **server:** Async iterator must implement `return` ([d99982b](d99982b)), closes [enisdenjo#149](https://github.com/ilijaNL/graphql-ws/issues/149)
* **server:** Client can complete/cancel any operation ([0ad1c4c](0ad1c4c))
* **server:** Close socket if `onSubscribe` returns invalid array ([enisdenjo#53](https://github.com/ilijaNL/graphql-ws/issues/53)) ([0464a54](0464a54))
* **server:** Consistently set `rootValue` and `contextValue`, if not overridden ([enisdenjo#49](https://github.com/ilijaNL/graphql-ws/issues/49)) ([7aa3bcd](7aa3bcd))
* **server:** Distribute server error to all clients even if one error listener throws ([enisdenjo#56](https://github.com/ilijaNL/graphql-ws/issues/56)) ([b96dbb9](b96dbb9))
* **server:** Don't surface bad request error details in production ([enisdenjo#55](https://github.com/ilijaNL/graphql-ws/issues/55)) ([70317b2](70317b2))
* **server:** Enforce ID uniqueness across all operations and during the whole subscription life ([enisdenjo#96](https://github.com/ilijaNL/graphql-ws/issues/96)) ([65d1bfa](65d1bfa))
* **server:** Handle upgrade requests with multiple subprotocols and omit `Sec-WebSocket-Protocol` header if none supported ([9bae064](9bae064))
* **server:** Hide internal server error messages from the client in production ([36fe405](36fe405)), closes [enisdenjo#31](https://github.com/ilijaNL/graphql-ws/issues/31)
* **server:** Init context first on connection open ([a80e753](a80e753)), closes [enisdenjo#181](https://github.com/ilijaNL/graphql-ws/issues/181)
* **server:** Limit internal server error close message size ([8479f76](8479f76))
* **server:** Log internal errors to the console ([6ddf0d1](6ddf0d1))
* **server:** Make sure to use `onSubscribe` result exclusively ([51fdb7c](51fdb7c))
* **server:** No need to bind `this` scope ([f76ac73](f76ac73))
* **server:** Operation result can be async generator or iterable ([b1fb883](b1fb883))
* **server:** Receiving more than one `ConnectionInit` message closes the socket immediately ([757c6e9](757c6e9))
* **server:** Respect completed subscriptions even if `subscribe` or `onOperation` didnt resolve yet ([4700154](4700154))
* **server:** Return ping's payload through the response pong ([47730a9](47730a9)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **server:** scoped execution result formatter from `onConnect` ([f91fadb](f91fadb))
* **server:** Should clean up subscription reservations on abrupt errors without relying on connection close ([611c223](611c223))
* **server:** Shouldn't send a complete message if client sent it ([331fe47](331fe47)), closes [enisdenjo#403](https://github.com/ilijaNL/graphql-ws/issues/403)
* **server:** store the intial request in the context ([6927ee0](6927ee0))
* **server:** Use `subscribe` from the config ([6fbd47c](6fbd47c))
* **server:** use subscription specific formatter for queries and mutations too ([5672a04](5672a04))
* Sink's next callback always receives an `ExecutionResult` ([045b402](045b402))
* Stop sending messages after receiving complete ([enisdenjo#65](https://github.com/ilijaNL/graphql-ws/issues/65)) ([3f4f836](3f4f836))
* Support more `graphql` versions ([de69b4e](de69b4e))
* Support more Node versions by not using `globalThis` ([79c2ed2](79c2ed2))
* Use `4406` close code for unsupported subprotocol (`1002` is an internal WebSocket close code) ([df85281](df85281))
* Use `4500` close code for internal server errors (`1011` is an internal WebSocket close code) ([3c0316d](3c0316d))
* Use `ID` type for message id field ([87ebd35](87ebd35))
* **uWebSockets:** Handle premature and abrupt socket closes ([9d3ff52](9d3ff52)), closes [enisdenjo#186](https://github.com/ilijaNL/graphql-ws/issues/186)
* Warn about subscriptions-transport-ws clients and provide migration link ([e080739](e080739)), closes [enisdenjo#339](https://github.com/ilijaNL/graphql-ws/issues/339) [enisdenjo#325](https://github.com/ilijaNL/graphql-ws/issues/325)
* **ws,fastify-websocket:** Send only on ready socket ([8d13c9e](8d13c9e))
* **ws,uWebSockets,@fastify/websocket:** Handle internal errors that are not instances of `Error` ([enisdenjo#442](https://github.com/ilijaNL/graphql-ws/issues/442)) ([9884889](9884889)), closes [enisdenjo#441](https://github.com/ilijaNL/graphql-ws/issues/441)
* **ws:** Handle socket emitted errors ([a22c00f](a22c00f))
* **ws:** Limit server emitted error close message size ([50620df](50620df))
* **ws:** Log server emitted errors to the console ([0826b0a](0826b0a))
* yarn engine is not required ([enisdenjo#34](https://github.com/ilijaNL/graphql-ws/issues/34)) ([89484b8](89484b8))

### Features

* `cjs`, `esm` and `umd` builds with minification and compression for the browser ([enisdenjo#58](https://github.com/ilijaNL/graphql-ws/issues/58)) ([ebb8dfe](ebb8dfe))
* Add `extensions` field to the subscribe message payload ([d86a8e4](d86a8e4))
* Allow null payloads in messages ([enisdenjo#456](https://github.com/ilijaNL/graphql-ws/issues/456)) ([eeb0265](eeb0265)), closes [enisdenjo#455](https://github.com/ilijaNL/graphql-ws/issues/455)
* Bidirectional ping/pong message types ([enisdenjo#201](https://github.com/ilijaNL/graphql-ws/issues/201)) ([1efaf83](1efaf83))
* Centralise expected close codes in `CloseCode` enum ([d10a75c](d10a75c))
* **client:** `connectionParams` can return `undefined` ([a543187](a543187))
* **client:** `connectionParams` may return a promise ([enisdenjo#71](https://github.com/ilijaNL/graphql-ws/issues/71)) ([33f210c](33f210c))
* **client:** `disablePong` option for when implementing a custom pinger ([6510360](6510360)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **client:** `isFatalConnectionProblem` option for deciding if the connect error should be immediately reported or the connection retried ([enisdenjo#126](https://github.com/ilijaNL/graphql-ws/issues/126)) ([8115871](8115871)), closes [enisdenjo#122](https://github.com/ilijaNL/graphql-ws/issues/122)
* **client:** `onNonLazyError` allows you to catch errors reported in non-lazy mode ([cd1e7df](cd1e7df))
* **client:** `url` option accepts a function or a Promise ([enisdenjo#143](https://github.com/ilijaNL/graphql-ws/issues/143)) ([76f522f](76f522f)), closes [enisdenjo#145](https://github.com/ilijaNL/graphql-ws/issues/145) [enisdenjo#146](https://github.com/ilijaNL/graphql-ws/issues/146)
* **client:** Add `connectionAckWaitTimeout` option ([enisdenjo#228](https://github.com/ilijaNL/graphql-ws/issues/228)) ([35ce054](35ce054))
* **client:** Add `opened` event for when a WebSocket opens ([9053224](9053224))
* **client:** Allow keeping the connection alive for some time before lazy closing ([enisdenjo#69](https://github.com/ilijaNL/graphql-ws/issues/69)) ([555c2c3](555c2c3))
* **client:** Deprecate `isFatalConnectionProblem` option in favour of `shouldRetry` ([d8dcf21](d8dcf21))
* **client:** Emit events for `connecting`, `connected` and `closed` ([627775b](627775b))
* **client:** Implement silent-reconnects ([c6f7872](c6f7872)), closes [enisdenjo#7](https://github.com/ilijaNL/graphql-ws/issues/7)
* **client:** introduce Socky 🧦 - the nifty internal socket state manager ([enisdenjo#8](https://github.com/ilijaNL/graphql-ws/issues/8)) ([a4bee6f](a4bee6f))
* **client:** Lazy option can be changed ([fb0ec14](fb0ec14))
* **client:** Optional `generateID` to provide subscription IDs ([enisdenjo#22](https://github.com/ilijaNL/graphql-ws/issues/22)) ([9a3f54a](9a3f54a)), closes [enisdenjo#21](https://github.com/ilijaNL/graphql-ws/issues/21)
* **client:** Provide subscribe payload in `generateID` ([d0bc6e1](d0bc6e1)), closes [enisdenjo#398](https://github.com/ilijaNL/graphql-ws/issues/398)
* **client:** Re-implement following the new transport protocol ([#6](#6)) ([5191a35](5191a35))
* **client:** Rename `keepAlive` option to `lazyCloseTimeout` ([3c1f13c](3c1f13c))
* **client:** Retry with randomised exponential backoff or provide your own strategy ([enisdenjo#84](https://github.com/ilijaNL/graphql-ws/issues/84)) ([d3e7a17](d3e7a17))
* **client:** Support providing custom WebSocket implementations ([enisdenjo#18](https://github.com/ilijaNL/graphql-ws/issues/18)) ([1515fe2](1515fe2))
* **client:** Terminate the WebSocket abruptly and immediately ([53ad515](53ad515)), closes [enisdenjo#290](https://github.com/ilijaNL/graphql-ws/issues/290)
* Descriptive invalid message errors ([b46379e](b46379e)), closes [enisdenjo#366](https://github.com/ilijaNL/graphql-ws/issues/366)
* Optional `payload` for ping/pong message types ([2fe0345](2fe0345)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* Package ECMAScript Modules too ([enisdenjo#87](https://github.com/ilijaNL/graphql-ws/issues/87)) ([2108174](2108174))
* Package rename `@enisdenjo/graphql-transport-ws` 👉 `graphql-transport-ws`. ([494f676](494f676))
* Rewrite GraphQL over WebSocket Protocol ([#2](#2)) ([42045c5](42045c5))
* Send optional payload with the `ConnectionAck` message ([enisdenjo#60](https://github.com/ilijaNL/graphql-ws/issues/60)) ([1327e77](1327e77))
* **server:** `context` may return a promise ([cd5c2f8](cd5c2f8)), closes [enisdenjo#74](https://github.com/ilijaNL/graphql-ws/issues/74)
* **server:** `execute` and `subscribe` are optional ([enisdenjo#148](https://github.com/ilijaNL/graphql-ws/issues/148)) ([af748b0](af748b0))
* **server:** Add `onClose` callback for closures at _any_ point in time ([dd0d4fa](dd0d4fa))
* **server:** Add `onDisconnect` callback ([enisdenjo#94](https://github.com/ilijaNL/graphql-ws/issues/94)) ([2a61268](2a61268))
* **server:** Add support for `ws@v8` ([9119153](9119153))
* **server:** Define execution/subscription `context` in creation options ([5b3d253](5b3d253)), closes [enisdenjo#13](https://github.com/ilijaNL/graphql-ws/issues/13)
* **server:** Dynamic `schema` support by accepting a function or a Promise ([enisdenjo#147](https://github.com/ilijaNL/graphql-ws/issues/147)) ([6a0bf94](6a0bf94)), closes [enisdenjo#127](https://github.com/ilijaNL/graphql-ws/issues/127)
* **server:** For dynamic usage, `context` option can be a function too ([enisdenjo#46](https://github.com/ilijaNL/graphql-ws/issues/46)) ([149b582](149b582))
* **server:** Implement following the new transport protocol ([#1](#1)) ([a412d25](a412d25))
* **server:** Log a warning for unsupported subprotocols ([88a12ef](88a12ef)), closes [enisdenjo#92](https://github.com/ilijaNL/graphql-ws/issues/92)
* **server:** Make and use with your own flavour ([enisdenjo#64](https://github.com/ilijaNL/graphql-ws/issues/64)) ([38bde87](38bde87)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61) [enisdenjo#73](https://github.com/ilijaNL/graphql-ws/issues/73) [enisdenjo#75](https://github.com/ilijaNL/graphql-ws/issues/75)
* **server:** More callbacks, clearer differences and higher extensibility ([enisdenjo#40](https://github.com/ilijaNL/graphql-ws/issues/40)) ([507a222](507a222))
* **server:** Optional `onPing` and `onPong` message type listeners ([f36066f](f36066f))
* **server:** Pass roots for operation fields as an option ([dcb5ed4](dcb5ed4))
* **server:** Support returning multiple results from `execute` ([enisdenjo#28](https://github.com/ilijaNL/graphql-ws/issues/28)) ([dbbd88b](dbbd88b))
* **server:** Use `@fastify/websocket` ([enisdenjo#382](https://github.com/ilijaNL/graphql-ws/issues/382)) ([dd755b0](dd755b0)), closes [enisdenjo#381](https://github.com/ilijaNL/graphql-ws/issues/381)
* **server:** Use `fastify-websocket` ([enisdenjo#200](https://github.com/ilijaNL/graphql-ws/issues/200)) ([b62fc95](b62fc95))
* **server:** Use `validate` option for custom GraphQL validation ([b68d56c](b68d56c))
* **server:** Use uWebSockets ([enisdenjo#89](https://github.com/ilijaNL/graphql-ws/issues/89)) ([45d08fc](45d08fc)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61)
* Subscribe message `query` must be a string ([enisdenjo#45](https://github.com/ilijaNL/graphql-ws/issues/45)) ([60d9cd5](60d9cd5))
* Support custom JSON message `reviver` and `replacer` ([enisdenjo#172](https://github.com/ilijaNL/graphql-ws/issues/172)) ([0a9894e](0a9894e))
* TypeScript generic for connection init payload (`connectionParams`) ([enisdenjo#311](https://github.com/ilijaNL/graphql-ws/issues/311)) ([e67cf80](e67cf80))
* **use:** Generic for extending the context extras ([401cd4c](401cd4c)), closes [enisdenjo#189](https://github.com/ilijaNL/graphql-ws/issues/189)
* **uWebSockets:** Add `persistedRequest` to context extra and deprecate uWS's stack allocated `request` ([enisdenjo#196](https://github.com/ilijaNL/graphql-ws/issues/196)) ([736e6ed](736e6ed))
* **uWebSockets:** Drop deprecated `request` context extra ([02ea5ee](02ea5ee))
* WebSocket Ping and Pong as keep-alive ([enisdenjo#11](https://github.com/ilijaNL/graphql-ws/issues/11)) ([16ae316](16ae316))

### Performance Improvements

* **client:** Await timeouts only in recursive connects ([55c8fc8](55c8fc8))
* **client:** Focus subscription message listeners on `id` ([enisdenjo#150](https://github.com/ilijaNL/graphql-ws/issues/150)) ([32c2268](32c2268))
* **client:** Memoize message parsing for each subscriber ([2a7ba46](2a7ba46))
* Easier message parser ([d44c6f1](d44c6f1))
* Reduce runtime prototype traversal for hasOwnProperty ([enisdenjo#52](https://github.com/ilijaNL/graphql-ws/issues/52)) ([1bb9218](1bb9218))

### Reverts

* Revert "refactor: emit client connect in next tick during testing" ([c10d0bf](c10d0bf))

### BREAKING CHANGES

* Because of the Protocol's strictness, an instant connection termination will happen whenever an invalid message is identified; meaning, all previous implementations will fail when receiving the new subprotocol ping/pong messages.

**Beware,** the client will NOT ping the server by default. Please make sure to upgrade your stack in order to support the new ping/pong message types.

A simple recipe showcasing a client that times out if no pong is received and measures latency, looks like this:
```js
import { createClient } from 'graphql-ws';

let activeSocket,
  timedOut,
  pingSentAt = 0,
  latency = 0;
createClient({
  url: 'ws://i.time.out:4000/and-measure/latency',
  keepAlive: 10_000, // ping server every 10 seconds
  on: {
    connected: (socket) => (activeSocket = socket),
    ping: (received) => {
      if (!received /* sent */) {
        pingSentAt = Date.now();
        timedOut = setTimeout(() => {
          if (activeSocket.readyState === WebSocket.OPEN)
            activeSocket.close(4408, 'Request Timeout');
        }, 5_000); // wait 5 seconds for the pong and then close the connection
      }
    },
    pong: (received) => {
      if (received) {
        latency = Date.now() - pingSentAt;
        clearTimeout(timedOut); // pong is received, clear connection close timeout
      }
    },
  },
});
```
* **uWebSockets:** The deprecated uWebSockets `request` context extra field has been dropped because it is stack allocated and cannot be used ouside the internal `upgrade` callback.
* **client:** Client `keepAlive` option has been renamed to `lazyCloseTimeout` in order to eliminate ambiguity with the client to server pings keep-alive option.
* **server:** The return function of `server.opened` (`closed`) now requires the close event code and reason for reporting to the `onDisconnect` callback.
* **server:** The `Context.subscriptions` record value can be either an `AsyncIterator` or a `Promise`.
* **client:** Client `retryTimeout` option has been replaced with the new `retryWait`.

`retryWait` allows you to control the retry timeout strategy by resolving the returned promise when ready. The default implements the randomised exponential backoff like so:
```ts
// this is the default
const retryWait = async function randomisedExponentialBackoff(retries: number) {
  let retryDelay = 1000; // start with 1s delay
  for (let i = 0; i < retries; i++) {
    retryDelay *= 2; // square `retries` times
  }
  await new Promise((resolve) =>
    setTimeout(
      // resolve pending promise with added random timeout from 300ms to 3s
      resolve,
      retryDelay + Math.floor(Math.random() * (3000 - 300) + 300),
    ),
  );
};
```
* **server:** You now "make" a ready-to-use server that can be used with _any_ WebSocket implementation!

Summary of breaking changes:
- No more `keepAlive`. The user should provide its own keep-alive implementation. _(I highly recommend [WebSocket Ping and Pongs](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets))_
- No more HTTP `request` in the server context.
- No more WebSocket in the server context (you're the one that creates it).
- You use your own WebSocket server
- Server exports only `makeServer` _(no more `createServer`)_

### Benefits
- You're responsible for the server (_any_ optimisation or adjustment can be applied)
- Any WebSocket server can be used (or even mocked if necessary)
- You control the disposal of the server (close or transfer clients however you wish)
- New `extra` field in the `Context` for storing custom values useful for callbacks
- Full control of authentication flow
- Full control over error handling
- True zero-dependency

### Migrating from v1

**Only the server has to be migrated.** Since this release allows you to use your favourite WebSocket library (or your own implementation), using [ws](https://github.com/websockets/ws) is just one way of using `graphql-ws`. This is how to use the implementation shipped with the lib:

```ts
/**
 * ❌ instead of the lib creating a WebSocket server internally with the provided arguments
 */
import https from 'https';
import { createServer } from 'graphql-ws';

const server = https.createServer(...);

createServer(
  {
    onConnect(ctx) {
      // were previously directly on the context
      ctx.request as IncomingRequest
      ctx.socket as WebSocket
    },
    ...rest,
  },
  {
    server,
    path: '/graphql',
  },
);

/**
 * ✅ you have to supply the server yourself
 */
import https from 'https';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws'; // notice the import path

const server = https.createServer(...);
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});

useServer(
  {
    onConnect(ctx) {
      // are now in the `extra` field
      ctx.extra.request as IncomingRequest
      ctx.extra.socket as WebSocket
    },
    ...rest,
  },
  wsServer,
  // optional keepAlive with ping pongs (defaults to 12 seconds)
);
```
* This lib is no longer compatible with [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws). It follows a redesigned transport protocol aiming to improve security, stability and reduce ambiguity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
released Has been released and published
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant