Skip to content

Commit

Permalink
feat(server): Make and use with your own flavour (enisdenjo#64)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: 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)
);
```


Closes: enisdenjo#61, closes: enisdenjo#73, closes: enisdenjo#75
  • Loading branch information
enisdenjo committed Nov 20, 2020
1 parent af2d95d commit 38bde87
Show file tree
Hide file tree
Showing 20 changed files with 1,099 additions and 823 deletions.
196 changes: 133 additions & 63 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/README.md
Expand Up @@ -13,3 +13,4 @@
* ["protocol"](modules/_protocol_.md)
* ["server"](modules/_server_.md)
* ["types"](modules/_types_.md)
* ["use/ws"](modules/_use_ws_.md)
35 changes: 16 additions & 19 deletions docs/interfaces/_server_.context.md
Expand Up @@ -2,7 +2,13 @@

> [Globals](../README.md) / ["server"](../modules/_server_.md) / Context
# Interface: Context
# Interface: Context\<E>

## Type parameters

Name | Default |
------ | ------ |
`E` | unknown |

## Hierarchy

Expand All @@ -15,15 +21,14 @@
* [acknowledged](_server_.context.md#acknowledged)
* [connectionInitReceived](_server_.context.md#connectioninitreceived)
* [connectionParams](_server_.context.md#connectionparams)
* [request](_server_.context.md#request)
* [socket](_server_.context.md#socket)
* [extra](_server_.context.md#extra)
* [subscriptions](_server_.context.md#subscriptions)

## Properties

### acknowledged

**acknowledged**: boolean
`Readonly` **acknowledged**: boolean

Indicates that the connection was acknowledged
by having dispatched the `ConnectionAck` message
Expand All @@ -33,7 +38,7 @@ ___

### connectionInitReceived

**connectionInitReceived**: boolean
`Readonly` **connectionInitReceived**: boolean

Indicates that the `ConnectionInit` message
has been received by the server. If this is
Expand All @@ -44,32 +49,24 @@ ___

### connectionParams

`Optional` **connectionParams**: Readonly\<Record\<string, unknown>>
`Optional` `Readonly` **connectionParams**: Readonly\<Record\<string, unknown>>

The parameters passed during the connection initialisation.

___

### request

`Readonly` **request**: IncomingMessage

The initial HTTP request before the actual
socket and connection is established.

___

### socket
### extra

`Readonly` **socket**: WebSocket
**extra**: E

The actual WebSocket connection between the server and the client.
An extra field where you can store your own context values
to pass between callbacks.

___

### subscriptions

**subscriptions**: Record\<[ID](../modules/_types_.md#id), AsyncIterator\<unknown>>
`Readonly` **subscriptions**: Record\<[ID](../modules/_types_.md#id), AsyncIterator\<unknown>>

Holds the active subscriptions for this context.
Subscriptions are for **streaming operations only**,
Expand Down
44 changes: 29 additions & 15 deletions docs/interfaces/_server_.server.md
Expand Up @@ -2,33 +2,47 @@

> [Globals](../README.md) / ["server"](../modules/_server_.md) / Server
# Interface: Server
# Interface: Server\<E>

## Hierarchy
## Type parameters

Name | Default |
------ | ------ |
`E` | undefined |

* [Disposable](_types_.disposable.md)
## Hierarchy

**Server**
* **Server**

## Index

### Properties
### Methods

* [opened](_server_.server.md#opened)

* [dispose](_server_.server.md#dispose)
* [webSocketServer](_server_.server.md#websocketserver)
## Methods

## Properties
### opened

### dispose
**opened**(`socket`: [WebSocket](_server_.websocket.md), `ctxExtra`: E): function

**dispose**: () => void \| Promise\<void>
New socket has beeen established. The lib will validate
the protocol and use the socket accordingly. Returned promise
will resolve after the socket closes.

*Inherited from [Disposable](_types_.disposable.md).[dispose](_types_.disposable.md#dispose)*
The second argument will be passed in the `extra` field
of the `Context`. You may pass the initial request or the
original WebSocket, if you need it down the road.

Dispose of the instance and clear up resources.
Returns a function that should be called when the same socket
has been closed, for whatever reason. The returned promise will
resolve once the internal cleanup is complete.

___
#### Parameters:

### webSocketServer
Name | Type |
------ | ------ |
`socket` | [WebSocket](_server_.websocket.md) |
`ctxExtra` | E |

**webSocketServer**: Server
**Returns:** function
37 changes: 14 additions & 23 deletions docs/interfaces/_server_.serveroptions.md
Expand Up @@ -2,7 +2,13 @@

> [Globals](../README.md) / ["server"](../modules/_server_.md) / ServerOptions
# Interface: ServerOptions
# Interface: ServerOptions\<E>

## Type parameters

Name | Default |
------ | ------ |
`E` | unknown |

## Hierarchy

Expand All @@ -15,7 +21,6 @@
* [connectionInitWaitTimeout](_server_.serveroptions.md#connectioninitwaittimeout)
* [context](_server_.serveroptions.md#context)
* [execute](_server_.serveroptions.md#execute)
* [keepAlive](_server_.serveroptions.md#keepalive)
* [onComplete](_server_.serveroptions.md#oncomplete)
* [onConnect](_server_.serveroptions.md#onconnect)
* [onError](_server_.serveroptions.md#onerror)
Expand Down Expand Up @@ -48,7 +53,7 @@ ___

### context

`Optional` **context**: [GraphQLExecutionContextValue](../modules/_server_.md#graphqlexecutioncontextvalue) \| (ctx: [Context](_server_.context.md), message: [SubscribeMessage](_message_.subscribemessage.md), args: ExecutionArgs) => [GraphQLExecutionContextValue](../modules/_server_.md#graphqlexecutioncontextvalue)
`Optional` **context**: [GraphQLExecutionContextValue](../modules/_server_.md#graphqlexecutioncontextvalue) \| (ctx: [Context](_server_.context.md)\<E>, message: [SubscribeMessage](_message_.subscribemessage.md), args: ExecutionArgs) => Promise\<[GraphQLExecutionContextValue](../modules/_server_.md#graphqlexecutioncontextvalue)> \| [GraphQLExecutionContextValue](../modules/_server_.md#graphqlexecutioncontextvalue)

A value which is provided to every resolver and holds
important contextual information like the currently
Expand Down Expand Up @@ -78,23 +83,9 @@ in the close event reason.

___

### keepAlive

`Optional` **keepAlive**: undefined \| number

The timout between dispatched keep-alive messages. Internally the lib
uses the [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)) to check that the link between
the clients and the server is operating and to prevent the link from being broken due to idling.

Set to nullish value to disable.

**`default`** 12 * 1000 (12 seconds)

___

### onComplete

`Optional` **onComplete**: undefined \| (ctx: [Context](_server_.context.md), message: [CompleteMessage](_message_.completemessage.md)) => Promise\<void> \| void
`Optional` **onComplete**: undefined \| (ctx: [Context](_server_.context.md)\<E>, message: [CompleteMessage](_message_.completemessage.md)) => Promise\<void> \| void

The complete callback is executed after the
operation has completed right before sending
Expand All @@ -112,7 +103,7 @@ ___

### onConnect

`Optional` **onConnect**: undefined \| (ctx: [Context](_server_.context.md)) => Promise\<Record\<string, unknown> \| boolean \| void> \| Record\<string, unknown> \| boolean \| void
`Optional` **onConnect**: undefined \| (ctx: [Context](_server_.context.md)\<E>) => Promise\<Record\<string, unknown> \| boolean \| void> \| Record\<string, unknown> \| boolean \| void

Is the connection callback called when the
client requests the connection initialisation
Expand Down Expand Up @@ -141,7 +132,7 @@ ___

### onError

`Optional` **onError**: undefined \| (ctx: [Context](_server_.context.md), message: [ErrorMessage](_message_.errormessage.md), errors: readonly GraphQLError[]) => Promise\<readonly GraphQLError[] \| void> \| readonly GraphQLError[] \| void
`Optional` **onError**: undefined \| (ctx: [Context](_server_.context.md)\<E>, message: [ErrorMessage](_message_.errormessage.md), errors: readonly GraphQLError[]) => Promise\<readonly GraphQLError[] \| void> \| readonly GraphQLError[] \| void

Executed after an error occured right before it
has been dispatched to the client.
Expand All @@ -159,7 +150,7 @@ ___

### onNext

`Optional` **onNext**: undefined \| (ctx: [Context](_server_.context.md), message: [NextMessage](_message_.nextmessage.md), args: ExecutionArgs, result: ExecutionResult) => Promise\<ExecutionResult \| void> \| ExecutionResult \| void
`Optional` **onNext**: undefined \| (ctx: [Context](_server_.context.md)\<E>, message: [NextMessage](_message_.nextmessage.md), args: ExecutionArgs, result: ExecutionResult) => Promise\<ExecutionResult \| void> \| ExecutionResult \| void

Executed after an operation has emitted a result right before
that result has been sent to the client. Results from both
Expand All @@ -178,7 +169,7 @@ ___

### onOperation

`Optional` **onOperation**: undefined \| (ctx: [Context](_server_.context.md), message: [SubscribeMessage](_message_.subscribemessage.md), args: ExecutionArgs, result: [OperationResult](../modules/_server_.md#operationresult)) => Promise\<[OperationResult](../modules/_server_.md#operationresult) \| void> \| [OperationResult](../modules/_server_.md#operationresult) \| void
`Optional` **onOperation**: undefined \| (ctx: [Context](_server_.context.md)\<E>, message: [SubscribeMessage](_message_.subscribemessage.md), args: ExecutionArgs, result: [OperationResult](../modules/_server_.md#operationresult)) => Promise\<[OperationResult](../modules/_server_.md#operationresult) \| void> \| [OperationResult](../modules/_server_.md#operationresult) \| void

Executed after the operation call resolves. For streaming
operations, triggering this callback does not necessarely
Expand All @@ -203,7 +194,7 @@ ___

### onSubscribe

`Optional` **onSubscribe**: undefined \| (ctx: [Context](_server_.context.md), message: [SubscribeMessage](_message_.subscribemessage.md)) => Promise\<ExecutionArgs \| readonly GraphQLError[] \| void> \| ExecutionArgs \| readonly GraphQLError[] \| void
`Optional` **onSubscribe**: undefined \| (ctx: [Context](_server_.context.md)\<E>, message: [SubscribeMessage](_message_.subscribemessage.md)) => Promise\<ExecutionArgs \| readonly GraphQLError[] \| void> \| ExecutionArgs \| readonly GraphQLError[] \| void

The subscribe callback executed right after
acknowledging the request before any payload
Expand Down
101 changes: 101 additions & 0 deletions docs/interfaces/_server_.websocket.md
@@ -0,0 +1,101 @@
**[graphql-ws](../README.md)**

> [Globals](../README.md) / ["server"](../modules/_server_.md) / WebSocket
# Interface: WebSocket

## Hierarchy

* **WebSocket**

## Index

### Properties

* [protocol](_server_.websocket.md#protocol)

### Methods

* [close](_server_.websocket.md#close)
* [onMessage](_server_.websocket.md#onmessage)
* [send](_server_.websocket.md#send)

## Properties

### protocol

`Readonly` **protocol**: string

The subprotocol of the WebSocket. Will be used
to validate agains the supported ones.

## Methods

### close

**close**(`code`: number, `reason`: string): Promise\<void> \| void

Closes the socket gracefully. Will always provide
the appropriate code and close reason.

The returned promise is used to control the graceful
closure.

#### Parameters:

Name | Type |
------ | ------ |
`code` | number |
`reason` | string |

**Returns:** Promise\<void> \| void

___

### onMessage

**onMessage**(`cb`: (data: string) => Promise\<void>): void

Called when message is received. The library requires the data
to be a `string`.

All operations requested from the client will block the promise until
completed, this means that the callback will not resolve until all
subscription events have been emittet (or until the client has completed
the stream), or until the query/mutation resolves.

Exceptions raised during any phase of operation processing will
reject the callback's promise, catch them and communicate them
to your clients however you wish.

#### Parameters:

Name | Type |
------ | ------ |
`cb` | (data: string) => Promise\<void> |

**Returns:** void

___

### send

**send**(`data`: string): Promise\<void> \| void

Sends a message through the socket. Will always
provide a `string` message.

Please take care that the send is ready. Meaning,
only provide a truly OPEN socket through the `opened`
method of the `Server`.

The returned promise is used to control the flow of data
(like handling backpressure).

#### Parameters:

Name | Type |
------ | ------ |
`data` | string |

**Returns:** Promise\<void> \| void
2 changes: 0 additions & 2 deletions docs/interfaces/_types_.disposable.md
Expand Up @@ -10,8 +10,6 @@

[Client](_client_.client.md)

[Server](_server_.server.md)

## Index

### Properties
Expand Down
35 changes: 35 additions & 0 deletions docs/interfaces/_use_ws_.extra.md
@@ -0,0 +1,35 @@
**[graphql-ws](../README.md)**

> [Globals](../README.md) / ["use/ws"](../modules/_use_ws_.md) / Extra
# Interface: Extra

The extra that will be put in the `Context`.

## Hierarchy

* **Extra**

## Index

### Properties

* [request](_use_ws_.extra.md#request)
* [socket](_use_ws_.extra.md#socket)

## Properties

### request

`Readonly` **request**: IncomingMessage

The initial HTTP request before the actual
socket and connection is established.

___

### socket

`Readonly` **socket**: WebSocket

The actual socket connection between the server and the client.

0 comments on commit 38bde87

Please sign in to comment.