Initial Sample.

This commit is contained in:
2024-06-03 20:23:50 +05:30
parent ef2b65f673
commit 5269ec3c66
2575 changed files with 282312 additions and 0 deletions

View File

@@ -0,0 +1,407 @@
/**
*
* client
*
*/
import { ExecutionResult } from 'graphql';
import { Sink, ID, Disposable, Message, ConnectionInitMessage, ConnectionAckMessage, PingMessage, PongMessage, SubscribePayload, JSONMessageReviver, JSONMessageReplacer } from './common.mjs';
/** This file is the entry point for browsers, re-export common elements. */
export * from './common.mjs';
/**
* WebSocket started connecting.
*
* @category Client
*/
export type EventConnecting = 'connecting';
/**
* WebSocket has opened.
*
* @category Client
*/
export type EventOpened = 'opened';
/**
* Open WebSocket connection has been acknowledged.
*
* @category Client
*/
export type EventConnected = 'connected';
/**
* `PingMessage` has been received or sent.
*
* @category Client
*/
export type EventPing = 'ping';
/**
* `PongMessage` has been received or sent.
*
* @category Client
*/
export type EventPong = 'pong';
/**
* A message has been received.
*
* @category Client
*/
export type EventMessage = 'message';
/**
* WebSocket connection has closed.
*
* @category Client
*/
export type EventClosed = 'closed';
/**
* WebSocket connection had an error or client had an internal error.
*
* @category Client
*/
export type EventError = 'error';
/**
* All events that could occur.
*
* @category Client
*/
export type Event = EventConnecting | EventOpened | EventConnected | EventPing | EventPong | EventMessage | EventClosed | EventError;
/** @category Client */
export type EventConnectingListener = (isRetry: boolean) => void;
/**
* The first argument is actually the `WebSocket`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* @category Client
*/
export type EventOpenedListener = (socket: unknown) => void;
/**
* The first argument is actually the `WebSocket`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* Also, the second argument is the optional payload that the server may
* send through the `ConnectionAck` message.
*
* @category Client
*/
export type EventConnectedListener = (socket: unknown, payload: ConnectionAckMessage['payload'], wasRetry: boolean) => void;
/**
* The first argument communicates whether the ping was received from the server.
* If `false`, the ping was sent by the client.
*
* @category Client
*/
export type EventPingListener = (received: boolean, payload: PingMessage['payload']) => void;
/**
* The first argument communicates whether the pong was received from the server.
* If `false`, the pong was sent by the client.
*
* @category Client
*/
export type EventPongListener = (received: boolean, payload: PongMessage['payload']) => void;
/**
* Called for all **valid** messages received by the client. Mainly useful for
* debugging and logging received messages.
*
* @category Client
*/
export type EventMessageListener = (message: Message) => void;
/**
* The argument is actually the websocket `CloseEvent`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* @category Client
*/
export type EventClosedListener = (event: unknown) => void;
/**
* Events dispatched from the WebSocket `onerror` are handled in this listener,
* as well as all internal client errors that could throw.
*
* @category Client
*/
export type EventErrorListener = (error: unknown) => void;
/** @category Client */
export type EventListener<E extends Event> = E extends EventConnecting ? EventConnectingListener : E extends EventOpened ? EventOpenedListener : E extends EventConnected ? EventConnectedListener : E extends EventPing ? EventPingListener : E extends EventPong ? EventPongListener : E extends EventMessage ? EventMessageListener : E extends EventClosed ? EventClosedListener : E extends EventError ? EventErrorListener : never;
/**
* Configuration used for the GraphQL over WebSocket client.
*
* @category Client
*/
export interface ClientOptions<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload']> {
/**
* URL of the GraphQL over WebSocket Protocol compliant server to connect.
*
* If the option is a function, it will be called on every WebSocket connection attempt.
* Returning a promise is supported too and the connecting phase will stall until it
* resolves with the URL.
*
* A good use-case for having a function is when using the URL for authentication,
* where subsequent reconnects (due to auth) may have a refreshed identity token in
* the URL.
*/
url: string | (() => Promise<string> | string);
/**
* Optional parameters, passed through the `payload` field with the `ConnectionInit` message,
* that the client specifies when establishing a connection with the server. You can use this
* for securely passing arguments for authentication.
*
* If you decide to return a promise, keep in mind that the server might kick you off if it
* takes too long to resolve! Check the `connectionInitWaitTimeout` on the server for more info.
*
* Throwing an error from within this function will close the socket with the `Error` message
* in the close event reason.
*/
connectionParams?: P | (() => Promise<P> | P);
/**
* Controls when should the connection be established.
*
* - `false`: Establish a connection immediately. Use `onNonLazyError` to handle errors.
* - `true`: Establish a connection on first subscribe and close on last unsubscribe. Use
* the subscription sink's `error` to handle errors.
*
* @default true
*/
lazy?: boolean;
/**
* Used ONLY when the client is in non-lazy mode (`lazy = false`). When
* using this mode, the errors might have no sinks to report to; however,
* to avoid swallowing errors, consider using `onNonLazyError`, which will
* be called when either:
* - An unrecoverable error/close event occurs
* - Silent retry attempts have been exceeded
*
* After a client has errored out, it will NOT perform any automatic actions.
*
* The argument can be a websocket `CloseEvent` or an `Error`. To avoid bundling
* DOM types, you should derive and assert the correct type. When receiving:
* - A `CloseEvent`: retry attempts have been exceeded or the specific
* close event is labeled as fatal (read more in `retryAttempts`).
* - An `Error`: some internal issue has occured, all internal errors are
* fatal by nature.
*
* @default console.error
*/
onNonLazyError?: (errorOrCloseEvent: unknown) => void;
/**
* How long should the client wait before closing the socket after the last oparation has
* completed. This is meant to be used in combination with `lazy`. You might want to have
* a calmdown time before actually closing the connection. Kinda' like a lazy close "debounce".
*
* @default 0
*/
lazyCloseTimeout?: number;
/**
* The timout between dispatched keep-alive messages, naimly server pings. Internally
* dispatches the `PingMessage` type to the server and expects a `PongMessage` in response.
* This helps with making sure that the connection with the server is alive and working.
*
* Timeout countdown starts from the moment the socket was opened and subsequently
* after every received `PongMessage`.
*
* Note that NOTHING will happen automatically with the client if the server never
* responds to a `PingMessage` with a `PongMessage`. If you want the connection to close,
* you should implement your own logic on top of the client. A simple example looks like this:
*
* ```js
* import { createClient } from 'graphql-ws';
*
* let activeSocket, timedOut;
* createClient({
* url: 'ws://i.time.out:4000/after-5/seconds',
* keepAlive: 10_000, // ping server every 10 seconds
* on: {
* connected: (socket) => (activeSocket = socket),
* ping: (received) => {
* if (!received) // sent
* 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) clearTimeout(timedOut); // pong is received, clear connection close timeout
* },
* },
* });
* ```
*
* @default 0
*/
keepAlive?: number;
/**
* The amount of time for which the client will wait
* for `ConnectionAck` message.
*
* Set the value to `Infinity`, `''`, `0`, `null` or `undefined` to skip waiting.
*
* If the wait timeout has passed and the server
* has not responded with `ConnectionAck` message,
* the client will terminate the socket by
* dispatching a close event `4418: Connection acknowledgement timeout`
*
* @default 0
*/
connectionAckWaitTimeout?: number;
/**
* Disable sending the `PongMessage` automatically.
*
* Useful for when integrating your own custom client pinger that performs
* custom actions before responding to a ping, or to pass along the optional pong
* message payload. Please check the readme recipes for a concrete example.
*/
disablePong?: boolean;
/**
* How many times should the client try to reconnect on abnormal socket closure before it errors out?
*
* The library classifies the following close events as fatal:
* - _All internal WebSocket fatal close codes (check `isFatalInternalCloseCode` in `src/client.ts` for exact list)_
* - `4500: Internal server error`
* - `4005: Internal client error`
* - `4400: Bad request`
* - `4004: Bad response`
* - `4401: Unauthorized` _tried subscribing before connect ack_
* - `4406: Subprotocol not acceptable`
* - `4409: Subscriber for <id> already exists` _distinction is very important_
* - `4429: Too many initialisation requests`
*
* In addition to the aforementioned close events, any _non-CloseEvent_ connection problem
* is considered fatal by default. However, this specific behaviour can be altered by using
* the `shouldRetry` option.
*
* These events are reported immediately and the client will not reconnect.
*
* @default 5
*/
retryAttempts?: number;
/**
* Control the wait time between retries. You may implement your own strategy
* by timing the resolution of the returned promise with the retries count.
* `retries` argument counts actual connection attempts, so it will begin with
* 0 after the first retryable disconnect.
*
* @default 'Randomised exponential backoff'
*/
retryWait?: (retries: number) => Promise<void>;
/**
* Check if the close event or connection error is fatal. If you return `false`,
* the client will fail immediately without additional retries; however, if you
* return `true`, the client will keep retrying until the `retryAttempts` have
* been exceeded.
*
* The argument is whatever has been thrown during the connection phase.
*
* Beware, the library classifies a few close events as fatal regardless of
* what is returned here. They are listed in the documentation of the `retryAttempts`
* option.
*
* @default 'Only `CloseEvent`s'
*/
shouldRetry?: (errOrCloseEvent: unknown) => boolean;
/**
* @deprecated Use `shouldRetry` instead.
*
* Check if the close event or connection error is fatal. If you return `true`,
* the client will fail immediately without additional retries; however, if you
* return `false`, the client will keep retrying until the `retryAttempts` have
* been exceeded.
*
* The argument is either a WebSocket `CloseEvent` or an error thrown during
* the connection phase.
*
* Beware, the library classifies a few close events as fatal regardless of
* what is returned. They are listed in the documentation of the `retryAttempts`
* option.
*
* @default 'Any non-`CloseEvent`'
*/
isFatalConnectionProblem?: (errOrCloseEvent: unknown) => boolean;
/**
* Register listeners before initialising the client. This way
* you can ensure to catch all client relevant emitted events.
*
* The listeners passed in will **always** be the first ones
* to get the emitted event before other registered listeners.
*/
on?: Partial<{
[event in Event]: EventListener<event>;
}>;
/**
* A custom WebSocket implementation to use instead of the
* one provided by the global scope. Mostly useful for when
* using the client outside of the browser environment.
*/
webSocketImpl?: unknown;
/**
* A custom ID generator for identifying subscriptions.
*
* The default generates a v4 UUID to be used as the ID using `Math`
* as the random number generator. Supply your own generator
* in case you need more uniqueness.
*
* Reference: https://gist.github.com/jed/982883
*/
generateID?: (payload: SubscribePayload) => ID;
/**
* An optional override for the JSON.parse function used to hydrate
* incoming messages to this client. Useful for parsing custom datatypes
* out of the incoming JSON.
*/
jsonMessageReviver?: JSONMessageReviver;
/**
* An optional override for the JSON.stringify function used to serialize
* outgoing messages from this client. Useful for serializing custom
* datatypes out to the client.
*/
jsonMessageReplacer?: JSONMessageReplacer;
}
/** @category Client */
export interface Client extends Disposable {
/**
* Listens on the client which dispatches events about the socket state.
*/
on<E extends Event>(event: E, listener: EventListener<E>): () => void;
/**
* Subscribes through the WebSocket following the config parameters. It
* uses the `sink` to emit received data or errors. Returns a _cleanup_
* function used for dropping the subscription and cleaning stuff up.
*/
subscribe<Data = Record<string, unknown>, Extensions = unknown>(payload: SubscribePayload, sink: Sink<ExecutionResult<Data, Extensions>>): () => void;
/**
* Subscribes and iterates over emitted results from the WebSocket
* through the returned async iterator.
*/
iterate<Data = Record<string, unknown>, Extensions = unknown>(payload: SubscribePayload): AsyncIterableIterator<ExecutionResult<Data, Extensions>>;
/**
* Terminates the WebSocket abruptly and immediately.
*
* A close event `4499: Terminated` is issued to the current WebSocket and a
* syntetic {@link TerminatedCloseEvent} is immediately emitted without waiting for
* the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
terminate(): void;
}
/**
* Creates a disposable GraphQL over WebSocket client.
*
* @category Client
*/
export declare function createClient<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload']>(options: ClientOptions<P>): Client;
/**
* A syntetic close event `4499: Terminated` is issued to the current to immediately
* close the connection without waiting for the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
export declare class TerminatedCloseEvent extends Error {
name: string;
message: string;
code: number;
reason: string;
wasClean: boolean;
}

View File

@@ -0,0 +1,407 @@
/**
*
* client
*
*/
import { ExecutionResult } from 'graphql';
import { Sink, ID, Disposable, Message, ConnectionInitMessage, ConnectionAckMessage, PingMessage, PongMessage, SubscribePayload, JSONMessageReviver, JSONMessageReplacer } from './common';
/** This file is the entry point for browsers, re-export common elements. */
export * from './common';
/**
* WebSocket started connecting.
*
* @category Client
*/
export type EventConnecting = 'connecting';
/**
* WebSocket has opened.
*
* @category Client
*/
export type EventOpened = 'opened';
/**
* Open WebSocket connection has been acknowledged.
*
* @category Client
*/
export type EventConnected = 'connected';
/**
* `PingMessage` has been received or sent.
*
* @category Client
*/
export type EventPing = 'ping';
/**
* `PongMessage` has been received or sent.
*
* @category Client
*/
export type EventPong = 'pong';
/**
* A message has been received.
*
* @category Client
*/
export type EventMessage = 'message';
/**
* WebSocket connection has closed.
*
* @category Client
*/
export type EventClosed = 'closed';
/**
* WebSocket connection had an error or client had an internal error.
*
* @category Client
*/
export type EventError = 'error';
/**
* All events that could occur.
*
* @category Client
*/
export type Event = EventConnecting | EventOpened | EventConnected | EventPing | EventPong | EventMessage | EventClosed | EventError;
/** @category Client */
export type EventConnectingListener = (isRetry: boolean) => void;
/**
* The first argument is actually the `WebSocket`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* @category Client
*/
export type EventOpenedListener = (socket: unknown) => void;
/**
* The first argument is actually the `WebSocket`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* Also, the second argument is the optional payload that the server may
* send through the `ConnectionAck` message.
*
* @category Client
*/
export type EventConnectedListener = (socket: unknown, payload: ConnectionAckMessage['payload'], wasRetry: boolean) => void;
/**
* The first argument communicates whether the ping was received from the server.
* If `false`, the ping was sent by the client.
*
* @category Client
*/
export type EventPingListener = (received: boolean, payload: PingMessage['payload']) => void;
/**
* The first argument communicates whether the pong was received from the server.
* If `false`, the pong was sent by the client.
*
* @category Client
*/
export type EventPongListener = (received: boolean, payload: PongMessage['payload']) => void;
/**
* Called for all **valid** messages received by the client. Mainly useful for
* debugging and logging received messages.
*
* @category Client
*/
export type EventMessageListener = (message: Message) => void;
/**
* The argument is actually the websocket `CloseEvent`, but to avoid
* bundling DOM typings because the client can run in Node env too,
* you should assert the websocket type during implementation.
*
* @category Client
*/
export type EventClosedListener = (event: unknown) => void;
/**
* Events dispatched from the WebSocket `onerror` are handled in this listener,
* as well as all internal client errors that could throw.
*
* @category Client
*/
export type EventErrorListener = (error: unknown) => void;
/** @category Client */
export type EventListener<E extends Event> = E extends EventConnecting ? EventConnectingListener : E extends EventOpened ? EventOpenedListener : E extends EventConnected ? EventConnectedListener : E extends EventPing ? EventPingListener : E extends EventPong ? EventPongListener : E extends EventMessage ? EventMessageListener : E extends EventClosed ? EventClosedListener : E extends EventError ? EventErrorListener : never;
/**
* Configuration used for the GraphQL over WebSocket client.
*
* @category Client
*/
export interface ClientOptions<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload']> {
/**
* URL of the GraphQL over WebSocket Protocol compliant server to connect.
*
* If the option is a function, it will be called on every WebSocket connection attempt.
* Returning a promise is supported too and the connecting phase will stall until it
* resolves with the URL.
*
* A good use-case for having a function is when using the URL for authentication,
* where subsequent reconnects (due to auth) may have a refreshed identity token in
* the URL.
*/
url: string | (() => Promise<string> | string);
/**
* Optional parameters, passed through the `payload` field with the `ConnectionInit` message,
* that the client specifies when establishing a connection with the server. You can use this
* for securely passing arguments for authentication.
*
* If you decide to return a promise, keep in mind that the server might kick you off if it
* takes too long to resolve! Check the `connectionInitWaitTimeout` on the server for more info.
*
* Throwing an error from within this function will close the socket with the `Error` message
* in the close event reason.
*/
connectionParams?: P | (() => Promise<P> | P);
/**
* Controls when should the connection be established.
*
* - `false`: Establish a connection immediately. Use `onNonLazyError` to handle errors.
* - `true`: Establish a connection on first subscribe and close on last unsubscribe. Use
* the subscription sink's `error` to handle errors.
*
* @default true
*/
lazy?: boolean;
/**
* Used ONLY when the client is in non-lazy mode (`lazy = false`). When
* using this mode, the errors might have no sinks to report to; however,
* to avoid swallowing errors, consider using `onNonLazyError`, which will
* be called when either:
* - An unrecoverable error/close event occurs
* - Silent retry attempts have been exceeded
*
* After a client has errored out, it will NOT perform any automatic actions.
*
* The argument can be a websocket `CloseEvent` or an `Error`. To avoid bundling
* DOM types, you should derive and assert the correct type. When receiving:
* - A `CloseEvent`: retry attempts have been exceeded or the specific
* close event is labeled as fatal (read more in `retryAttempts`).
* - An `Error`: some internal issue has occured, all internal errors are
* fatal by nature.
*
* @default console.error
*/
onNonLazyError?: (errorOrCloseEvent: unknown) => void;
/**
* How long should the client wait before closing the socket after the last oparation has
* completed. This is meant to be used in combination with `lazy`. You might want to have
* a calmdown time before actually closing the connection. Kinda' like a lazy close "debounce".
*
* @default 0
*/
lazyCloseTimeout?: number;
/**
* The timout between dispatched keep-alive messages, naimly server pings. Internally
* dispatches the `PingMessage` type to the server and expects a `PongMessage` in response.
* This helps with making sure that the connection with the server is alive and working.
*
* Timeout countdown starts from the moment the socket was opened and subsequently
* after every received `PongMessage`.
*
* Note that NOTHING will happen automatically with the client if the server never
* responds to a `PingMessage` with a `PongMessage`. If you want the connection to close,
* you should implement your own logic on top of the client. A simple example looks like this:
*
* ```js
* import { createClient } from 'graphql-ws';
*
* let activeSocket, timedOut;
* createClient({
* url: 'ws://i.time.out:4000/after-5/seconds',
* keepAlive: 10_000, // ping server every 10 seconds
* on: {
* connected: (socket) => (activeSocket = socket),
* ping: (received) => {
* if (!received) // sent
* 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) clearTimeout(timedOut); // pong is received, clear connection close timeout
* },
* },
* });
* ```
*
* @default 0
*/
keepAlive?: number;
/**
* The amount of time for which the client will wait
* for `ConnectionAck` message.
*
* Set the value to `Infinity`, `''`, `0`, `null` or `undefined` to skip waiting.
*
* If the wait timeout has passed and the server
* has not responded with `ConnectionAck` message,
* the client will terminate the socket by
* dispatching a close event `4418: Connection acknowledgement timeout`
*
* @default 0
*/
connectionAckWaitTimeout?: number;
/**
* Disable sending the `PongMessage` automatically.
*
* Useful for when integrating your own custom client pinger that performs
* custom actions before responding to a ping, or to pass along the optional pong
* message payload. Please check the readme recipes for a concrete example.
*/
disablePong?: boolean;
/**
* How many times should the client try to reconnect on abnormal socket closure before it errors out?
*
* The library classifies the following close events as fatal:
* - _All internal WebSocket fatal close codes (check `isFatalInternalCloseCode` in `src/client.ts` for exact list)_
* - `4500: Internal server error`
* - `4005: Internal client error`
* - `4400: Bad request`
* - `4004: Bad response`
* - `4401: Unauthorized` _tried subscribing before connect ack_
* - `4406: Subprotocol not acceptable`
* - `4409: Subscriber for <id> already exists` _distinction is very important_
* - `4429: Too many initialisation requests`
*
* In addition to the aforementioned close events, any _non-CloseEvent_ connection problem
* is considered fatal by default. However, this specific behaviour can be altered by using
* the `shouldRetry` option.
*
* These events are reported immediately and the client will not reconnect.
*
* @default 5
*/
retryAttempts?: number;
/**
* Control the wait time between retries. You may implement your own strategy
* by timing the resolution of the returned promise with the retries count.
* `retries` argument counts actual connection attempts, so it will begin with
* 0 after the first retryable disconnect.
*
* @default 'Randomised exponential backoff'
*/
retryWait?: (retries: number) => Promise<void>;
/**
* Check if the close event or connection error is fatal. If you return `false`,
* the client will fail immediately without additional retries; however, if you
* return `true`, the client will keep retrying until the `retryAttempts` have
* been exceeded.
*
* The argument is whatever has been thrown during the connection phase.
*
* Beware, the library classifies a few close events as fatal regardless of
* what is returned here. They are listed in the documentation of the `retryAttempts`
* option.
*
* @default 'Only `CloseEvent`s'
*/
shouldRetry?: (errOrCloseEvent: unknown) => boolean;
/**
* @deprecated Use `shouldRetry` instead.
*
* Check if the close event or connection error is fatal. If you return `true`,
* the client will fail immediately without additional retries; however, if you
* return `false`, the client will keep retrying until the `retryAttempts` have
* been exceeded.
*
* The argument is either a WebSocket `CloseEvent` or an error thrown during
* the connection phase.
*
* Beware, the library classifies a few close events as fatal regardless of
* what is returned. They are listed in the documentation of the `retryAttempts`
* option.
*
* @default 'Any non-`CloseEvent`'
*/
isFatalConnectionProblem?: (errOrCloseEvent: unknown) => boolean;
/**
* Register listeners before initialising the client. This way
* you can ensure to catch all client relevant emitted events.
*
* The listeners passed in will **always** be the first ones
* to get the emitted event before other registered listeners.
*/
on?: Partial<{
[event in Event]: EventListener<event>;
}>;
/**
* A custom WebSocket implementation to use instead of the
* one provided by the global scope. Mostly useful for when
* using the client outside of the browser environment.
*/
webSocketImpl?: unknown;
/**
* A custom ID generator for identifying subscriptions.
*
* The default generates a v4 UUID to be used as the ID using `Math`
* as the random number generator. Supply your own generator
* in case you need more uniqueness.
*
* Reference: https://gist.github.com/jed/982883
*/
generateID?: (payload: SubscribePayload) => ID;
/**
* An optional override for the JSON.parse function used to hydrate
* incoming messages to this client. Useful for parsing custom datatypes
* out of the incoming JSON.
*/
jsonMessageReviver?: JSONMessageReviver;
/**
* An optional override for the JSON.stringify function used to serialize
* outgoing messages from this client. Useful for serializing custom
* datatypes out to the client.
*/
jsonMessageReplacer?: JSONMessageReplacer;
}
/** @category Client */
export interface Client extends Disposable {
/**
* Listens on the client which dispatches events about the socket state.
*/
on<E extends Event>(event: E, listener: EventListener<E>): () => void;
/**
* Subscribes through the WebSocket following the config parameters. It
* uses the `sink` to emit received data or errors. Returns a _cleanup_
* function used for dropping the subscription and cleaning stuff up.
*/
subscribe<Data = Record<string, unknown>, Extensions = unknown>(payload: SubscribePayload, sink: Sink<ExecutionResult<Data, Extensions>>): () => void;
/**
* Subscribes and iterates over emitted results from the WebSocket
* through the returned async iterator.
*/
iterate<Data = Record<string, unknown>, Extensions = unknown>(payload: SubscribePayload): AsyncIterableIterator<ExecutionResult<Data, Extensions>>;
/**
* Terminates the WebSocket abruptly and immediately.
*
* A close event `4499: Terminated` is issued to the current WebSocket and a
* syntetic {@link TerminatedCloseEvent} is immediately emitted without waiting for
* the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
terminate(): void;
}
/**
* Creates a disposable GraphQL over WebSocket client.
*
* @category Client
*/
export declare function createClient<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload']>(options: ClientOptions<P>): Client;
/**
* A syntetic close event `4499: Terminated` is issued to the current to immediately
* close the connection without waiting for the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
export declare class TerminatedCloseEvent extends Error {
name: string;
message: string;
code: number;
reason: string;
wasClean: boolean;
}

View File

@@ -0,0 +1,575 @@
"use strict";
/**
*
* client
*
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TerminatedCloseEvent = exports.createClient = void 0;
const common_1 = require("./common");
const utils_1 = require("./utils");
/** This file is the entry point for browsers, re-export common elements. */
__exportStar(require("./common"), exports);
/**
* Creates a disposable GraphQL over WebSocket client.
*
* @category Client
*/
function createClient(options) {
const { url, connectionParams, lazy = true, onNonLazyError = console.error, lazyCloseTimeout: lazyCloseTimeoutMs = 0, keepAlive = 0, disablePong, connectionAckWaitTimeout = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) {
let retryDelay = 1000; // start with 1s delay
for (let i = 0; i < retries; i++) {
retryDelay *= 2;
}
await new Promise((resolve) => setTimeout(resolve, retryDelay +
// add random timeout from 300ms to 3s
Math.floor(Math.random() * (3000 - 300) + 300)));
}, shouldRetry = isLikeCloseEvent, isFatalConnectionProblem, on, webSocketImpl,
/**
* Generates a v4 UUID to be used as the ID using `Math`
* as the random number generator. Supply your own generator
* in case you need more uniqueness.
*
* Reference: https://gist.github.com/jed/982883
*/
generateID = function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}, jsonMessageReplacer: replacer, jsonMessageReviver: reviver, } = options;
let ws;
if (webSocketImpl) {
if (!isWebSocket(webSocketImpl)) {
throw new Error('Invalid WebSocket implementation provided');
}
ws = webSocketImpl;
}
else if (typeof WebSocket !== 'undefined') {
ws = WebSocket;
}
else if (typeof global !== 'undefined') {
ws =
global.WebSocket ||
// @ts-expect-error: Support more browsers
global.MozWebSocket;
}
else if (typeof window !== 'undefined') {
ws =
window.WebSocket ||
// @ts-expect-error: Support more browsers
window.MozWebSocket;
}
if (!ws)
throw new Error("WebSocket implementation missing; on Node you can `import WebSocket from 'ws';` and pass `webSocketImpl: WebSocket` to `createClient`");
const WebSocketImpl = ws;
// websocket status emitter, subscriptions are handled differently
const emitter = (() => {
const message = (() => {
const listeners = {};
return {
on(id, listener) {
listeners[id] = listener;
return () => {
delete listeners[id];
};
},
emit(message) {
var _a;
if ('id' in message)
(_a = listeners[message.id]) === null || _a === void 0 ? void 0 : _a.call(listeners, message);
},
};
})();
const listeners = {
connecting: (on === null || on === void 0 ? void 0 : on.connecting) ? [on.connecting] : [],
opened: (on === null || on === void 0 ? void 0 : on.opened) ? [on.opened] : [],
connected: (on === null || on === void 0 ? void 0 : on.connected) ? [on.connected] : [],
ping: (on === null || on === void 0 ? void 0 : on.ping) ? [on.ping] : [],
pong: (on === null || on === void 0 ? void 0 : on.pong) ? [on.pong] : [],
message: (on === null || on === void 0 ? void 0 : on.message) ? [message.emit, on.message] : [message.emit],
closed: (on === null || on === void 0 ? void 0 : on.closed) ? [on.closed] : [],
error: (on === null || on === void 0 ? void 0 : on.error) ? [on.error] : [],
};
return {
onMessage: message.on,
on(event, listener) {
const l = listeners[event];
l.push(listener);
return () => {
l.splice(l.indexOf(listener), 1);
};
},
emit(event, ...args) {
// we copy the listeners so that unlistens dont "pull the rug under our feet"
for (const listener of [...listeners[event]]) {
// @ts-expect-error: The args should fit
listener(...args);
}
},
};
})();
// invokes the callback either when an error or closed event is emitted,
// first one that gets called prevails, other emissions are ignored
function errorOrClosed(cb) {
const listening = [
// errors are fatal and more critical than close events, throw them first
emitter.on('error', (err) => {
listening.forEach((unlisten) => unlisten());
cb(err);
}),
// closes can be graceful and not fatal, throw them second (if error didnt throw)
emitter.on('closed', (event) => {
listening.forEach((unlisten) => unlisten());
cb(event);
}),
];
}
let connecting, locks = 0, lazyCloseTimeout, retrying = false, retries = 0, disposed = false;
async function connect() {
// clear the lazy close timeout immediatelly so that close gets debounced
// see: https://github.com/enisdenjo/graphql-ws/issues/388
clearTimeout(lazyCloseTimeout);
const [socket, throwOnClose] = await (connecting !== null && connecting !== void 0 ? connecting : (connecting = new Promise((connected, denied) => (async () => {
if (retrying) {
await retryWait(retries);
// subscriptions might complete while waiting for retry
if (!locks) {
connecting = undefined;
return denied({ code: 1000, reason: 'All Subscriptions Gone' });
}
retries++;
}
emitter.emit('connecting', retrying);
const socket = new WebSocketImpl(typeof url === 'function' ? await url() : url, common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL);
let connectionAckTimeout, queuedPing;
function enqueuePing() {
if (isFinite(keepAlive) && keepAlive > 0) {
clearTimeout(queuedPing); // in case where a pong was received before a ping (this is valid behaviour)
queuedPing = setTimeout(() => {
if (socket.readyState === WebSocketImpl.OPEN) {
socket.send((0, common_1.stringifyMessage)({ type: common_1.MessageType.Ping }));
emitter.emit('ping', false, undefined);
}
}, keepAlive);
}
}
errorOrClosed((errOrEvent) => {
connecting = undefined;
clearTimeout(connectionAckTimeout);
clearTimeout(queuedPing);
denied(errOrEvent);
if (errOrEvent instanceof TerminatedCloseEvent) {
socket.close(4499, 'Terminated'); // close event is artificial and emitted manually, see `Client.terminate()` below
socket.onerror = null;
socket.onclose = null;
}
});
socket.onerror = (err) => emitter.emit('error', err);
socket.onclose = (event) => emitter.emit('closed', event);
socket.onopen = async () => {
try {
emitter.emit('opened', socket);
const payload = typeof connectionParams === 'function'
? await connectionParams()
: connectionParams;
// connectionParams might take too long causing the server to kick off the client
// the necessary error/close event is already reported - simply stop execution
if (socket.readyState !== WebSocketImpl.OPEN)
return;
socket.send((0, common_1.stringifyMessage)(payload
? {
type: common_1.MessageType.ConnectionInit,
payload,
}
: {
type: common_1.MessageType.ConnectionInit,
// payload is completely absent if not provided
}, replacer));
if (isFinite(connectionAckWaitTimeout) &&
connectionAckWaitTimeout > 0) {
connectionAckTimeout = setTimeout(() => {
socket.close(common_1.CloseCode.ConnectionAcknowledgementTimeout, 'Connection acknowledgement timeout');
}, connectionAckWaitTimeout);
}
enqueuePing(); // enqueue ping (noop if disabled)
}
catch (err) {
emitter.emit('error', err);
socket.close(common_1.CloseCode.InternalClientError, (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : new Error(err).message, 'Internal client error'));
}
};
let acknowledged = false;
socket.onmessage = ({ data }) => {
try {
const message = (0, common_1.parseMessage)(data, reviver);
emitter.emit('message', message);
if (message.type === 'ping' || message.type === 'pong') {
emitter.emit(message.type, true, message.payload); // received
if (message.type === 'pong') {
enqueuePing(); // enqueue next ping (noop if disabled)
}
else if (!disablePong) {
// respond with pong on ping
socket.send((0, common_1.stringifyMessage)(message.payload
? {
type: common_1.MessageType.Pong,
payload: message.payload,
}
: {
type: common_1.MessageType.Pong,
// payload is completely absent if not provided
}));
emitter.emit('pong', false, message.payload);
}
return; // ping and pongs can be received whenever
}
if (acknowledged)
return; // already connected and acknowledged
if (message.type !== common_1.MessageType.ConnectionAck)
throw new Error(`First message cannot be of type ${message.type}`);
clearTimeout(connectionAckTimeout);
acknowledged = true;
emitter.emit('connected', socket, message.payload, retrying); // connected = socket opened + acknowledged
retrying = false; // future lazy connects are not retries
retries = 0; // reset the retries on connect
connected([
socket,
new Promise((_, reject) => errorOrClosed(reject)),
]);
}
catch (err) {
socket.onmessage = null; // stop reading messages as soon as reading breaks once
emitter.emit('error', err);
socket.close(common_1.CloseCode.BadResponse, (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : new Error(err).message, 'Bad response'));
}
};
})())));
// if the provided socket is in a closing state, wait for the throw on close
if (socket.readyState === WebSocketImpl.CLOSING)
await throwOnClose;
let release = () => {
// releases this connection
};
const released = new Promise((resolve) => (release = resolve));
return [
socket,
release,
Promise.race([
// wait for
released.then(() => {
if (!locks) {
// and if no more locks are present, complete the connection
const complete = () => socket.close(1000, 'Normal Closure');
if (isFinite(lazyCloseTimeoutMs) && lazyCloseTimeoutMs > 0) {
// if the keepalive is set, allow for the specified calmdown time and
// then complete if the socket is still open.
lazyCloseTimeout = setTimeout(() => {
if (socket.readyState === WebSocketImpl.OPEN)
complete();
}, lazyCloseTimeoutMs);
}
else {
// otherwise complete immediately
complete();
}
}
}),
// or
throwOnClose,
]),
];
}
/**
* Checks the `connect` problem and evaluates if the client should retry.
*/
function shouldRetryConnectOrThrow(errOrCloseEvent) {
// some close codes are worth reporting immediately
if (isLikeCloseEvent(errOrCloseEvent) &&
(isFatalInternalCloseCode(errOrCloseEvent.code) ||
[
common_1.CloseCode.InternalServerError,
common_1.CloseCode.InternalClientError,
common_1.CloseCode.BadRequest,
common_1.CloseCode.BadResponse,
common_1.CloseCode.Unauthorized,
// CloseCode.Forbidden, might grant access out after retry
common_1.CloseCode.SubprotocolNotAcceptable,
// CloseCode.ConnectionInitialisationTimeout, might not time out after retry
// CloseCode.ConnectionAcknowledgementTimeout, might not time out after retry
common_1.CloseCode.SubscriberAlreadyExists,
common_1.CloseCode.TooManyInitialisationRequests,
// 4499, // Terminated, probably because the socket froze, we want to retry
].includes(errOrCloseEvent.code)))
throw errOrCloseEvent;
// client was disposed, no retries should proceed regardless
if (disposed)
return false;
// normal closure (possibly all subscriptions have completed)
// if no locks were acquired in the meantime, shouldnt try again
if (isLikeCloseEvent(errOrCloseEvent) && errOrCloseEvent.code === 1000)
return locks > 0;
// retries are not allowed or we tried to many times, report error
if (!retryAttempts || retries >= retryAttempts)
throw errOrCloseEvent;
// throw non-retryable connection problems
if (!shouldRetry(errOrCloseEvent))
throw errOrCloseEvent;
// @deprecated throw fatal connection problems immediately
if (isFatalConnectionProblem === null || isFatalConnectionProblem === void 0 ? void 0 : isFatalConnectionProblem(errOrCloseEvent))
throw errOrCloseEvent;
// looks good, start retrying
return (retrying = true);
}
// in non-lazy (hot?) mode always hold one connection lock to persist the socket
if (!lazy) {
(async () => {
locks++;
for (;;) {
try {
const [, , throwOnClose] = await connect();
await throwOnClose; // will always throw because releaser is not used
}
catch (errOrCloseEvent) {
try {
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
return;
}
catch (errOrCloseEvent) {
// report thrown error, no further retries
return onNonLazyError === null || onNonLazyError === void 0 ? void 0 : onNonLazyError(errOrCloseEvent);
}
}
}
})();
}
function subscribe(payload, sink) {
const id = generateID(payload);
let done = false, errored = false, releaser = () => {
// for handling completions before connect
locks--;
done = true;
};
(async () => {
locks++;
for (;;) {
try {
const [socket, release, waitForReleaseOrThrowOnClose] = await connect();
// if done while waiting for connect, release the connection lock right away
if (done)
return release();
const unlisten = emitter.onMessage(id, (message) => {
switch (message.type) {
case common_1.MessageType.Next: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- payload will fit type
sink.next(message.payload);
return;
}
case common_1.MessageType.Error: {
(errored = true), (done = true);
sink.error(message.payload);
releaser();
return;
}
case common_1.MessageType.Complete: {
done = true;
releaser(); // release completes the sink
return;
}
}
});
socket.send((0, common_1.stringifyMessage)({
id,
type: common_1.MessageType.Subscribe,
payload,
}, replacer));
releaser = () => {
if (!done && socket.readyState === WebSocketImpl.OPEN)
// if not completed already and socket is open, send complete message to server on release
socket.send((0, common_1.stringifyMessage)({
id,
type: common_1.MessageType.Complete,
}, replacer));
locks--;
done = true;
release();
};
// either the releaser will be called, connection completed and
// the promise resolved or the socket closed and the promise rejected.
// whatever happens though, we want to stop listening for messages
await waitForReleaseOrThrowOnClose.finally(unlisten);
return; // completed, shouldnt try again
}
catch (errOrCloseEvent) {
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
return;
}
}
})()
.then(() => {
// delivering either an error or a complete terminates the sequence
if (!errored)
sink.complete();
}) // resolves on release or normal closure
.catch((err) => {
sink.error(err);
}); // rejects on close events and errors
return () => {
// dispose only of active subscriptions
if (!done)
releaser();
};
}
return {
on: emitter.on,
subscribe,
iterate(request) {
const pending = [];
const deferred = {
done: false,
error: null,
resolve: () => {
// noop
},
};
const dispose = subscribe(request, {
next(val) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pending.push(val);
deferred.resolve();
},
error(err) {
deferred.done = true;
deferred.error = err;
deferred.resolve();
},
complete() {
deferred.done = true;
deferred.resolve();
},
});
const iterator = (function iterator() {
return __asyncGenerator(this, arguments, function* iterator_1() {
for (;;) {
if (!pending.length) {
// only wait if there are no pending messages available
yield __await(new Promise((resolve) => (deferred.resolve = resolve)));
}
// first flush
while (pending.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
yield yield __await(pending.shift());
}
// then error
if (deferred.error) {
throw deferred.error;
}
// or complete
if (deferred.done) {
return yield __await(void 0);
}
}
});
})();
iterator.throw = async (err) => {
if (!deferred.done) {
deferred.done = true;
deferred.error = err;
deferred.resolve();
}
return { done: true, value: undefined };
};
iterator.return = async () => {
dispose();
return { done: true, value: undefined };
};
return iterator;
},
async dispose() {
disposed = true;
if (connecting) {
// if there is a connection, close it
const [socket] = await connecting;
socket.close(1000, 'Normal Closure');
}
},
terminate() {
if (connecting) {
// only if there is a connection
emitter.emit('closed', new TerminatedCloseEvent());
}
},
};
}
exports.createClient = createClient;
/**
* A syntetic close event `4499: Terminated` is issued to the current to immediately
* close the connection without waiting for the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
class TerminatedCloseEvent extends Error {
constructor() {
super(...arguments);
this.name = 'TerminatedCloseEvent';
this.message = '4499: Terminated';
this.code = 4499;
this.reason = 'Terminated';
this.wasClean = false;
}
}
exports.TerminatedCloseEvent = TerminatedCloseEvent;
function isLikeCloseEvent(val) {
return (0, utils_1.isObject)(val) && 'code' in val && 'reason' in val;
}
function isFatalInternalCloseCode(code) {
if ([
1000,
1001,
1006,
1005,
1012,
1013,
1014, // Bad Gateway
].includes(code))
return false;
// all other internal errors are fatal
return code >= 1000 && code <= 1999;
}
function isWebSocket(val) {
return (typeof val === 'function' &&
'constructor' in val &&
'CLOSED' in val &&
'CLOSING' in val &&
'CONNECTING' in val &&
'OPEN' in val);
}

View File

@@ -0,0 +1,556 @@
/**
*
* client
*
*/
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
import { GRAPHQL_TRANSPORT_WS_PROTOCOL, CloseCode, MessageType, parseMessage, stringifyMessage, } from './common.mjs';
import { isObject, limitCloseReason } from './utils.mjs';
/** This file is the entry point for browsers, re-export common elements. */
export * from './common.mjs';
/**
* Creates a disposable GraphQL over WebSocket client.
*
* @category Client
*/
export function createClient(options) {
const { url, connectionParams, lazy = true, onNonLazyError = console.error, lazyCloseTimeout: lazyCloseTimeoutMs = 0, keepAlive = 0, disablePong, connectionAckWaitTimeout = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) {
let retryDelay = 1000; // start with 1s delay
for (let i = 0; i < retries; i++) {
retryDelay *= 2;
}
await new Promise((resolve) => setTimeout(resolve, retryDelay +
// add random timeout from 300ms to 3s
Math.floor(Math.random() * (3000 - 300) + 300)));
}, shouldRetry = isLikeCloseEvent, isFatalConnectionProblem, on, webSocketImpl,
/**
* Generates a v4 UUID to be used as the ID using `Math`
* as the random number generator. Supply your own generator
* in case you need more uniqueness.
*
* Reference: https://gist.github.com/jed/982883
*/
generateID = function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}, jsonMessageReplacer: replacer, jsonMessageReviver: reviver, } = options;
let ws;
if (webSocketImpl) {
if (!isWebSocket(webSocketImpl)) {
throw new Error('Invalid WebSocket implementation provided');
}
ws = webSocketImpl;
}
else if (typeof WebSocket !== 'undefined') {
ws = WebSocket;
}
else if (typeof global !== 'undefined') {
ws =
global.WebSocket ||
// @ts-expect-error: Support more browsers
global.MozWebSocket;
}
else if (typeof window !== 'undefined') {
ws =
window.WebSocket ||
// @ts-expect-error: Support more browsers
window.MozWebSocket;
}
if (!ws)
throw new Error("WebSocket implementation missing; on Node you can `import WebSocket from 'ws';` and pass `webSocketImpl: WebSocket` to `createClient`");
const WebSocketImpl = ws;
// websocket status emitter, subscriptions are handled differently
const emitter = (() => {
const message = (() => {
const listeners = {};
return {
on(id, listener) {
listeners[id] = listener;
return () => {
delete listeners[id];
};
},
emit(message) {
var _a;
if ('id' in message)
(_a = listeners[message.id]) === null || _a === void 0 ? void 0 : _a.call(listeners, message);
},
};
})();
const listeners = {
connecting: (on === null || on === void 0 ? void 0 : on.connecting) ? [on.connecting] : [],
opened: (on === null || on === void 0 ? void 0 : on.opened) ? [on.opened] : [],
connected: (on === null || on === void 0 ? void 0 : on.connected) ? [on.connected] : [],
ping: (on === null || on === void 0 ? void 0 : on.ping) ? [on.ping] : [],
pong: (on === null || on === void 0 ? void 0 : on.pong) ? [on.pong] : [],
message: (on === null || on === void 0 ? void 0 : on.message) ? [message.emit, on.message] : [message.emit],
closed: (on === null || on === void 0 ? void 0 : on.closed) ? [on.closed] : [],
error: (on === null || on === void 0 ? void 0 : on.error) ? [on.error] : [],
};
return {
onMessage: message.on,
on(event, listener) {
const l = listeners[event];
l.push(listener);
return () => {
l.splice(l.indexOf(listener), 1);
};
},
emit(event, ...args) {
// we copy the listeners so that unlistens dont "pull the rug under our feet"
for (const listener of [...listeners[event]]) {
// @ts-expect-error: The args should fit
listener(...args);
}
},
};
})();
// invokes the callback either when an error or closed event is emitted,
// first one that gets called prevails, other emissions are ignored
function errorOrClosed(cb) {
const listening = [
// errors are fatal and more critical than close events, throw them first
emitter.on('error', (err) => {
listening.forEach((unlisten) => unlisten());
cb(err);
}),
// closes can be graceful and not fatal, throw them second (if error didnt throw)
emitter.on('closed', (event) => {
listening.forEach((unlisten) => unlisten());
cb(event);
}),
];
}
let connecting, locks = 0, lazyCloseTimeout, retrying = false, retries = 0, disposed = false;
async function connect() {
// clear the lazy close timeout immediatelly so that close gets debounced
// see: https://github.com/enisdenjo/graphql-ws/issues/388
clearTimeout(lazyCloseTimeout);
const [socket, throwOnClose] = await (connecting !== null && connecting !== void 0 ? connecting : (connecting = new Promise((connected, denied) => (async () => {
if (retrying) {
await retryWait(retries);
// subscriptions might complete while waiting for retry
if (!locks) {
connecting = undefined;
return denied({ code: 1000, reason: 'All Subscriptions Gone' });
}
retries++;
}
emitter.emit('connecting', retrying);
const socket = new WebSocketImpl(typeof url === 'function' ? await url() : url, GRAPHQL_TRANSPORT_WS_PROTOCOL);
let connectionAckTimeout, queuedPing;
function enqueuePing() {
if (isFinite(keepAlive) && keepAlive > 0) {
clearTimeout(queuedPing); // in case where a pong was received before a ping (this is valid behaviour)
queuedPing = setTimeout(() => {
if (socket.readyState === WebSocketImpl.OPEN) {
socket.send(stringifyMessage({ type: MessageType.Ping }));
emitter.emit('ping', false, undefined);
}
}, keepAlive);
}
}
errorOrClosed((errOrEvent) => {
connecting = undefined;
clearTimeout(connectionAckTimeout);
clearTimeout(queuedPing);
denied(errOrEvent);
if (errOrEvent instanceof TerminatedCloseEvent) {
socket.close(4499, 'Terminated'); // close event is artificial and emitted manually, see `Client.terminate()` below
socket.onerror = null;
socket.onclose = null;
}
});
socket.onerror = (err) => emitter.emit('error', err);
socket.onclose = (event) => emitter.emit('closed', event);
socket.onopen = async () => {
try {
emitter.emit('opened', socket);
const payload = typeof connectionParams === 'function'
? await connectionParams()
: connectionParams;
// connectionParams might take too long causing the server to kick off the client
// the necessary error/close event is already reported - simply stop execution
if (socket.readyState !== WebSocketImpl.OPEN)
return;
socket.send(stringifyMessage(payload
? {
type: MessageType.ConnectionInit,
payload,
}
: {
type: MessageType.ConnectionInit,
// payload is completely absent if not provided
}, replacer));
if (isFinite(connectionAckWaitTimeout) &&
connectionAckWaitTimeout > 0) {
connectionAckTimeout = setTimeout(() => {
socket.close(CloseCode.ConnectionAcknowledgementTimeout, 'Connection acknowledgement timeout');
}, connectionAckWaitTimeout);
}
enqueuePing(); // enqueue ping (noop if disabled)
}
catch (err) {
emitter.emit('error', err);
socket.close(CloseCode.InternalClientError, limitCloseReason(err instanceof Error ? err.message : new Error(err).message, 'Internal client error'));
}
};
let acknowledged = false;
socket.onmessage = ({ data }) => {
try {
const message = parseMessage(data, reviver);
emitter.emit('message', message);
if (message.type === 'ping' || message.type === 'pong') {
emitter.emit(message.type, true, message.payload); // received
if (message.type === 'pong') {
enqueuePing(); // enqueue next ping (noop if disabled)
}
else if (!disablePong) {
// respond with pong on ping
socket.send(stringifyMessage(message.payload
? {
type: MessageType.Pong,
payload: message.payload,
}
: {
type: MessageType.Pong,
// payload is completely absent if not provided
}));
emitter.emit('pong', false, message.payload);
}
return; // ping and pongs can be received whenever
}
if (acknowledged)
return; // already connected and acknowledged
if (message.type !== MessageType.ConnectionAck)
throw new Error(`First message cannot be of type ${message.type}`);
clearTimeout(connectionAckTimeout);
acknowledged = true;
emitter.emit('connected', socket, message.payload, retrying); // connected = socket opened + acknowledged
retrying = false; // future lazy connects are not retries
retries = 0; // reset the retries on connect
connected([
socket,
new Promise((_, reject) => errorOrClosed(reject)),
]);
}
catch (err) {
socket.onmessage = null; // stop reading messages as soon as reading breaks once
emitter.emit('error', err);
socket.close(CloseCode.BadResponse, limitCloseReason(err instanceof Error ? err.message : new Error(err).message, 'Bad response'));
}
};
})())));
// if the provided socket is in a closing state, wait for the throw on close
if (socket.readyState === WebSocketImpl.CLOSING)
await throwOnClose;
let release = () => {
// releases this connection
};
const released = new Promise((resolve) => (release = resolve));
return [
socket,
release,
Promise.race([
// wait for
released.then(() => {
if (!locks) {
// and if no more locks are present, complete the connection
const complete = () => socket.close(1000, 'Normal Closure');
if (isFinite(lazyCloseTimeoutMs) && lazyCloseTimeoutMs > 0) {
// if the keepalive is set, allow for the specified calmdown time and
// then complete if the socket is still open.
lazyCloseTimeout = setTimeout(() => {
if (socket.readyState === WebSocketImpl.OPEN)
complete();
}, lazyCloseTimeoutMs);
}
else {
// otherwise complete immediately
complete();
}
}
}),
// or
throwOnClose,
]),
];
}
/**
* Checks the `connect` problem and evaluates if the client should retry.
*/
function shouldRetryConnectOrThrow(errOrCloseEvent) {
// some close codes are worth reporting immediately
if (isLikeCloseEvent(errOrCloseEvent) &&
(isFatalInternalCloseCode(errOrCloseEvent.code) ||
[
CloseCode.InternalServerError,
CloseCode.InternalClientError,
CloseCode.BadRequest,
CloseCode.BadResponse,
CloseCode.Unauthorized,
// CloseCode.Forbidden, might grant access out after retry
CloseCode.SubprotocolNotAcceptable,
// CloseCode.ConnectionInitialisationTimeout, might not time out after retry
// CloseCode.ConnectionAcknowledgementTimeout, might not time out after retry
CloseCode.SubscriberAlreadyExists,
CloseCode.TooManyInitialisationRequests,
// 4499, // Terminated, probably because the socket froze, we want to retry
].includes(errOrCloseEvent.code)))
throw errOrCloseEvent;
// client was disposed, no retries should proceed regardless
if (disposed)
return false;
// normal closure (possibly all subscriptions have completed)
// if no locks were acquired in the meantime, shouldnt try again
if (isLikeCloseEvent(errOrCloseEvent) && errOrCloseEvent.code === 1000)
return locks > 0;
// retries are not allowed or we tried to many times, report error
if (!retryAttempts || retries >= retryAttempts)
throw errOrCloseEvent;
// throw non-retryable connection problems
if (!shouldRetry(errOrCloseEvent))
throw errOrCloseEvent;
// @deprecated throw fatal connection problems immediately
if (isFatalConnectionProblem === null || isFatalConnectionProblem === void 0 ? void 0 : isFatalConnectionProblem(errOrCloseEvent))
throw errOrCloseEvent;
// looks good, start retrying
return (retrying = true);
}
// in non-lazy (hot?) mode always hold one connection lock to persist the socket
if (!lazy) {
(async () => {
locks++;
for (;;) {
try {
const [, , throwOnClose] = await connect();
await throwOnClose; // will always throw because releaser is not used
}
catch (errOrCloseEvent) {
try {
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
return;
}
catch (errOrCloseEvent) {
// report thrown error, no further retries
return onNonLazyError === null || onNonLazyError === void 0 ? void 0 : onNonLazyError(errOrCloseEvent);
}
}
}
})();
}
function subscribe(payload, sink) {
const id = generateID(payload);
let done = false, errored = false, releaser = () => {
// for handling completions before connect
locks--;
done = true;
};
(async () => {
locks++;
for (;;) {
try {
const [socket, release, waitForReleaseOrThrowOnClose] = await connect();
// if done while waiting for connect, release the connection lock right away
if (done)
return release();
const unlisten = emitter.onMessage(id, (message) => {
switch (message.type) {
case MessageType.Next: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- payload will fit type
sink.next(message.payload);
return;
}
case MessageType.Error: {
(errored = true), (done = true);
sink.error(message.payload);
releaser();
return;
}
case MessageType.Complete: {
done = true;
releaser(); // release completes the sink
return;
}
}
});
socket.send(stringifyMessage({
id,
type: MessageType.Subscribe,
payload,
}, replacer));
releaser = () => {
if (!done && socket.readyState === WebSocketImpl.OPEN)
// if not completed already and socket is open, send complete message to server on release
socket.send(stringifyMessage({
id,
type: MessageType.Complete,
}, replacer));
locks--;
done = true;
release();
};
// either the releaser will be called, connection completed and
// the promise resolved or the socket closed and the promise rejected.
// whatever happens though, we want to stop listening for messages
await waitForReleaseOrThrowOnClose.finally(unlisten);
return; // completed, shouldnt try again
}
catch (errOrCloseEvent) {
if (!shouldRetryConnectOrThrow(errOrCloseEvent))
return;
}
}
})()
.then(() => {
// delivering either an error or a complete terminates the sequence
if (!errored)
sink.complete();
}) // resolves on release or normal closure
.catch((err) => {
sink.error(err);
}); // rejects on close events and errors
return () => {
// dispose only of active subscriptions
if (!done)
releaser();
};
}
return {
on: emitter.on,
subscribe,
iterate(request) {
const pending = [];
const deferred = {
done: false,
error: null,
resolve: () => {
// noop
},
};
const dispose = subscribe(request, {
next(val) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pending.push(val);
deferred.resolve();
},
error(err) {
deferred.done = true;
deferred.error = err;
deferred.resolve();
},
complete() {
deferred.done = true;
deferred.resolve();
},
});
const iterator = (function iterator() {
return __asyncGenerator(this, arguments, function* iterator_1() {
for (;;) {
if (!pending.length) {
// only wait if there are no pending messages available
yield __await(new Promise((resolve) => (deferred.resolve = resolve)));
}
// first flush
while (pending.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
yield yield __await(pending.shift());
}
// then error
if (deferred.error) {
throw deferred.error;
}
// or complete
if (deferred.done) {
return yield __await(void 0);
}
}
});
})();
iterator.throw = async (err) => {
if (!deferred.done) {
deferred.done = true;
deferred.error = err;
deferred.resolve();
}
return { done: true, value: undefined };
};
iterator.return = async () => {
dispose();
return { done: true, value: undefined };
};
return iterator;
},
async dispose() {
disposed = true;
if (connecting) {
// if there is a connection, close it
const [socket] = await connecting;
socket.close(1000, 'Normal Closure');
}
},
terminate() {
if (connecting) {
// only if there is a connection
emitter.emit('closed', new TerminatedCloseEvent());
}
},
};
}
/**
* A syntetic close event `4499: Terminated` is issued to the current to immediately
* close the connection without waiting for the one coming from `WebSocket.onclose`.
*
* Terminating is not considered fatal and a connection retry will occur as expected.
*
* Useful in cases where the WebSocket is stuck and not emitting any events;
* can happen on iOS Safari, see: https://github.com/enisdenjo/graphql-ws/discussions/290.
*/
export class TerminatedCloseEvent extends Error {
constructor() {
super(...arguments);
this.name = 'TerminatedCloseEvent';
this.message = '4499: Terminated';
this.code = 4499;
this.reason = 'Terminated';
this.wasClean = false;
}
}
function isLikeCloseEvent(val) {
return isObject(val) && 'code' in val && 'reason' in val;
}
function isFatalInternalCloseCode(code) {
if ([
1000,
1001,
1006,
1005,
1012,
1013,
1014, // Bad Gateway
].includes(code))
return false;
// all other internal errors are fatal
return code >= 1000 && code <= 1999;
}
function isWebSocket(val) {
return (typeof val === 'function' &&
'constructor' in val &&
'CLOSED' in val &&
'CLOSING' in val &&
'CONNECTING' in val &&
'OPEN' in val);
}

View File

@@ -0,0 +1,201 @@
/**
*
* common
*
*/
import { GraphQLError } from 'graphql';
/**
* The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export declare const GRAPHQL_TRANSPORT_WS_PROTOCOL = "graphql-transport-ws";
/**
* The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws).
*
* @private
*/
export declare const DEPRECATED_GRAPHQL_WS_PROTOCOL = "graphql-ws";
/**
* `graphql-ws` expected and standard close codes of the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export declare enum CloseCode {
InternalServerError = 4500,
InternalClientError = 4005,
BadRequest = 4400,
BadResponse = 4004,
/** Tried subscribing before connect ack */
Unauthorized = 4401,
Forbidden = 4403,
SubprotocolNotAcceptable = 4406,
ConnectionInitialisationTimeout = 4408,
ConnectionAcknowledgementTimeout = 4504,
/** Subscriber distinction is very important */
SubscriberAlreadyExists = 4409,
TooManyInitialisationRequests = 4429
}
/**
* ID is a string type alias representing
* the globally unique ID used for identifying
* subscriptions established by the client.
*
* @category Common
*/
export type ID = string;
/** @category Common */
export interface Disposable {
/** Dispose of the instance and clear up resources. */
dispose: () => void | Promise<void>;
}
/**
* A representation of any set of values over any amount of time.
*
* @category Common
*/
export interface Sink<T = unknown> {
/** Next value arriving. */
next(value: T): void;
/**
* An error that has occured. Calling this function "closes" the sink.
* Besides the errors being `Error` and `readonly GraphQLError[]`, it
* can also be a `CloseEvent`, but to avoid bundling DOM typings because
* the client can run in Node env too, you should assert the close event
* type during implementation.
*/
error(error: unknown): void;
/** The sink has completed. This function "closes" the sink. */
complete(): void;
}
/**
* Types of messages allowed to be sent by the client/server over the WS protocol.
*
* @category Common
*/
export declare enum MessageType {
ConnectionInit = "connection_init",
ConnectionAck = "connection_ack",
Ping = "ping",
Pong = "pong",
Subscribe = "subscribe",
Next = "next",
Error = "error",
Complete = "complete"
}
/** @category Common */
export interface ConnectionInitMessage {
readonly type: MessageType.ConnectionInit;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface ConnectionAckMessage {
readonly type: MessageType.ConnectionAck;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface PingMessage {
readonly type: MessageType.Ping;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface PongMessage {
readonly type: MessageType.Pong;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface SubscribeMessage {
readonly id: ID;
readonly type: MessageType.Subscribe;
readonly payload: SubscribePayload;
}
/** @category Common */
export interface SubscribePayload {
readonly operationName?: string | null;
readonly query: string;
readonly variables?: Record<string, unknown> | null;
readonly extensions?: Record<string, unknown> | null;
}
/** @category Common */
export interface ExecutionResult<Data = Record<string, unknown>, Extensions = Record<string, unknown>> {
errors?: ReadonlyArray<GraphQLError>;
data?: Data | null;
hasNext?: boolean;
extensions?: Extensions;
}
/** @category Common */
export interface ExecutionPatchResult<Data = unknown, Extensions = Record<string, unknown>> {
errors?: ReadonlyArray<GraphQLError>;
data?: Data | null;
path?: ReadonlyArray<string | number>;
label?: string;
hasNext: boolean;
extensions?: Extensions;
}
/** @category Common */
export interface NextMessage {
readonly id: ID;
readonly type: MessageType.Next;
readonly payload: ExecutionResult | ExecutionPatchResult;
}
/** @category Common */
export interface ErrorMessage {
readonly id: ID;
readonly type: MessageType.Error;
readonly payload: readonly GraphQLError[];
}
/** @category Common */
export interface CompleteMessage {
readonly id: ID;
readonly type: MessageType.Complete;
}
/** @category Common */
export type Message<T extends MessageType = MessageType> = T extends MessageType.ConnectionAck ? ConnectionAckMessage : T extends MessageType.ConnectionInit ? ConnectionInitMessage : T extends MessageType.Ping ? PingMessage : T extends MessageType.Pong ? PongMessage : T extends MessageType.Subscribe ? SubscribeMessage : T extends MessageType.Next ? NextMessage : T extends MessageType.Error ? ErrorMessage : T extends MessageType.Complete ? CompleteMessage : never;
/**
* Validates the message against the GraphQL over WebSocket Protocol.
*
* Invalid messages will throw descriptive errors.
*
* @category Common
*/
export declare function validateMessage(val: unknown): Message;
/**
* Checks if the provided value is a valid GraphQL over WebSocket message.
*
* @deprecated Use `validateMessage` instead.
*
* @category Common
*/
export declare function isMessage(val: unknown): val is Message;
/**
* Function for transforming values within a message during JSON parsing
* The values are produced by parsing the incoming raw JSON.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter
*
* @category Common
*/
export type JSONMessageReviver = (this: any, key: string, value: any) => any;
/**
* Parses the raw websocket message data to a valid message.
*
* @category Common
*/
export declare function parseMessage(data: unknown, reviver?: JSONMessageReviver): Message;
/**
* Function that allows customization of the produced JSON string
* for the elements of an outgoing `Message` object.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
*
* @category Common
*/
export type JSONMessageReplacer = (this: any, key: string, value: any) => any;
/**
* Stringifies a valid message ready to be sent through the socket.
*
* @category Common
*/
export declare function stringifyMessage<T extends MessageType>(msg: Message<T>, replacer?: JSONMessageReplacer): string;

View File

@@ -0,0 +1,201 @@
/**
*
* common
*
*/
import { GraphQLError } from 'graphql';
/**
* The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export declare const GRAPHQL_TRANSPORT_WS_PROTOCOL = "graphql-transport-ws";
/**
* The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws).
*
* @private
*/
export declare const DEPRECATED_GRAPHQL_WS_PROTOCOL = "graphql-ws";
/**
* `graphql-ws` expected and standard close codes of the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export declare enum CloseCode {
InternalServerError = 4500,
InternalClientError = 4005,
BadRequest = 4400,
BadResponse = 4004,
/** Tried subscribing before connect ack */
Unauthorized = 4401,
Forbidden = 4403,
SubprotocolNotAcceptable = 4406,
ConnectionInitialisationTimeout = 4408,
ConnectionAcknowledgementTimeout = 4504,
/** Subscriber distinction is very important */
SubscriberAlreadyExists = 4409,
TooManyInitialisationRequests = 4429
}
/**
* ID is a string type alias representing
* the globally unique ID used for identifying
* subscriptions established by the client.
*
* @category Common
*/
export type ID = string;
/** @category Common */
export interface Disposable {
/** Dispose of the instance and clear up resources. */
dispose: () => void | Promise<void>;
}
/**
* A representation of any set of values over any amount of time.
*
* @category Common
*/
export interface Sink<T = unknown> {
/** Next value arriving. */
next(value: T): void;
/**
* An error that has occured. Calling this function "closes" the sink.
* Besides the errors being `Error` and `readonly GraphQLError[]`, it
* can also be a `CloseEvent`, but to avoid bundling DOM typings because
* the client can run in Node env too, you should assert the close event
* type during implementation.
*/
error(error: unknown): void;
/** The sink has completed. This function "closes" the sink. */
complete(): void;
}
/**
* Types of messages allowed to be sent by the client/server over the WS protocol.
*
* @category Common
*/
export declare enum MessageType {
ConnectionInit = "connection_init",
ConnectionAck = "connection_ack",
Ping = "ping",
Pong = "pong",
Subscribe = "subscribe",
Next = "next",
Error = "error",
Complete = "complete"
}
/** @category Common */
export interface ConnectionInitMessage {
readonly type: MessageType.ConnectionInit;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface ConnectionAckMessage {
readonly type: MessageType.ConnectionAck;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface PingMessage {
readonly type: MessageType.Ping;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface PongMessage {
readonly type: MessageType.Pong;
readonly payload?: Record<string, unknown>;
}
/** @category Common */
export interface SubscribeMessage {
readonly id: ID;
readonly type: MessageType.Subscribe;
readonly payload: SubscribePayload;
}
/** @category Common */
export interface SubscribePayload {
readonly operationName?: string | null;
readonly query: string;
readonly variables?: Record<string, unknown> | null;
readonly extensions?: Record<string, unknown> | null;
}
/** @category Common */
export interface ExecutionResult<Data = Record<string, unknown>, Extensions = Record<string, unknown>> {
errors?: ReadonlyArray<GraphQLError>;
data?: Data | null;
hasNext?: boolean;
extensions?: Extensions;
}
/** @category Common */
export interface ExecutionPatchResult<Data = unknown, Extensions = Record<string, unknown>> {
errors?: ReadonlyArray<GraphQLError>;
data?: Data | null;
path?: ReadonlyArray<string | number>;
label?: string;
hasNext: boolean;
extensions?: Extensions;
}
/** @category Common */
export interface NextMessage {
readonly id: ID;
readonly type: MessageType.Next;
readonly payload: ExecutionResult | ExecutionPatchResult;
}
/** @category Common */
export interface ErrorMessage {
readonly id: ID;
readonly type: MessageType.Error;
readonly payload: readonly GraphQLError[];
}
/** @category Common */
export interface CompleteMessage {
readonly id: ID;
readonly type: MessageType.Complete;
}
/** @category Common */
export type Message<T extends MessageType = MessageType> = T extends MessageType.ConnectionAck ? ConnectionAckMessage : T extends MessageType.ConnectionInit ? ConnectionInitMessage : T extends MessageType.Ping ? PingMessage : T extends MessageType.Pong ? PongMessage : T extends MessageType.Subscribe ? SubscribeMessage : T extends MessageType.Next ? NextMessage : T extends MessageType.Error ? ErrorMessage : T extends MessageType.Complete ? CompleteMessage : never;
/**
* Validates the message against the GraphQL over WebSocket Protocol.
*
* Invalid messages will throw descriptive errors.
*
* @category Common
*/
export declare function validateMessage(val: unknown): Message;
/**
* Checks if the provided value is a valid GraphQL over WebSocket message.
*
* @deprecated Use `validateMessage` instead.
*
* @category Common
*/
export declare function isMessage(val: unknown): val is Message;
/**
* Function for transforming values within a message during JSON parsing
* The values are produced by parsing the incoming raw JSON.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter
*
* @category Common
*/
export type JSONMessageReviver = (this: any, key: string, value: any) => any;
/**
* Parses the raw websocket message data to a valid message.
*
* @category Common
*/
export declare function parseMessage(data: unknown, reviver?: JSONMessageReviver): Message;
/**
* Function that allows customization of the produced JSON string
* for the elements of an outgoing `Message` object.
*
* Read more about using it:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
*
* @category Common
*/
export type JSONMessageReplacer = (this: any, key: string, value: any) => any;
/**
* Stringifies a valid message ready to be sent through the socket.
*
* @category Common
*/
export declare function stringifyMessage<T extends MessageType>(msg: Message<T>, replacer?: JSONMessageReplacer): string;

View File

@@ -0,0 +1,185 @@
"use strict";
/**
*
* common
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.stringifyMessage = exports.parseMessage = exports.isMessage = exports.validateMessage = exports.MessageType = exports.CloseCode = exports.DEPRECATED_GRAPHQL_WS_PROTOCOL = exports.GRAPHQL_TRANSPORT_WS_PROTOCOL = void 0;
const utils_1 = require("./utils");
/**
* The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
exports.GRAPHQL_TRANSPORT_WS_PROTOCOL = 'graphql-transport-ws';
/**
* The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws).
*
* @private
*/
exports.DEPRECATED_GRAPHQL_WS_PROTOCOL = 'graphql-ws';
/**
* `graphql-ws` expected and standard close codes of the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
var CloseCode;
(function (CloseCode) {
CloseCode[CloseCode["InternalServerError"] = 4500] = "InternalServerError";
CloseCode[CloseCode["InternalClientError"] = 4005] = "InternalClientError";
CloseCode[CloseCode["BadRequest"] = 4400] = "BadRequest";
CloseCode[CloseCode["BadResponse"] = 4004] = "BadResponse";
/** Tried subscribing before connect ack */
CloseCode[CloseCode["Unauthorized"] = 4401] = "Unauthorized";
CloseCode[CloseCode["Forbidden"] = 4403] = "Forbidden";
CloseCode[CloseCode["SubprotocolNotAcceptable"] = 4406] = "SubprotocolNotAcceptable";
CloseCode[CloseCode["ConnectionInitialisationTimeout"] = 4408] = "ConnectionInitialisationTimeout";
CloseCode[CloseCode["ConnectionAcknowledgementTimeout"] = 4504] = "ConnectionAcknowledgementTimeout";
/** Subscriber distinction is very important */
CloseCode[CloseCode["SubscriberAlreadyExists"] = 4409] = "SubscriberAlreadyExists";
CloseCode[CloseCode["TooManyInitialisationRequests"] = 4429] = "TooManyInitialisationRequests";
})(CloseCode || (exports.CloseCode = CloseCode = {}));
/**
* Types of messages allowed to be sent by the client/server over the WS protocol.
*
* @category Common
*/
var MessageType;
(function (MessageType) {
MessageType["ConnectionInit"] = "connection_init";
MessageType["ConnectionAck"] = "connection_ack";
MessageType["Ping"] = "ping";
MessageType["Pong"] = "pong";
MessageType["Subscribe"] = "subscribe";
MessageType["Next"] = "next";
MessageType["Error"] = "error";
MessageType["Complete"] = "complete";
})(MessageType || (exports.MessageType = MessageType = {}));
/**
* Validates the message against the GraphQL over WebSocket Protocol.
*
* Invalid messages will throw descriptive errors.
*
* @category Common
*/
function validateMessage(val) {
if (!(0, utils_1.isObject)(val)) {
throw new Error(`Message is expected to be an object, but got ${(0, utils_1.extendedTypeof)(val)}`);
}
if (!val.type) {
throw new Error(`Message is missing the 'type' property`);
}
if (typeof val.type !== 'string') {
throw new Error(`Message is expects the 'type' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.type)}`);
}
switch (val.type) {
case MessageType.ConnectionInit:
case MessageType.ConnectionAck:
case MessageType.Ping:
case MessageType.Pong: {
if (val.payload != null && !(0, utils_1.isObject)(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object or nullish or missing, but got "${val.payload}"`);
}
break;
}
case MessageType.Subscribe: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!(0, utils_1.isObject)(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${(0, utils_1.extendedTypeof)(val.payload)}`);
}
if (typeof val.payload.query !== 'string') {
throw new Error(`"${val.type}" message payload expects the 'query' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.payload.query)}`);
}
if (val.payload.variables != null && !(0, utils_1.isObject)(val.payload.variables)) {
throw new Error(`"${val.type}" message payload expects the 'variables' property to be a an object or nullish or missing, but got ${(0, utils_1.extendedTypeof)(val.payload.variables)}`);
}
if (val.payload.operationName != null &&
(0, utils_1.extendedTypeof)(val.payload.operationName) !== 'string') {
throw new Error(`"${val.type}" message payload expects the 'operationName' property to be a string or nullish or missing, but got ${(0, utils_1.extendedTypeof)(val.payload.operationName)}`);
}
if (val.payload.extensions != null && !(0, utils_1.isObject)(val.payload.extensions)) {
throw new Error(`"${val.type}" message payload expects the 'extensions' property to be a an object or nullish or missing, but got ${(0, utils_1.extendedTypeof)(val.payload.extensions)}`);
}
break;
}
case MessageType.Next: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!(0, utils_1.isObject)(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${(0, utils_1.extendedTypeof)(val.payload)}`);
}
break;
}
case MessageType.Error: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!(0, utils_1.areGraphQLErrors)(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an array of GraphQL errors, but got ${JSON.stringify(val.payload)}`);
}
break;
}
case MessageType.Complete: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${(0, utils_1.extendedTypeof)(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
break;
}
default:
throw new Error(`Invalid message 'type' property "${val.type}"`);
}
return val;
}
exports.validateMessage = validateMessage;
/**
* Checks if the provided value is a valid GraphQL over WebSocket message.
*
* @deprecated Use `validateMessage` instead.
*
* @category Common
*/
function isMessage(val) {
try {
validateMessage(val);
return true;
}
catch (_a) {
return false;
}
}
exports.isMessage = isMessage;
/**
* Parses the raw websocket message data to a valid message.
*
* @category Common
*/
function parseMessage(data, reviver) {
return validateMessage(typeof data === 'string' ? JSON.parse(data, reviver) : data);
}
exports.parseMessage = parseMessage;
/**
* Stringifies a valid message ready to be sent through the socket.
*
* @category Common
*/
function stringifyMessage(msg, replacer) {
validateMessage(msg);
return JSON.stringify(msg, replacer);
}
exports.stringifyMessage = stringifyMessage;

View File

@@ -0,0 +1,178 @@
/**
*
* common
*
*/
import { areGraphQLErrors, extendedTypeof, isObject } from './utils.mjs';
/**
* The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export const GRAPHQL_TRANSPORT_WS_PROTOCOL = 'graphql-transport-ws';
/**
* The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws).
*
* @private
*/
export const DEPRECATED_GRAPHQL_WS_PROTOCOL = 'graphql-ws';
/**
* `graphql-ws` expected and standard close codes of the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Common
*/
export var CloseCode;
(function (CloseCode) {
CloseCode[CloseCode["InternalServerError"] = 4500] = "InternalServerError";
CloseCode[CloseCode["InternalClientError"] = 4005] = "InternalClientError";
CloseCode[CloseCode["BadRequest"] = 4400] = "BadRequest";
CloseCode[CloseCode["BadResponse"] = 4004] = "BadResponse";
/** Tried subscribing before connect ack */
CloseCode[CloseCode["Unauthorized"] = 4401] = "Unauthorized";
CloseCode[CloseCode["Forbidden"] = 4403] = "Forbidden";
CloseCode[CloseCode["SubprotocolNotAcceptable"] = 4406] = "SubprotocolNotAcceptable";
CloseCode[CloseCode["ConnectionInitialisationTimeout"] = 4408] = "ConnectionInitialisationTimeout";
CloseCode[CloseCode["ConnectionAcknowledgementTimeout"] = 4504] = "ConnectionAcknowledgementTimeout";
/** Subscriber distinction is very important */
CloseCode[CloseCode["SubscriberAlreadyExists"] = 4409] = "SubscriberAlreadyExists";
CloseCode[CloseCode["TooManyInitialisationRequests"] = 4429] = "TooManyInitialisationRequests";
})(CloseCode || (CloseCode = {}));
/**
* Types of messages allowed to be sent by the client/server over the WS protocol.
*
* @category Common
*/
export var MessageType;
(function (MessageType) {
MessageType["ConnectionInit"] = "connection_init";
MessageType["ConnectionAck"] = "connection_ack";
MessageType["Ping"] = "ping";
MessageType["Pong"] = "pong";
MessageType["Subscribe"] = "subscribe";
MessageType["Next"] = "next";
MessageType["Error"] = "error";
MessageType["Complete"] = "complete";
})(MessageType || (MessageType = {}));
/**
* Validates the message against the GraphQL over WebSocket Protocol.
*
* Invalid messages will throw descriptive errors.
*
* @category Common
*/
export function validateMessage(val) {
if (!isObject(val)) {
throw new Error(`Message is expected to be an object, but got ${extendedTypeof(val)}`);
}
if (!val.type) {
throw new Error(`Message is missing the 'type' property`);
}
if (typeof val.type !== 'string') {
throw new Error(`Message is expects the 'type' property to be a string, but got ${extendedTypeof(val.type)}`);
}
switch (val.type) {
case MessageType.ConnectionInit:
case MessageType.ConnectionAck:
case MessageType.Ping:
case MessageType.Pong: {
if (val.payload != null && !isObject(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object or nullish or missing, but got "${val.payload}"`);
}
break;
}
case MessageType.Subscribe: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!isObject(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${extendedTypeof(val.payload)}`);
}
if (typeof val.payload.query !== 'string') {
throw new Error(`"${val.type}" message payload expects the 'query' property to be a string, but got ${extendedTypeof(val.payload.query)}`);
}
if (val.payload.variables != null && !isObject(val.payload.variables)) {
throw new Error(`"${val.type}" message payload expects the 'variables' property to be a an object or nullish or missing, but got ${extendedTypeof(val.payload.variables)}`);
}
if (val.payload.operationName != null &&
extendedTypeof(val.payload.operationName) !== 'string') {
throw new Error(`"${val.type}" message payload expects the 'operationName' property to be a string or nullish or missing, but got ${extendedTypeof(val.payload.operationName)}`);
}
if (val.payload.extensions != null && !isObject(val.payload.extensions)) {
throw new Error(`"${val.type}" message payload expects the 'extensions' property to be a an object or nullish or missing, but got ${extendedTypeof(val.payload.extensions)}`);
}
break;
}
case MessageType.Next: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!isObject(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an object, but got ${extendedTypeof(val.payload)}`);
}
break;
}
case MessageType.Error: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
if (!areGraphQLErrors(val.payload)) {
throw new Error(`"${val.type}" message expects the 'payload' property to be an array of GraphQL errors, but got ${JSON.stringify(val.payload)}`);
}
break;
}
case MessageType.Complete: {
if (typeof val.id !== 'string') {
throw new Error(`"${val.type}" message expects the 'id' property to be a string, but got ${extendedTypeof(val.id)}`);
}
if (!val.id) {
throw new Error(`"${val.type}" message requires a non-empty 'id' property`);
}
break;
}
default:
throw new Error(`Invalid message 'type' property "${val.type}"`);
}
return val;
}
/**
* Checks if the provided value is a valid GraphQL over WebSocket message.
*
* @deprecated Use `validateMessage` instead.
*
* @category Common
*/
export function isMessage(val) {
try {
validateMessage(val);
return true;
}
catch (_a) {
return false;
}
}
/**
* Parses the raw websocket message data to a valid message.
*
* @category Common
*/
export function parseMessage(data, reviver) {
return validateMessage(typeof data === 'string' ? JSON.parse(data, reviver) : data);
}
/**
* Stringifies a valid message ready to be sent through the socket.
*
* @category Common
*/
export function stringifyMessage(msg, replacer) {
validateMessage(msg);
return JSON.stringify(msg, replacer);
}

View File

@@ -0,0 +1,3 @@
export * from './client.mjs';
export * from './server.mjs';
export * from './common.mjs';

View File

@@ -0,0 +1,3 @@
export * from './client';
export * from './server';
export * from './common';

View File

@@ -0,0 +1,19 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./client"), exports);
__exportStar(require("./server"), exports);
__exportStar(require("./common"), exports);

View File

@@ -0,0 +1,3 @@
export * from './client.mjs';
export * from './server.mjs';
export * from './common.mjs';

View File

@@ -0,0 +1,417 @@
/**
*
* server
*
*/
import { OperationTypeNode, GraphQLSchema, ExecutionArgs, validate as graphqlValidate, GraphQLError, SubscriptionArgs } from 'graphql';
import { GRAPHQL_TRANSPORT_WS_PROTOCOL, ID, ConnectionInitMessage, SubscribeMessage, NextMessage, ErrorMessage, CompleteMessage, JSONMessageReplacer, JSONMessageReviver, PingMessage, PongMessage, ExecutionResult, ExecutionPatchResult } from './common.mjs';
/** @category Server */
export type OperationResult = Promise<AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult> | AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult;
/**
* A concrete GraphQL execution context value type.
*
* Mainly used because TypeScript collapses unions
* with `any` or `unknown` to `any` or `unknown`. So,
* we use a custom type to allow definitions such as
* the `context` server option.
*
* @category Server
*/
export type GraphQLExecutionContextValue = object | symbol | number | string | boolean | undefined | null;
/** @category Server */
export interface ServerOptions<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown> {
/**
* The GraphQL schema on which the operations
* will be executed and validated against.
*
* If a function is provided, it will be called on
* every subscription request allowing you to manipulate
* schema dynamically.
*
* If the schema is left undefined, you're trusted to
* provide one in the returned `ExecutionArgs` from the
* `onSubscribe` callback.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
schema?: GraphQLSchema | ((ctx: Context<P, E>, message: SubscribeMessage, args: Omit<ExecutionArgs, 'schema'>) => Promise<GraphQLSchema> | GraphQLSchema);
/**
* A value which is provided to every resolver and holds
* important contextual information like the currently
* logged in user, or access to a database.
*
* If you return from `onSubscribe`, and the returned value is
* missing the `contextValue` field, this context will be used
* instead.
*
* If you use the function signature, the final execution arguments
* will be passed in (also the returned value from `onSubscribe`).
* Since the context is injected on every subscribe, the `SubscribeMessage`
* with the regular `Context` will be passed in through the arguments too.
*
* Note that the context function is invoked on each operation only once.
* Meaning, for subscriptions, only at the point of initialising the subscription;
* not on every subscription event emission. Read more about the context lifecycle
* in subscriptions here: https://github.com/graphql/graphql-js/issues/894.
*/
context?: GraphQLExecutionContextValue | ((ctx: Context<P, E>, message: SubscribeMessage, args: ExecutionArgs) => Promise<GraphQLExecutionContextValue> | GraphQLExecutionContextValue);
/**
* The GraphQL root fields or resolvers to go
* alongside the schema. Learn more about them
* here: https://graphql.org/learn/execution/#root-fields-resolvers.
*
* If you return from `onSubscribe`, and the returned value is
* missing the `rootValue` field, the relevant operation root
* will be used instead.
*/
roots?: {
[operation in OperationTypeNode]?: Record<string, NonNullable<SubscriptionArgs['rootValue']>>;
};
/**
* A custom GraphQL validate function allowing you to apply your
* own validation rules.
*
* Returned, non-empty, array of `GraphQLError`s will be communicated
* to the client through the `ErrorMessage`. Use an empty array if the
* document is valid and no errors have been encountered.
*
* Will not be used when implementing a custom `onSubscribe`.
*
* Throwing an error from within this function will close the socket
* with the `Error` message in the close event reason.
*/
validate?: typeof graphqlValidate;
/**
* Is the `execute` function from GraphQL which is
* used to execute the query and mutation operations.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
execute?: (args: ExecutionArgs) => OperationResult;
/**
* Is the `subscribe` function from GraphQL which is
* used to execute the subscription operation.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
subscribe?: (args: ExecutionArgs) => OperationResult;
/**
* The amount of time for which the server will wait
* for `ConnectionInit` message.
*
* Set the value to `Infinity`, `''`, `0`, `null` or `undefined` to skip waiting.
*
* If the wait timeout has passed and the client
* has not sent the `ConnectionInit` message,
* the server will terminate the socket by
* dispatching a close event `4408: Connection initialisation timeout`
*
* @default 3_000 // 3 seconds
*/
connectionInitWaitTimeout?: number;
/**
* Is the connection callback called when the
* client requests the connection initialisation
* through the message `ConnectionInit`.
*
* The message payload (`connectionParams` from the
* client) is present in the `Context.connectionParams`.
*
* - Returning `true` or nothing from the callback will
* allow the client to connect.
*
* - Returning `false` from the callback will
* terminate the socket by dispatching the
* close event `4403: Forbidden`.
*
* - Returning a `Record` from the callback will
* allow the client to connect and pass the returned
* value to the client through the optional `payload`
* field in the `ConnectionAck` message.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onConnect?: (ctx: Context<P, E>) => Promise<Record<string, unknown> | boolean | void> | Record<string, unknown> | boolean | void;
/**
* Called when the client disconnects for whatever reason after
* he successfully went through the connection initialisation phase.
* Provides the close event too. Beware that this callback happens
* AFTER all subscriptions have been gracefully completed and BEFORE
* the `onClose` callback.
*
* If you are interested in tracking the subscriptions completions,
* consider using the `onComplete` callback.
*
* This callback will be called EXCLUSIVELY if the client connection
* is acknowledged. Meaning, `onConnect` will be called before the `onDisconnect`.
*
* For tracking socket closures at any point in time, regardless
* of the connection state - consider using the `onClose` callback.
*/
onDisconnect?: (ctx: Context<P, E>, code: number, reason: string) => Promise<void> | void;
/**
* Called when the socket closes for whatever reason, at any
* point in time. Provides the close event too. Beware
* that this callback happens AFTER all subscriptions have
* been gracefully completed and AFTER the `onDisconnect` callback.
*
* If you are interested in tracking the subscriptions completions,
* consider using the `onComplete` callback.
*
* In comparison to `onDisconnect`, this callback will ALWAYS
* be called, regardless if the user successfully went through
* the connection initialisation or not. `onConnect` might not
* called before the `onClose`.
*/
onClose?: (ctx: Context<P, E>, code: number, reason: string) => Promise<void> | void;
/**
* The subscribe callback executed right after
* acknowledging the request before any payload
* processing has been performed.
*
* If you return `ExecutionArgs` from the callback,
* it will be used instead of trying to build one
* internally. In this case, you are responsible
* for providing a ready set of arguments which will
* be directly plugged in the operation execution.
*
* Omitting the fields `contextValue` or `rootValue`
* from the returned value will have the provided server
* options fill in the gaps.
*
* To report GraphQL errors simply return an array
* of them from the callback, they will be reported
* to the client through the error message.
*
* Useful for preparing the execution arguments
* following a custom logic. A typical use case are
* persisted queries, you can identify the query from
* the subscribe message and create the GraphQL operation
* execution args which are then returned by the function.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onSubscribe?: (ctx: Context<P, E>, message: SubscribeMessage) => Promise<ExecutionArgs | readonly GraphQLError[] | void> | ExecutionArgs | readonly GraphQLError[] | void;
/**
* Executed after the operation call resolves. For streaming
* operations, triggering this callback does not necessarily
* mean that there is already a result available - it means
* that the subscription process for the stream has resolved
* and that the client is now subscribed.
*
* The `OperationResult` argument is the result of operation
* execution. It can be an iterator or already a value.
*
* If you want the single result and the events from a streaming
* operation, use the `onNext` callback.
*
* Use this callback to listen for subscribe operation and
* execution result manipulation.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onOperation?: (ctx: Context<P, E>, message: SubscribeMessage, args: ExecutionArgs, result: OperationResult) => Promise<OperationResult | void> | OperationResult | void;
/**
* Executed after an error occurred right before it
* has been dispatched to the client.
*
* Use this callback to format the outgoing GraphQL
* errors before they reach the client.
*
* Returned result will be injected in the error message payload.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onError?: (ctx: Context<P, E>, message: ErrorMessage, errors: readonly GraphQLError[]) => Promise<readonly GraphQLError[] | void> | readonly GraphQLError[] | void;
/**
* Executed after an operation has emitted a result right before
* that result has been sent to the client. Results from both
* single value and streaming operations will appear in this callback.
*
* Use this callback if you want to format the execution result
* before it reaches the client.
*
* Returned result will be injected in the next message payload.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onNext?: (ctx: Context<P, E>, message: NextMessage, args: ExecutionArgs, result: ExecutionResult | ExecutionPatchResult) => Promise<ExecutionResult | ExecutionPatchResult | void> | ExecutionResult | ExecutionPatchResult | void;
/**
* The complete callback is executed after the
* operation has completed right before sending
* the complete message to the client.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*
* Since the library makes sure to complete streaming
* operations even after an abrupt closure, this callback
* will still be called.
*/
onComplete?: (ctx: Context<P, E>, message: CompleteMessage) => Promise<void> | void;
/**
* An optional override for the JSON.parse function used to hydrate
* incoming messages to this server. Useful for parsing custom datatypes
* out of the incoming JSON.
*/
jsonMessageReviver?: JSONMessageReviver;
/**
* An optional override for the JSON.stringify function used to serialize
* outgoing messages to from server. Useful for serializing custom
* datatypes out to the client.
*/
jsonMessageReplacer?: JSONMessageReplacer;
}
/** @category Server */
export interface Server<E = undefined> {
/**
* New socket has been established. The lib will validate
* the protocol and use the socket accordingly. Returned promise
* will resolve after the socket closes.
*
* 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.
*
* Returns a function that should be called when the same socket
* has been closed, for whatever reason. The close code and reason
* must be passed for reporting to the `onDisconnect` callback. Returned
* promise will resolve once the internal cleanup is complete.
*/
opened(socket: WebSocket, ctxExtra: E): (code: number, reason: string) => Promise<void>;
}
/** @category Server */
export interface WebSocket {
/**
* The subprotocol of the WebSocket. Will be used
* to validate against the supported ones.
*/
readonly protocol: string;
/**
* 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).
*/
send(data: string): Promise<void> | void;
/**
* Closes the socket gracefully. Will always provide
* the appropriate code and close reason. `onDisconnect`
* callback will be called.
*
* The returned promise is used to control the graceful
* closure.
*/
close(code: number, reason: 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 emitted (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.
*/
onMessage(cb: (data: string) => Promise<void>): void;
/**
* Implement a listener for the `PingMessage` sent from the client to the server.
* If the client sent the ping with a payload, it will be passed through the
* first argument.
*
* If this listener is implemented, the server will NOT automatically reply
* to any pings from the client. Implementing it makes it your responsibility
* to decide how and when to respond.
*/
onPing?(payload: PingMessage['payload']): Promise<void> | void;
/**
* Implement a listener for the `PongMessage` sent from the client to the server.
* If the client sent the pong with a payload, it will be passed through the
* first argument.
*/
onPong?(payload: PongMessage['payload']): Promise<void> | void;
}
/** @category Server */
export interface Context<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown> {
/**
* Indicates that the `ConnectionInit` message
* has been received by the server. If this is
* `true`, the client wont be kicked off after
* the wait timeout has passed.
*/
readonly connectionInitReceived: boolean;
/**
* Indicates that the connection was acknowledged
* by having dispatched the `ConnectionAck` message
* to the related client.
*/
readonly acknowledged: boolean;
/** The parameters passed during the connection initialisation. */
readonly connectionParams?: Readonly<P>;
/**
* Holds the active subscriptions for this context. **All operations**
* that are taking place are aggregated here. The user is _subscribed_
* to an operation when waiting for result(s).
*
* If the subscription behind an ID is an `AsyncIterator` - the operation
* is streaming; on the contrary, if the subscription is `null` - it is simply
* a reservation, meaning - the operation resolves to a single result or is still
* pending/being prepared.
*/
readonly subscriptions: Record<ID, AsyncGenerator<unknown> | AsyncIterable<unknown> | null>;
/**
* An extra field where you can store your own context values
* to pass between callbacks.
*/
extra: E;
}
/**
* Makes a Protocol compliant WebSocket GraphQL server. The server
* is actually an API which is to be used with your favourite WebSocket
* server library!
*
* Read more about the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Server
*/
export declare function makeServer<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown>(options: ServerOptions<P, E>): Server<E>;
/**
* Helper utility for choosing the "graphql-transport-ws" subprotocol from
* a set of WebSocket subprotocols.
*
* Accepts a set of already extracted WebSocket subprotocols or the raw
* Sec-WebSocket-Protocol header value. In either case, if the right
* protocol appears, it will be returned.
*
* By specification, the server should not provide a value with Sec-WebSocket-Protocol
* if it does not agree with client's subprotocols. The client has a responsibility
* to handle the connection afterwards.
*
* @category Server
*/
export declare function handleProtocols(protocols: Set<string> | string[] | string): typeof GRAPHQL_TRANSPORT_WS_PROTOCOL | false;

View File

@@ -0,0 +1,417 @@
/**
*
* server
*
*/
import { OperationTypeNode, GraphQLSchema, ExecutionArgs, validate as graphqlValidate, GraphQLError, SubscriptionArgs } from 'graphql';
import { GRAPHQL_TRANSPORT_WS_PROTOCOL, ID, ConnectionInitMessage, SubscribeMessage, NextMessage, ErrorMessage, CompleteMessage, JSONMessageReplacer, JSONMessageReviver, PingMessage, PongMessage, ExecutionResult, ExecutionPatchResult } from './common';
/** @category Server */
export type OperationResult = Promise<AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult> | AsyncGenerator<ExecutionResult | ExecutionPatchResult> | AsyncIterable<ExecutionResult | ExecutionPatchResult> | ExecutionResult;
/**
* A concrete GraphQL execution context value type.
*
* Mainly used because TypeScript collapses unions
* with `any` or `unknown` to `any` or `unknown`. So,
* we use a custom type to allow definitions such as
* the `context` server option.
*
* @category Server
*/
export type GraphQLExecutionContextValue = object | symbol | number | string | boolean | undefined | null;
/** @category Server */
export interface ServerOptions<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown> {
/**
* The GraphQL schema on which the operations
* will be executed and validated against.
*
* If a function is provided, it will be called on
* every subscription request allowing you to manipulate
* schema dynamically.
*
* If the schema is left undefined, you're trusted to
* provide one in the returned `ExecutionArgs` from the
* `onSubscribe` callback.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
schema?: GraphQLSchema | ((ctx: Context<P, E>, message: SubscribeMessage, args: Omit<ExecutionArgs, 'schema'>) => Promise<GraphQLSchema> | GraphQLSchema);
/**
* A value which is provided to every resolver and holds
* important contextual information like the currently
* logged in user, or access to a database.
*
* If you return from `onSubscribe`, and the returned value is
* missing the `contextValue` field, this context will be used
* instead.
*
* If you use the function signature, the final execution arguments
* will be passed in (also the returned value from `onSubscribe`).
* Since the context is injected on every subscribe, the `SubscribeMessage`
* with the regular `Context` will be passed in through the arguments too.
*
* Note that the context function is invoked on each operation only once.
* Meaning, for subscriptions, only at the point of initialising the subscription;
* not on every subscription event emission. Read more about the context lifecycle
* in subscriptions here: https://github.com/graphql/graphql-js/issues/894.
*/
context?: GraphQLExecutionContextValue | ((ctx: Context<P, E>, message: SubscribeMessage, args: ExecutionArgs) => Promise<GraphQLExecutionContextValue> | GraphQLExecutionContextValue);
/**
* The GraphQL root fields or resolvers to go
* alongside the schema. Learn more about them
* here: https://graphql.org/learn/execution/#root-fields-resolvers.
*
* If you return from `onSubscribe`, and the returned value is
* missing the `rootValue` field, the relevant operation root
* will be used instead.
*/
roots?: {
[operation in OperationTypeNode]?: Record<string, NonNullable<SubscriptionArgs['rootValue']>>;
};
/**
* A custom GraphQL validate function allowing you to apply your
* own validation rules.
*
* Returned, non-empty, array of `GraphQLError`s will be communicated
* to the client through the `ErrorMessage`. Use an empty array if the
* document is valid and no errors have been encountered.
*
* Will not be used when implementing a custom `onSubscribe`.
*
* Throwing an error from within this function will close the socket
* with the `Error` message in the close event reason.
*/
validate?: typeof graphqlValidate;
/**
* Is the `execute` function from GraphQL which is
* used to execute the query and mutation operations.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
execute?: (args: ExecutionArgs) => OperationResult;
/**
* Is the `subscribe` function from GraphQL which is
* used to execute the subscription operation.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
subscribe?: (args: ExecutionArgs) => OperationResult;
/**
* The amount of time for which the server will wait
* for `ConnectionInit` message.
*
* Set the value to `Infinity`, `''`, `0`, `null` or `undefined` to skip waiting.
*
* If the wait timeout has passed and the client
* has not sent the `ConnectionInit` message,
* the server will terminate the socket by
* dispatching a close event `4408: Connection initialisation timeout`
*
* @default 3_000 // 3 seconds
*/
connectionInitWaitTimeout?: number;
/**
* Is the connection callback called when the
* client requests the connection initialisation
* through the message `ConnectionInit`.
*
* The message payload (`connectionParams` from the
* client) is present in the `Context.connectionParams`.
*
* - Returning `true` or nothing from the callback will
* allow the client to connect.
*
* - Returning `false` from the callback will
* terminate the socket by dispatching the
* close event `4403: Forbidden`.
*
* - Returning a `Record` from the callback will
* allow the client to connect and pass the returned
* value to the client through the optional `payload`
* field in the `ConnectionAck` message.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onConnect?: (ctx: Context<P, E>) => Promise<Record<string, unknown> | boolean | void> | Record<string, unknown> | boolean | void;
/**
* Called when the client disconnects for whatever reason after
* he successfully went through the connection initialisation phase.
* Provides the close event too. Beware that this callback happens
* AFTER all subscriptions have been gracefully completed and BEFORE
* the `onClose` callback.
*
* If you are interested in tracking the subscriptions completions,
* consider using the `onComplete` callback.
*
* This callback will be called EXCLUSIVELY if the client connection
* is acknowledged. Meaning, `onConnect` will be called before the `onDisconnect`.
*
* For tracking socket closures at any point in time, regardless
* of the connection state - consider using the `onClose` callback.
*/
onDisconnect?: (ctx: Context<P, E>, code: number, reason: string) => Promise<void> | void;
/**
* Called when the socket closes for whatever reason, at any
* point in time. Provides the close event too. Beware
* that this callback happens AFTER all subscriptions have
* been gracefully completed and AFTER the `onDisconnect` callback.
*
* If you are interested in tracking the subscriptions completions,
* consider using the `onComplete` callback.
*
* In comparison to `onDisconnect`, this callback will ALWAYS
* be called, regardless if the user successfully went through
* the connection initialisation or not. `onConnect` might not
* called before the `onClose`.
*/
onClose?: (ctx: Context<P, E>, code: number, reason: string) => Promise<void> | void;
/**
* The subscribe callback executed right after
* acknowledging the request before any payload
* processing has been performed.
*
* If you return `ExecutionArgs` from the callback,
* it will be used instead of trying to build one
* internally. In this case, you are responsible
* for providing a ready set of arguments which will
* be directly plugged in the operation execution.
*
* Omitting the fields `contextValue` or `rootValue`
* from the returned value will have the provided server
* options fill in the gaps.
*
* To report GraphQL errors simply return an array
* of them from the callback, they will be reported
* to the client through the error message.
*
* Useful for preparing the execution arguments
* following a custom logic. A typical use case are
* persisted queries, you can identify the query from
* the subscribe message and create the GraphQL operation
* execution args which are then returned by the function.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onSubscribe?: (ctx: Context<P, E>, message: SubscribeMessage) => Promise<ExecutionArgs | readonly GraphQLError[] | void> | ExecutionArgs | readonly GraphQLError[] | void;
/**
* Executed after the operation call resolves. For streaming
* operations, triggering this callback does not necessarily
* mean that there is already a result available - it means
* that the subscription process for the stream has resolved
* and that the client is now subscribed.
*
* The `OperationResult` argument is the result of operation
* execution. It can be an iterator or already a value.
*
* If you want the single result and the events from a streaming
* operation, use the `onNext` callback.
*
* Use this callback to listen for subscribe operation and
* execution result manipulation.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onOperation?: (ctx: Context<P, E>, message: SubscribeMessage, args: ExecutionArgs, result: OperationResult) => Promise<OperationResult | void> | OperationResult | void;
/**
* Executed after an error occurred right before it
* has been dispatched to the client.
*
* Use this callback to format the outgoing GraphQL
* errors before they reach the client.
*
* Returned result will be injected in the error message payload.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onError?: (ctx: Context<P, E>, message: ErrorMessage, errors: readonly GraphQLError[]) => Promise<readonly GraphQLError[] | void> | readonly GraphQLError[] | void;
/**
* Executed after an operation has emitted a result right before
* that result has been sent to the client. Results from both
* single value and streaming operations will appear in this callback.
*
* Use this callback if you want to format the execution result
* before it reaches the client.
*
* Returned result will be injected in the next message payload.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*/
onNext?: (ctx: Context<P, E>, message: NextMessage, args: ExecutionArgs, result: ExecutionResult | ExecutionPatchResult) => Promise<ExecutionResult | ExecutionPatchResult | void> | ExecutionResult | ExecutionPatchResult | void;
/**
* The complete callback is executed after the
* operation has completed right before sending
* the complete message to the client.
*
* Throwing an error from within this function will
* close the socket with the `Error` message
* in the close event reason.
*
* Since the library makes sure to complete streaming
* operations even after an abrupt closure, this callback
* will still be called.
*/
onComplete?: (ctx: Context<P, E>, message: CompleteMessage) => Promise<void> | void;
/**
* An optional override for the JSON.parse function used to hydrate
* incoming messages to this server. Useful for parsing custom datatypes
* out of the incoming JSON.
*/
jsonMessageReviver?: JSONMessageReviver;
/**
* An optional override for the JSON.stringify function used to serialize
* outgoing messages to from server. Useful for serializing custom
* datatypes out to the client.
*/
jsonMessageReplacer?: JSONMessageReplacer;
}
/** @category Server */
export interface Server<E = undefined> {
/**
* New socket has been established. The lib will validate
* the protocol and use the socket accordingly. Returned promise
* will resolve after the socket closes.
*
* 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.
*
* Returns a function that should be called when the same socket
* has been closed, for whatever reason. The close code and reason
* must be passed for reporting to the `onDisconnect` callback. Returned
* promise will resolve once the internal cleanup is complete.
*/
opened(socket: WebSocket, ctxExtra: E): (code: number, reason: string) => Promise<void>;
}
/** @category Server */
export interface WebSocket {
/**
* The subprotocol of the WebSocket. Will be used
* to validate against the supported ones.
*/
readonly protocol: string;
/**
* 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).
*/
send(data: string): Promise<void> | void;
/**
* Closes the socket gracefully. Will always provide
* the appropriate code and close reason. `onDisconnect`
* callback will be called.
*
* The returned promise is used to control the graceful
* closure.
*/
close(code: number, reason: 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 emitted (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.
*/
onMessage(cb: (data: string) => Promise<void>): void;
/**
* Implement a listener for the `PingMessage` sent from the client to the server.
* If the client sent the ping with a payload, it will be passed through the
* first argument.
*
* If this listener is implemented, the server will NOT automatically reply
* to any pings from the client. Implementing it makes it your responsibility
* to decide how and when to respond.
*/
onPing?(payload: PingMessage['payload']): Promise<void> | void;
/**
* Implement a listener for the `PongMessage` sent from the client to the server.
* If the client sent the pong with a payload, it will be passed through the
* first argument.
*/
onPong?(payload: PongMessage['payload']): Promise<void> | void;
}
/** @category Server */
export interface Context<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown> {
/**
* Indicates that the `ConnectionInit` message
* has been received by the server. If this is
* `true`, the client wont be kicked off after
* the wait timeout has passed.
*/
readonly connectionInitReceived: boolean;
/**
* Indicates that the connection was acknowledged
* by having dispatched the `ConnectionAck` message
* to the related client.
*/
readonly acknowledged: boolean;
/** The parameters passed during the connection initialisation. */
readonly connectionParams?: Readonly<P>;
/**
* Holds the active subscriptions for this context. **All operations**
* that are taking place are aggregated here. The user is _subscribed_
* to an operation when waiting for result(s).
*
* If the subscription behind an ID is an `AsyncIterator` - the operation
* is streaming; on the contrary, if the subscription is `null` - it is simply
* a reservation, meaning - the operation resolves to a single result or is still
* pending/being prepared.
*/
readonly subscriptions: Record<ID, AsyncGenerator<unknown> | AsyncIterable<unknown> | null>;
/**
* An extra field where you can store your own context values
* to pass between callbacks.
*/
extra: E;
}
/**
* Makes a Protocol compliant WebSocket GraphQL server. The server
* is actually an API which is to be used with your favourite WebSocket
* server library!
*
* Read more about the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Server
*/
export declare function makeServer<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E = unknown>(options: ServerOptions<P, E>): Server<E>;
/**
* Helper utility for choosing the "graphql-transport-ws" subprotocol from
* a set of WebSocket subprotocols.
*
* Accepts a set of already extracted WebSocket subprotocols or the raw
* Sec-WebSocket-Protocol header value. In either case, if the right
* protocol appears, it will be returned.
*
* By specification, the server should not provide a value with Sec-WebSocket-Protocol
* if it does not agree with client's subprotocols. The client has a responsibility
* to handle the connection afterwards.
*
* @category Server
*/
export declare function handleProtocols(protocols: Set<string> | string[] | string): typeof GRAPHQL_TRANSPORT_WS_PROTOCOL | false;

View File

@@ -0,0 +1,301 @@
"use strict";
/**
*
* server
*
*/
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleProtocols = exports.makeServer = void 0;
const graphql_1 = require("graphql");
const common_1 = require("./common");
const utils_1 = require("./utils");
/**
* Makes a Protocol compliant WebSocket GraphQL server. The server
* is actually an API which is to be used with your favourite WebSocket
* server library!
*
* Read more about the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Server
*/
function makeServer(options) {
const { schema, context, roots, validate, execute, subscribe, connectionInitWaitTimeout = 3000, // 3 seconds
onConnect, onDisconnect, onClose, onSubscribe, onOperation, onNext, onError, onComplete, jsonMessageReviver: reviver, jsonMessageReplacer: replacer, } = options;
return {
opened(socket, extra) {
const ctx = {
connectionInitReceived: false,
acknowledged: false,
subscriptions: {},
extra,
};
if (socket.protocol !== common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL) {
socket.close(common_1.CloseCode.SubprotocolNotAcceptable, 'Subprotocol not acceptable');
return async (code, reason) => {
/* nothing was set up, just notify the closure */
await (onClose === null || onClose === void 0 ? void 0 : onClose(ctx, code, reason));
};
}
// kick the client off (close socket) if the connection has
// not been initialised after the specified wait timeout
const connectionInitWait = connectionInitWaitTimeout > 0 && isFinite(connectionInitWaitTimeout)
? setTimeout(() => {
if (!ctx.connectionInitReceived)
socket.close(common_1.CloseCode.ConnectionInitialisationTimeout, 'Connection initialisation timeout');
}, connectionInitWaitTimeout)
: null;
socket.onMessage(async function onMessage(data) {
var _a, e_1, _b, _c;
var _d;
let message;
try {
message = (0, common_1.parseMessage)(data, reviver);
}
catch (err) {
return socket.close(common_1.CloseCode.BadRequest, 'Invalid message received');
}
switch (message.type) {
case common_1.MessageType.ConnectionInit: {
if (ctx.connectionInitReceived)
return socket.close(common_1.CloseCode.TooManyInitialisationRequests, 'Too many initialisation requests');
// @ts-expect-error: I can write
ctx.connectionInitReceived = true;
if ((0, utils_1.isObject)(message.payload))
// @ts-expect-error: I can write
ctx.connectionParams = message.payload;
const permittedOrPayload = await (onConnect === null || onConnect === void 0 ? void 0 : onConnect(ctx));
if (permittedOrPayload === false)
return socket.close(common_1.CloseCode.Forbidden, 'Forbidden');
// we should acknowledge before send to avoid race conditions (like those exampled in https://github.com/enisdenjo/graphql-ws/issues/501)
// even if the send fails/throws, the connection should be closed because its malfunctioning
// @ts-expect-error: I can write
ctx.acknowledged = true;
await socket.send((0, common_1.stringifyMessage)((0, utils_1.isObject)(permittedOrPayload)
? {
type: common_1.MessageType.ConnectionAck,
payload: permittedOrPayload,
}
: {
type: common_1.MessageType.ConnectionAck,
// payload is completely absent if not provided
}, replacer));
return;
}
case common_1.MessageType.Ping: {
if (socket.onPing)
// if the onPing listener is registered, automatic pong is disabled
return await socket.onPing(message.payload);
await socket.send((0, common_1.stringifyMessage)(message.payload
? { type: common_1.MessageType.Pong, payload: message.payload }
: {
type: common_1.MessageType.Pong,
// payload is completely absent if not provided
}));
return;
}
case common_1.MessageType.Pong:
return await ((_d = socket.onPong) === null || _d === void 0 ? void 0 : _d.call(socket, message.payload));
case common_1.MessageType.Subscribe: {
if (!ctx.acknowledged)
return socket.close(common_1.CloseCode.Unauthorized, 'Unauthorized');
const { id, payload } = message;
if (id in ctx.subscriptions)
return socket.close(common_1.CloseCode.SubscriberAlreadyExists, `Subscriber for ${id} already exists`);
// if this turns out to be a streaming operation, the subscription value
// will change to an `AsyncIterable`, otherwise it will stay as is
ctx.subscriptions[id] = null;
const emit = {
next: async (result, args) => {
let nextMessage = {
id,
type: common_1.MessageType.Next,
payload: result,
};
const maybeResult = await (onNext === null || onNext === void 0 ? void 0 : onNext(ctx, nextMessage, args, result));
if (maybeResult)
nextMessage = Object.assign(Object.assign({}, nextMessage), { payload: maybeResult });
await socket.send((0, common_1.stringifyMessage)(nextMessage, replacer));
},
error: async (errors) => {
let errorMessage = {
id,
type: common_1.MessageType.Error,
payload: errors,
};
const maybeErrors = await (onError === null || onError === void 0 ? void 0 : onError(ctx, errorMessage, errors));
if (maybeErrors)
errorMessage = Object.assign(Object.assign({}, errorMessage), { payload: maybeErrors });
await socket.send((0, common_1.stringifyMessage)(errorMessage, replacer));
},
complete: async (notifyClient) => {
const completeMessage = {
id,
type: common_1.MessageType.Complete,
};
await (onComplete === null || onComplete === void 0 ? void 0 : onComplete(ctx, completeMessage));
if (notifyClient)
await socket.send((0, common_1.stringifyMessage)(completeMessage, replacer));
},
};
try {
let execArgs;
const maybeExecArgsOrErrors = await (onSubscribe === null || onSubscribe === void 0 ? void 0 : onSubscribe(ctx, message));
if (maybeExecArgsOrErrors) {
if ((0, utils_1.areGraphQLErrors)(maybeExecArgsOrErrors))
return await emit.error(maybeExecArgsOrErrors);
else if (Array.isArray(maybeExecArgsOrErrors))
throw new Error('Invalid return value from onSubscribe hook, expected an array of GraphQLError objects');
// not errors, is exec args
execArgs = maybeExecArgsOrErrors;
}
else {
// you either provide a schema dynamically through
// `onSubscribe` or you set one up during the server setup
if (!schema)
throw new Error('The GraphQL schema is not provided');
const args = {
operationName: payload.operationName,
document: (0, graphql_1.parse)(payload.query),
variableValues: payload.variables,
};
execArgs = Object.assign(Object.assign({}, args), { schema: typeof schema === 'function'
? await schema(ctx, message, args)
: schema });
const validationErrors = (validate !== null && validate !== void 0 ? validate : graphql_1.validate)(execArgs.schema, execArgs.document);
if (validationErrors.length > 0)
return await emit.error(validationErrors);
}
const operationAST = (0, graphql_1.getOperationAST)(execArgs.document, execArgs.operationName);
if (!operationAST)
return await emit.error([
new graphql_1.GraphQLError('Unable to identify operation'),
]);
// if `onSubscribe` didn't specify a rootValue, inject one
if (!('rootValue' in execArgs))
execArgs.rootValue = roots === null || roots === void 0 ? void 0 : roots[operationAST.operation];
// if `onSubscribe` didn't specify a context, inject one
if (!('contextValue' in execArgs))
execArgs.contextValue =
typeof context === 'function'
? await context(ctx, message, execArgs)
: context;
// the execution arguments have been prepared
// perform the operation and act accordingly
let operationResult;
if (operationAST.operation === 'subscription')
operationResult = await (subscribe !== null && subscribe !== void 0 ? subscribe : graphql_1.subscribe)(execArgs);
// operation === 'query' || 'mutation'
else
operationResult = await (execute !== null && execute !== void 0 ? execute : graphql_1.execute)(execArgs);
const maybeResult = await (onOperation === null || onOperation === void 0 ? void 0 : onOperation(ctx, message, execArgs, operationResult));
if (maybeResult)
operationResult = maybeResult;
if ((0, utils_1.isAsyncIterable)(operationResult)) {
/** multiple emitted results */
if (!(id in ctx.subscriptions)) {
// subscription was completed/canceled before the operation settled
if ((0, utils_1.isAsyncGenerator)(operationResult))
operationResult.return(undefined);
}
else {
ctx.subscriptions[id] = operationResult;
try {
for (var _e = true, operationResult_1 = __asyncValues(operationResult), operationResult_1_1; operationResult_1_1 = await operationResult_1.next(), _a = operationResult_1_1.done, !_a; _e = true) {
_c = operationResult_1_1.value;
_e = false;
const result = _c;
await emit.next(result, execArgs);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_e && !_a && (_b = operationResult_1.return)) await _b.call(operationResult_1);
}
finally { if (e_1) throw e_1.error; }
}
}
}
else {
/** single emitted result */
// if the client completed the subscription before the single result
// became available, he effectively canceled it and no data should be sent
if (id in ctx.subscriptions)
await emit.next(operationResult, execArgs);
}
// lack of subscription at this point indicates that the client
// completed the subscription, he doesn't need to be reminded
await emit.complete(id in ctx.subscriptions);
}
finally {
// whatever happens to the subscription, we finally want to get rid of the reservation
delete ctx.subscriptions[id];
}
return;
}
case common_1.MessageType.Complete: {
const subscription = ctx.subscriptions[message.id];
delete ctx.subscriptions[message.id]; // deleting the subscription means no further activity should take place
if ((0, utils_1.isAsyncGenerator)(subscription))
await subscription.return(undefined);
return;
}
default:
throw new Error(`Unexpected message of type ${message.type} received`);
}
});
// wait for close, cleanup and the disconnect callback
return async (code, reason) => {
if (connectionInitWait)
clearTimeout(connectionInitWait);
for (const [id, sub] of Object.entries(ctx.subscriptions)) {
if ((0, utils_1.isAsyncGenerator)(sub))
await sub.return(undefined);
delete ctx.subscriptions[id]; // deleting the subscription means no further activity should take place
}
if (ctx.acknowledged)
await (onDisconnect === null || onDisconnect === void 0 ? void 0 : onDisconnect(ctx, code, reason));
await (onClose === null || onClose === void 0 ? void 0 : onClose(ctx, code, reason));
};
},
};
}
exports.makeServer = makeServer;
/**
* Helper utility for choosing the "graphql-transport-ws" subprotocol from
* a set of WebSocket subprotocols.
*
* Accepts a set of already extracted WebSocket subprotocols or the raw
* Sec-WebSocket-Protocol header value. In either case, if the right
* protocol appears, it will be returned.
*
* By specification, the server should not provide a value with Sec-WebSocket-Protocol
* if it does not agree with client's subprotocols. The client has a responsibility
* to handle the connection afterwards.
*
* @category Server
*/
function handleProtocols(protocols) {
switch (true) {
case protocols instanceof Set &&
protocols.has(common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL):
case Array.isArray(protocols) &&
protocols.includes(common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL):
case typeof protocols === 'string' &&
protocols
.split(',')
.map((p) => p.trim())
.includes(common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL):
return common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL;
default:
return false;
}
}
exports.handleProtocols = handleProtocols;

View File

@@ -0,0 +1,296 @@
/**
*
* server
*
*/
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
import { parse, validate as graphqlValidate, execute as graphqlExecute, subscribe as graphqlSubscribe, getOperationAST, GraphQLError, } from 'graphql';
import { GRAPHQL_TRANSPORT_WS_PROTOCOL, CloseCode, MessageType, stringifyMessage, parseMessage, } from './common.mjs';
import { isObject, isAsyncGenerator, isAsyncIterable, areGraphQLErrors, } from './utils.mjs';
/**
* Makes a Protocol compliant WebSocket GraphQL server. The server
* is actually an API which is to be used with your favourite WebSocket
* server library!
*
* Read more about the [GraphQL over WebSocket Protocol](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverWebSocket.md).
*
* @category Server
*/
export function makeServer(options) {
const { schema, context, roots, validate, execute, subscribe, connectionInitWaitTimeout = 3000, // 3 seconds
onConnect, onDisconnect, onClose, onSubscribe, onOperation, onNext, onError, onComplete, jsonMessageReviver: reviver, jsonMessageReplacer: replacer, } = options;
return {
opened(socket, extra) {
const ctx = {
connectionInitReceived: false,
acknowledged: false,
subscriptions: {},
extra,
};
if (socket.protocol !== GRAPHQL_TRANSPORT_WS_PROTOCOL) {
socket.close(CloseCode.SubprotocolNotAcceptable, 'Subprotocol not acceptable');
return async (code, reason) => {
/* nothing was set up, just notify the closure */
await (onClose === null || onClose === void 0 ? void 0 : onClose(ctx, code, reason));
};
}
// kick the client off (close socket) if the connection has
// not been initialised after the specified wait timeout
const connectionInitWait = connectionInitWaitTimeout > 0 && isFinite(connectionInitWaitTimeout)
? setTimeout(() => {
if (!ctx.connectionInitReceived)
socket.close(CloseCode.ConnectionInitialisationTimeout, 'Connection initialisation timeout');
}, connectionInitWaitTimeout)
: null;
socket.onMessage(async function onMessage(data) {
var _a, e_1, _b, _c;
var _d;
let message;
try {
message = parseMessage(data, reviver);
}
catch (err) {
return socket.close(CloseCode.BadRequest, 'Invalid message received');
}
switch (message.type) {
case MessageType.ConnectionInit: {
if (ctx.connectionInitReceived)
return socket.close(CloseCode.TooManyInitialisationRequests, 'Too many initialisation requests');
// @ts-expect-error: I can write
ctx.connectionInitReceived = true;
if (isObject(message.payload))
// @ts-expect-error: I can write
ctx.connectionParams = message.payload;
const permittedOrPayload = await (onConnect === null || onConnect === void 0 ? void 0 : onConnect(ctx));
if (permittedOrPayload === false)
return socket.close(CloseCode.Forbidden, 'Forbidden');
// we should acknowledge before send to avoid race conditions (like those exampled in https://github.com/enisdenjo/graphql-ws/issues/501)
// even if the send fails/throws, the connection should be closed because its malfunctioning
// @ts-expect-error: I can write
ctx.acknowledged = true;
await socket.send(stringifyMessage(isObject(permittedOrPayload)
? {
type: MessageType.ConnectionAck,
payload: permittedOrPayload,
}
: {
type: MessageType.ConnectionAck,
// payload is completely absent if not provided
}, replacer));
return;
}
case MessageType.Ping: {
if (socket.onPing)
// if the onPing listener is registered, automatic pong is disabled
return await socket.onPing(message.payload);
await socket.send(stringifyMessage(message.payload
? { type: MessageType.Pong, payload: message.payload }
: {
type: MessageType.Pong,
// payload is completely absent if not provided
}));
return;
}
case MessageType.Pong:
return await ((_d = socket.onPong) === null || _d === void 0 ? void 0 : _d.call(socket, message.payload));
case MessageType.Subscribe: {
if (!ctx.acknowledged)
return socket.close(CloseCode.Unauthorized, 'Unauthorized');
const { id, payload } = message;
if (id in ctx.subscriptions)
return socket.close(CloseCode.SubscriberAlreadyExists, `Subscriber for ${id} already exists`);
// if this turns out to be a streaming operation, the subscription value
// will change to an `AsyncIterable`, otherwise it will stay as is
ctx.subscriptions[id] = null;
const emit = {
next: async (result, args) => {
let nextMessage = {
id,
type: MessageType.Next,
payload: result,
};
const maybeResult = await (onNext === null || onNext === void 0 ? void 0 : onNext(ctx, nextMessage, args, result));
if (maybeResult)
nextMessage = Object.assign(Object.assign({}, nextMessage), { payload: maybeResult });
await socket.send(stringifyMessage(nextMessage, replacer));
},
error: async (errors) => {
let errorMessage = {
id,
type: MessageType.Error,
payload: errors,
};
const maybeErrors = await (onError === null || onError === void 0 ? void 0 : onError(ctx, errorMessage, errors));
if (maybeErrors)
errorMessage = Object.assign(Object.assign({}, errorMessage), { payload: maybeErrors });
await socket.send(stringifyMessage(errorMessage, replacer));
},
complete: async (notifyClient) => {
const completeMessage = {
id,
type: MessageType.Complete,
};
await (onComplete === null || onComplete === void 0 ? void 0 : onComplete(ctx, completeMessage));
if (notifyClient)
await socket.send(stringifyMessage(completeMessage, replacer));
},
};
try {
let execArgs;
const maybeExecArgsOrErrors = await (onSubscribe === null || onSubscribe === void 0 ? void 0 : onSubscribe(ctx, message));
if (maybeExecArgsOrErrors) {
if (areGraphQLErrors(maybeExecArgsOrErrors))
return await emit.error(maybeExecArgsOrErrors);
else if (Array.isArray(maybeExecArgsOrErrors))
throw new Error('Invalid return value from onSubscribe hook, expected an array of GraphQLError objects');
// not errors, is exec args
execArgs = maybeExecArgsOrErrors;
}
else {
// you either provide a schema dynamically through
// `onSubscribe` or you set one up during the server setup
if (!schema)
throw new Error('The GraphQL schema is not provided');
const args = {
operationName: payload.operationName,
document: parse(payload.query),
variableValues: payload.variables,
};
execArgs = Object.assign(Object.assign({}, args), { schema: typeof schema === 'function'
? await schema(ctx, message, args)
: schema });
const validationErrors = (validate !== null && validate !== void 0 ? validate : graphqlValidate)(execArgs.schema, execArgs.document);
if (validationErrors.length > 0)
return await emit.error(validationErrors);
}
const operationAST = getOperationAST(execArgs.document, execArgs.operationName);
if (!operationAST)
return await emit.error([
new GraphQLError('Unable to identify operation'),
]);
// if `onSubscribe` didn't specify a rootValue, inject one
if (!('rootValue' in execArgs))
execArgs.rootValue = roots === null || roots === void 0 ? void 0 : roots[operationAST.operation];
// if `onSubscribe` didn't specify a context, inject one
if (!('contextValue' in execArgs))
execArgs.contextValue =
typeof context === 'function'
? await context(ctx, message, execArgs)
: context;
// the execution arguments have been prepared
// perform the operation and act accordingly
let operationResult;
if (operationAST.operation === 'subscription')
operationResult = await (subscribe !== null && subscribe !== void 0 ? subscribe : graphqlSubscribe)(execArgs);
// operation === 'query' || 'mutation'
else
operationResult = await (execute !== null && execute !== void 0 ? execute : graphqlExecute)(execArgs);
const maybeResult = await (onOperation === null || onOperation === void 0 ? void 0 : onOperation(ctx, message, execArgs, operationResult));
if (maybeResult)
operationResult = maybeResult;
if (isAsyncIterable(operationResult)) {
/** multiple emitted results */
if (!(id in ctx.subscriptions)) {
// subscription was completed/canceled before the operation settled
if (isAsyncGenerator(operationResult))
operationResult.return(undefined);
}
else {
ctx.subscriptions[id] = operationResult;
try {
for (var _e = true, operationResult_1 = __asyncValues(operationResult), operationResult_1_1; operationResult_1_1 = await operationResult_1.next(), _a = operationResult_1_1.done, !_a; _e = true) {
_c = operationResult_1_1.value;
_e = false;
const result = _c;
await emit.next(result, execArgs);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_e && !_a && (_b = operationResult_1.return)) await _b.call(operationResult_1);
}
finally { if (e_1) throw e_1.error; }
}
}
}
else {
/** single emitted result */
// if the client completed the subscription before the single result
// became available, he effectively canceled it and no data should be sent
if (id in ctx.subscriptions)
await emit.next(operationResult, execArgs);
}
// lack of subscription at this point indicates that the client
// completed the subscription, he doesn't need to be reminded
await emit.complete(id in ctx.subscriptions);
}
finally {
// whatever happens to the subscription, we finally want to get rid of the reservation
delete ctx.subscriptions[id];
}
return;
}
case MessageType.Complete: {
const subscription = ctx.subscriptions[message.id];
delete ctx.subscriptions[message.id]; // deleting the subscription means no further activity should take place
if (isAsyncGenerator(subscription))
await subscription.return(undefined);
return;
}
default:
throw new Error(`Unexpected message of type ${message.type} received`);
}
});
// wait for close, cleanup and the disconnect callback
return async (code, reason) => {
if (connectionInitWait)
clearTimeout(connectionInitWait);
for (const [id, sub] of Object.entries(ctx.subscriptions)) {
if (isAsyncGenerator(sub))
await sub.return(undefined);
delete ctx.subscriptions[id]; // deleting the subscription means no further activity should take place
}
if (ctx.acknowledged)
await (onDisconnect === null || onDisconnect === void 0 ? void 0 : onDisconnect(ctx, code, reason));
await (onClose === null || onClose === void 0 ? void 0 : onClose(ctx, code, reason));
};
},
};
}
/**
* Helper utility for choosing the "graphql-transport-ws" subprotocol from
* a set of WebSocket subprotocols.
*
* Accepts a set of already extracted WebSocket subprotocols or the raw
* Sec-WebSocket-Protocol header value. In either case, if the right
* protocol appears, it will be returned.
*
* By specification, the server should not provide a value with Sec-WebSocket-Protocol
* if it does not agree with client's subprotocols. The client has a responsibility
* to handle the connection afterwards.
*
* @category Server
*/
export function handleProtocols(protocols) {
switch (true) {
case protocols instanceof Set &&
protocols.has(GRAPHQL_TRANSPORT_WS_PROTOCOL):
case Array.isArray(protocols) &&
protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL):
case typeof protocols === 'string' &&
protocols
.split(',')
.map((p) => p.trim())
.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL):
return GRAPHQL_TRANSPORT_WS_PROTOCOL;
default:
return false;
}
}

View File

@@ -0,0 +1,36 @@
import type { FastifyRequest } from 'fastify';
import type * as fastifyWebsocket from '@fastify/websocket';
import { ServerOptions } from '../../server.mjs';
import { ConnectionInitMessage } from '../../common.mjs';
/**
* The extra that will be put in the `Context`.
*
* @category Server/@fastify/websocket
*/
export interface Extra {
/**
* The underlying socket connection between the server and the client.
* The WebSocket socket is located under the `socket` parameter.
*/
readonly connection: fastifyWebsocket.SocketStream;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: FastifyRequest;
}
/**
* Make a handler to use on a [@fastify/websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/@fastify/websocket
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): fastifyWebsocket.WebsocketHandler;

View File

@@ -0,0 +1,36 @@
import type { FastifyRequest } from 'fastify';
import type * as fastifyWebsocket from '@fastify/websocket';
import { ServerOptions } from '../../server';
import { ConnectionInitMessage } from '../../common';
/**
* The extra that will be put in the `Context`.
*
* @category Server/@fastify/websocket
*/
export interface Extra {
/**
* The underlying socket connection between the server and the client.
* The WebSocket socket is located under the `socket` parameter.
*/
readonly connection: fastifyWebsocket.SocketStream;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: FastifyRequest;
}
/**
* Make a handler to use on a [@fastify/websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/@fastify/websocket
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): fastifyWebsocket.WebsocketHandler;

View File

@@ -0,0 +1,126 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeHandler = void 0;
const server_1 = require("../../server");
const common_1 = require("../../common");
const utils_1 = require("../../utils");
/**
* Make a handler to use on a [@fastify/websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/@fastify/websocket
*/
function makeHandler(options,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = (0, server_1.makeServer)(options);
// we dont have access to the fastify-websocket server instance yet,
// register an error handler on first connection ONCE only
let handlingServerEmittedErrors = false;
return function handler(connection, request) {
const { socket } = connection;
// might be too late, but meh
this.websocketServer.options.handleProtocols = server_1.handleProtocols;
// handle server emitted errors only if not already handling
if (!handlingServerEmittedErrors) {
handlingServerEmittedErrors = true;
this.websocketServer.once('error', (err) => {
console.error('Internal error emitted on the WebSocket server. ' +
'Please check your implementation.', err);
// catch the first thrown error and re-throw it once all clients have been notified
let firstErr = null;
// report server errors by erroring out all clients with the same error
for (const client of this.websocketServer.clients) {
try {
client.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
catch (err) {
firstErr = firstErr !== null && firstErr !== void 0 ? firstErr : err;
}
}
if (firstErr)
throw firstErr;
});
}
// used as listener on two streams, prevent superfluous calls on close
let emittedErrorHandled = false;
function handleEmittedError(err) {
if (emittedErrorHandled)
return;
emittedErrorHandled = true;
console.error('Internal error emitted on a WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
// fastify-websocket uses the WebSocket.createWebSocketStream,
// therefore errors get emitted on both the connection and the socket
connection.once('error', handleEmittedError);
socket.once('error', handleEmittedError);
// keep alive through ping-pong messages
let pongWait = null;
const pingInterval = keepAlive > 0 && isFinite(keepAlive)
? setInterval(() => {
// ping pong on open sockets only
if (socket.readyState === socket.OPEN) {
// terminate the connection after pong wait has passed because the client is idle
pongWait = setTimeout(() => {
socket.terminate();
}, keepAlive);
// listen for client's pong and stop socket termination
socket.once('pong', () => {
if (pongWait) {
clearTimeout(pongWait);
pongWait = null;
}
});
socket.ping();
}
}, keepAlive)
: null;
const closed = server.opened({
protocol: socket.protocol,
send: (data) => new Promise((resolve, reject) => {
if (socket.readyState !== socket.OPEN)
return resolve();
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => socket.on('message', async (event) => {
try {
await cb(String(event));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
}),
}, { connection, request });
socket.once('close', (code, reason) => {
if (pongWait)
clearTimeout(pongWait);
if (pingInterval)
clearInterval(pingInterval);
if (!isProd &&
code === common_1.CloseCode.SubprotocolNotAcceptable &&
socket.protocol === common_1.DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(code, String(reason));
});
};
}
exports.makeHandler = makeHandler;

View File

@@ -0,0 +1,122 @@
import { handleProtocols, makeServer } from '../../server.mjs';
import { DEPRECATED_GRAPHQL_WS_PROTOCOL, CloseCode, } from '../../common.mjs';
import { limitCloseReason } from '../../utils.mjs';
/**
* Make a handler to use on a [@fastify/websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/@fastify/websocket
*/
export function makeHandler(options,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = makeServer(options);
// we dont have access to the fastify-websocket server instance yet,
// register an error handler on first connection ONCE only
let handlingServerEmittedErrors = false;
return function handler(connection, request) {
const { socket } = connection;
// might be too late, but meh
this.websocketServer.options.handleProtocols = handleProtocols;
// handle server emitted errors only if not already handling
if (!handlingServerEmittedErrors) {
handlingServerEmittedErrors = true;
this.websocketServer.once('error', (err) => {
console.error('Internal error emitted on the WebSocket server. ' +
'Please check your implementation.', err);
// catch the first thrown error and re-throw it once all clients have been notified
let firstErr = null;
// report server errors by erroring out all clients with the same error
for (const client of this.websocketServer.clients) {
try {
client.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
catch (err) {
firstErr = firstErr !== null && firstErr !== void 0 ? firstErr : err;
}
}
if (firstErr)
throw firstErr;
});
}
// used as listener on two streams, prevent superfluous calls on close
let emittedErrorHandled = false;
function handleEmittedError(err) {
if (emittedErrorHandled)
return;
emittedErrorHandled = true;
console.error('Internal error emitted on a WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
// fastify-websocket uses the WebSocket.createWebSocketStream,
// therefore errors get emitted on both the connection and the socket
connection.once('error', handleEmittedError);
socket.once('error', handleEmittedError);
// keep alive through ping-pong messages
let pongWait = null;
const pingInterval = keepAlive > 0 && isFinite(keepAlive)
? setInterval(() => {
// ping pong on open sockets only
if (socket.readyState === socket.OPEN) {
// terminate the connection after pong wait has passed because the client is idle
pongWait = setTimeout(() => {
socket.terminate();
}, keepAlive);
// listen for client's pong and stop socket termination
socket.once('pong', () => {
if (pongWait) {
clearTimeout(pongWait);
pongWait = null;
}
});
socket.ping();
}
}, keepAlive)
: null;
const closed = server.opened({
protocol: socket.protocol,
send: (data) => new Promise((resolve, reject) => {
if (socket.readyState !== socket.OPEN)
return resolve();
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => socket.on('message', async (event) => {
try {
await cb(String(event));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
}),
}, { connection, request });
socket.once('close', (code, reason) => {
if (pongWait)
clearTimeout(pongWait);
if (pingInterval)
clearInterval(pingInterval);
if (!isProd &&
code === CloseCode.SubprotocolNotAcceptable &&
socket.protocol === DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(code, String(reason));
});
};
}

View File

@@ -0,0 +1,60 @@
/// <reference types="bun-types" />
import type { WebSocketHandler, ServerWebSocket } from 'bun';
import { ConnectionInitMessage } from '../common.mjs';
import { ServerOptions } from '../server.mjs';
/**
* Convenience export for checking the WebSocket protocol on the request in `Bun.serve`.
*/
export { handleProtocols } from '../server.mjs';
/**
* The extra that will be put in the `Context`.
*
* @category Server/bun
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: ServerWebSocket;
}
/**
* Use the server with [Bun](https://bun.sh/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* The WebSocket subprotocol is not available on the established socket and therefore
* needs to be checked during the request handling.
*
* Additionally, the keep-alive logic _seems_ to be handled by Bun seeing that
* they default [`sendPingsAutomatically` to `true`](https://github.com/oven-sh/bun/blob/6a163cf933542506354dc836bd92693bcae5939b/src/deps/uws.zig#L893).
*
* ```ts
* import { makeHandler, handleProtocols } from 'graphql-ws/lib/use/lib/bun';
* import { schema } from './my-schema/index.mjs';
*
* Bun.serve({
* fetch(req, server) {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* if (!handleProtocols(req.headers.get('sec-websocket-protocol') || '')) {
* return new Response('Bad Request', { status: 404 });
* }
* if (!server.upgrade(req)) {
* return new Response('Internal Server Error', { status: 500 });
* }
* return new Response();
* },
* websocket: makeHandler({ schema }),
* port: 4000,
* });
*
* console.log('Listening to port 4000');
* ```
*
* @category Server/bun
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>): WebSocketHandler;

View File

@@ -0,0 +1,60 @@
/// <reference types="bun-types" />
import type { WebSocketHandler, ServerWebSocket } from 'bun';
import { ConnectionInitMessage } from '../common';
import { ServerOptions } from '../server';
/**
* Convenience export for checking the WebSocket protocol on the request in `Bun.serve`.
*/
export { handleProtocols } from '../server';
/**
* The extra that will be put in the `Context`.
*
* @category Server/bun
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: ServerWebSocket;
}
/**
* Use the server with [Bun](https://bun.sh/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* The WebSocket subprotocol is not available on the established socket and therefore
* needs to be checked during the request handling.
*
* Additionally, the keep-alive logic _seems_ to be handled by Bun seeing that
* they default [`sendPingsAutomatically` to `true`](https://github.com/oven-sh/bun/blob/6a163cf933542506354dc836bd92693bcae5939b/src/deps/uws.zig#L893).
*
* ```ts
* import { makeHandler, handleProtocols } from 'graphql-ws/lib/use/lib/bun';
* import { schema } from './my-schema';
*
* Bun.serve({
* fetch(req, server) {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* if (!handleProtocols(req.headers.get('sec-websocket-protocol') || '')) {
* return new Response('Bad Request', { status: 404 });
* }
* if (!server.upgrade(req)) {
* return new Response('Internal Server Error', { status: 500 });
* }
* return new Response();
* },
* websocket: makeHandler({ schema }),
* port: 4000,
* });
*
* console.log('Listening to port 4000');
* ```
*
* @category Server/bun
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>): WebSocketHandler;

View File

@@ -0,0 +1,97 @@
"use strict";
/// <reference types="bun-types" />
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeHandler = exports.handleProtocols = void 0;
const common_1 = require("../common");
const server_1 = require("../server");
/**
* Convenience export for checking the WebSocket protocol on the request in `Bun.serve`.
*/
var server_2 = require("../server");
Object.defineProperty(exports, "handleProtocols", { enumerable: true, get: function () { return server_2.handleProtocols; } });
/**
* Use the server with [Bun](https://bun.sh/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* The WebSocket subprotocol is not available on the established socket and therefore
* needs to be checked during the request handling.
*
* Additionally, the keep-alive logic _seems_ to be handled by Bun seeing that
* they default [`sendPingsAutomatically` to `true`](https://github.com/oven-sh/bun/blob/6a163cf933542506354dc836bd92693bcae5939b/src/deps/uws.zig#L893).
*
* ```ts
* import { makeHandler, handleProtocols } from 'graphql-ws/lib/use/lib/bun';
* import { schema } from './my-schema';
*
* Bun.serve({
* fetch(req, server) {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* if (!handleProtocols(req.headers.get('sec-websocket-protocol') || '')) {
* return new Response('Bad Request', { status: 404 });
* }
* if (!server.upgrade(req)) {
* return new Response('Internal Server Error', { status: 500 });
* }
* return new Response();
* },
* websocket: makeHandler({ schema }),
* port: 4000,
* });
*
* console.log('Listening to port 4000');
* ```
*
* @category Server/bun
*/
function makeHandler(options) {
const server = (0, server_1.makeServer)(options);
const clients = new WeakMap();
return {
open(ws) {
const client = {
handleMessage: () => {
throw new Error('Message received before handler was registered');
},
closed: () => {
throw new Error('Closed before handler was registered');
},
};
client.closed = server.opened({
// TODO: use protocol on socket once Bun exposes it
protocol: common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL,
send: async (message) => {
// ws might have been destroyed in the meantime, send only if exists
if (clients.has(ws)) {
ws.sendText(message);
}
},
close: (code, reason) => {
if (clients.has(ws)) {
ws.close(code, reason);
}
},
onMessage: (cb) => (client.handleMessage = cb),
}, { socket: ws });
clients.set(ws, client);
},
message(ws, message) {
const client = clients.get(ws);
if (!client)
throw new Error('Message received for a missing client');
return client.handleMessage(String(message));
},
close(ws, code, message) {
const client = clients.get(ws);
if (!client)
throw new Error('Closing a missing client');
return client.closed(code, message);
},
};
}
exports.makeHandler = makeHandler;

View File

@@ -0,0 +1,92 @@
/// <reference types="bun-types" />
import { GRAPHQL_TRANSPORT_WS_PROTOCOL, } from '../common.mjs';
import { makeServer } from '../server.mjs';
/**
* Convenience export for checking the WebSocket protocol on the request in `Bun.serve`.
*/
export { handleProtocols } from '../server.mjs';
/**
* Use the server with [Bun](https://bun.sh/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* The WebSocket subprotocol is not available on the established socket and therefore
* needs to be checked during the request handling.
*
* Additionally, the keep-alive logic _seems_ to be handled by Bun seeing that
* they default [`sendPingsAutomatically` to `true`](https://github.com/oven-sh/bun/blob/6a163cf933542506354dc836bd92693bcae5939b/src/deps/uws.zig#L893).
*
* ```ts
* import { makeHandler, handleProtocols } from 'graphql-ws/lib/use/lib/bun';
* import { schema } from './my-schema/index.mjs';
*
* Bun.serve({
* fetch(req, server) {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* if (!handleProtocols(req.headers.get('sec-websocket-protocol') || '')) {
* return new Response('Bad Request', { status: 404 });
* }
* if (!server.upgrade(req)) {
* return new Response('Internal Server Error', { status: 500 });
* }
* return new Response();
* },
* websocket: makeHandler({ schema }),
* port: 4000,
* });
*
* console.log('Listening to port 4000');
* ```
*
* @category Server/bun
*/
export function makeHandler(options) {
const server = makeServer(options);
const clients = new WeakMap();
return {
open(ws) {
const client = {
handleMessage: () => {
throw new Error('Message received before handler was registered');
},
closed: () => {
throw new Error('Closed before handler was registered');
},
};
client.closed = server.opened({
// TODO: use protocol on socket once Bun exposes it
protocol: GRAPHQL_TRANSPORT_WS_PROTOCOL,
send: async (message) => {
// ws might have been destroyed in the meantime, send only if exists
if (clients.has(ws)) {
ws.sendText(message);
}
},
close: (code, reason) => {
if (clients.has(ws)) {
ws.close(code, reason);
}
},
onMessage: (cb) => (client.handleMessage = cb),
}, { socket: ws });
clients.set(ws, client);
},
message(ws, message) {
const client = clients.get(ws);
if (!client)
throw new Error('Message received for a missing client');
return client.handleMessage(String(message));
},
close(ws, code, message) {
const client = clients.get(ws);
if (!client)
throw new Error('Closing a missing client');
return client.closed(code, message);
},
};
}

View File

@@ -0,0 +1,56 @@
import { ServerOptions } from '../server.mjs';
import { ConnectionInitMessage } from '../common.mjs';
export { GRAPHQL_TRANSPORT_WS_PROTOCOL } from '../common.mjs';
/**
* The extra that will be put in the `Context`.
*
* @category Server/deno
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: WebSocket;
}
/**
* Use the server with [Deno](https://deno.com/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs.
*
* The keep-alive is set in `Deno.upgradeWebSocket` during the upgrade.
*
* Additionally, the required WebSocket protocol is also defined during the upgrade,
* the correct example being:
*
* ```ts
* import { serve } from 'https://deno.land/std/http/mod.ts';
* import {
* makeHandler,
* GRAPHQL_TRANSPORT_WS_PROTOCOL,
* } from 'https://esm.sh/graphql-ws/lib/use/deno';
* import { schema } from './my-schema.ts/index.mjs';
*
* const handler = makeHandler({ schema });
*
* serve(
* (req: Request) => {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* const { socket, response } = Deno.upgradeWebSocket(req, {
* protocol: GRAPHQL_TRANSPORT_WS_PROTOCOL,
* idleTimeout: 12_000,
* });
* handler(socket);
* return response;
* },
* { port: 4000 },
* );
* ```
*
* @category Server/deno
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>): (socket: WebSocket) => void;

View File

@@ -0,0 +1,56 @@
import { ServerOptions } from '../server';
import { ConnectionInitMessage } from '../common';
export { GRAPHQL_TRANSPORT_WS_PROTOCOL } from '../common';
/**
* The extra that will be put in the `Context`.
*
* @category Server/deno
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: WebSocket;
}
/**
* Use the server with [Deno](https://deno.com/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs.
*
* The keep-alive is set in `Deno.upgradeWebSocket` during the upgrade.
*
* Additionally, the required WebSocket protocol is also defined during the upgrade,
* the correct example being:
*
* ```ts
* import { serve } from 'https://deno.land/std/http/mod.ts';
* import {
* makeHandler,
* GRAPHQL_TRANSPORT_WS_PROTOCOL,
* } from 'https://esm.sh/graphql-ws/lib/use/deno';
* import { schema } from './my-schema.ts';
*
* const handler = makeHandler({ schema });
*
* serve(
* (req: Request) => {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* const { socket, response } = Deno.upgradeWebSocket(req, {
* protocol: GRAPHQL_TRANSPORT_WS_PROTOCOL,
* idleTimeout: 12_000,
* });
* handler(socket);
* return response;
* },
* { port: 4000 },
* );
* ```
*
* @category Server/deno
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>): (socket: WebSocket) => void;

View File

@@ -0,0 +1,88 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeHandler = exports.GRAPHQL_TRANSPORT_WS_PROTOCOL = void 0;
const server_1 = require("../server");
const common_1 = require("../common");
var common_2 = require("../common");
Object.defineProperty(exports, "GRAPHQL_TRANSPORT_WS_PROTOCOL", { enumerable: true, get: function () { return common_2.GRAPHQL_TRANSPORT_WS_PROTOCOL; } });
/**
* Use the server with [Deno](https://deno.com/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs.
*
* The keep-alive is set in `Deno.upgradeWebSocket` during the upgrade.
*
* Additionally, the required WebSocket protocol is also defined during the upgrade,
* the correct example being:
*
* ```ts
* import { serve } from 'https://deno.land/std/http/mod.ts';
* import {
* makeHandler,
* GRAPHQL_TRANSPORT_WS_PROTOCOL,
* } from 'https://esm.sh/graphql-ws/lib/use/deno';
* import { schema } from './my-schema.ts';
*
* const handler = makeHandler({ schema });
*
* serve(
* (req: Request) => {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* const { socket, response } = Deno.upgradeWebSocket(req, {
* protocol: GRAPHQL_TRANSPORT_WS_PROTOCOL,
* idleTimeout: 12_000,
* });
* handler(socket);
* return response;
* },
* { port: 4000 },
* );
* ```
*
* @category Server/deno
*/
function makeHandler(options) {
const server = (0, server_1.makeServer)(options);
return function handle(socket) {
socket.onerror = (err) => {
console.error('Internal error emitted on the WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, 'Internal server error');
};
let closed = () => {
// noop
};
socket.onopen = () => {
closed = server.opened({
protocol: socket.protocol,
send: (msg) => socket.send(msg),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => {
socket.onmessage = async (event) => {
try {
await cb(String(event.data));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, 'Internal server error');
}
};
},
}, { socket });
};
socket.onclose = (event) => {
if (event.code === common_1.CloseCode.SubprotocolNotAcceptable &&
socket.protocol === common_1.DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(event.code, event.reason);
};
};
}
exports.makeHandler = makeHandler;

View File

@@ -0,0 +1,83 @@
import { makeServer } from '../server.mjs';
import { DEPRECATED_GRAPHQL_WS_PROTOCOL, CloseCode, } from '../common.mjs';
export { GRAPHQL_TRANSPORT_WS_PROTOCOL } from '../common.mjs';
/**
* Use the server with [Deno](https://deno.com/).
* This is a basic starter, feel free to copy the code over and adjust it to your needs.
*
* The keep-alive is set in `Deno.upgradeWebSocket` during the upgrade.
*
* Additionally, the required WebSocket protocol is also defined during the upgrade,
* the correct example being:
*
* ```ts
* import { serve } from 'https://deno.land/std/http/mod.ts';
* import {
* makeHandler,
* GRAPHQL_TRANSPORT_WS_PROTOCOL,
* } from 'https://esm.sh/graphql-ws/lib/use/deno';
* import { schema } from './my-schema.ts/index.mjs';
*
* const handler = makeHandler({ schema });
*
* serve(
* (req: Request) => {
* const [path, _search] = req.url.split('?');
* if (!path.endsWith('/graphql')) {
* return new Response('Not Found', { status: 404 });
* }
* if (req.headers.get('upgrade') != 'websocket') {
* return new Response('Upgrade Required', { status: 426 });
* }
* const { socket, response } = Deno.upgradeWebSocket(req, {
* protocol: GRAPHQL_TRANSPORT_WS_PROTOCOL,
* idleTimeout: 12_000,
* });
* handler(socket);
* return response;
* },
* { port: 4000 },
* );
* ```
*
* @category Server/deno
*/
export function makeHandler(options) {
const server = makeServer(options);
return function handle(socket) {
socket.onerror = (err) => {
console.error('Internal error emitted on the WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, 'Internal server error');
};
let closed = () => {
// noop
};
socket.onopen = () => {
closed = server.opened({
protocol: socket.protocol,
send: (msg) => socket.send(msg),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => {
socket.onmessage = async (event) => {
try {
await cb(String(event.data));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, 'Internal server error');
}
};
},
}, { socket });
};
socket.onclose = (event) => {
if (event.code === CloseCode.SubprotocolNotAcceptable &&
socket.protocol === DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(event.code, event.reason);
};
};
}

View File

@@ -0,0 +1,40 @@
import type { FastifyRequest } from 'fastify';
import type * as fastifyWebsocket from 'fastify-websocket';
import { ServerOptions } from '../server.mjs';
import { ConnectionInitMessage } from '../common.mjs';
/**
* The extra that will be put in the `Context`.
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
export interface Extra {
/**
* The underlying socket connection between the server and the client.
* The WebSocket socket is located under the `socket` parameter.
*/
readonly connection: fastifyWebsocket.SocketStream;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: FastifyRequest;
}
/**
* Make a handler to use on a [fastify-websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): fastifyWebsocket.WebsocketHandler;

View File

@@ -0,0 +1,40 @@
import type { FastifyRequest } from 'fastify';
import type * as fastifyWebsocket from 'fastify-websocket';
import { ServerOptions } from '../server';
import { ConnectionInitMessage } from '../common';
/**
* The extra that will be put in the `Context`.
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
export interface Extra {
/**
* The underlying socket connection between the server and the client.
* The WebSocket socket is located under the `socket` parameter.
*/
readonly connection: fastifyWebsocket.SocketStream;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: FastifyRequest;
}
/**
* Make a handler to use on a [fastify-websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
export declare function makeHandler<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): fastifyWebsocket.WebsocketHandler;

View File

@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeHandler = void 0;
const websocket_1 = require("./@fastify/websocket");
/**
* Make a handler to use on a [fastify-websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
function makeHandler(options,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
// new handler can be reused, the semantics stayed the same
return (0, websocket_1.makeHandler)(options, keepAlive);
}
exports.makeHandler = makeHandler;

View File

@@ -0,0 +1,21 @@
import { makeHandler as makeHandlerCurrent } from './@fastify/websocket.mjs';
/**
* Make a handler to use on a [fastify-websocket](https://github.com/fastify/fastify-websocket) route.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @deprecated Use `@fastify/websocket` instead.
*
* @category Server/fastify-websocket
*/
export function makeHandler(options,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
// new handler can be reused, the semantics stayed the same
return makeHandlerCurrent(options, keepAlive);
}

View File

@@ -0,0 +1,67 @@
/// <reference types="node" />
import type * as uWS from 'uWebSockets.js';
import type http from 'http';
import { ServerOptions } from '../server.mjs';
import { ConnectionInitMessage } from '../common.mjs';
/**
* The extra that will be put in the `Context`.
*
* @category Server/uWebSockets
*/
export interface Extra extends UpgradeData {
/**
* The actual socket connection between the server and the client
* with the upgrade data.
*/
readonly socket: uWS.WebSocket<unknown> & UpgradeData;
}
/**
* Data acquired during the HTTP upgrade callback from uWS.
*
* @category Server/uWebSockets
*/
export interface UpgradeData {
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*
* uWS's request is stack allocated and cannot be accessed
* from outside of the internal upgrade; therefore, the persisted
* request holds the relevant values extracted from the uWS's request
* while it is accessible.
*/
readonly persistedRequest: PersistedRequest;
}
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*
* uWS's request is stack allocated and cannot be accessed
* from outside of the internal upgrade; therefore, the persisted
* request holds relevant values extracted from the uWS's request
* while it is accessible.
*
* @category Server/uWebSockets
*/
export interface PersistedRequest {
method: string;
url: string;
/** The raw query string (after the `?` sign) or empty string. */
query: string;
headers: http.IncomingHttpHeaders;
}
/**
* Make the behaviour for using a [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js) WebSocket server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/uWebSockets
*/
export declare function makeBehavior<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>, behavior?: uWS.WebSocketBehavior<unknown>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): uWS.WebSocketBehavior<unknown>;

View File

@@ -0,0 +1,67 @@
/// <reference types="node" />
import type * as uWS from 'uWebSockets.js';
import type http from 'http';
import { ServerOptions } from '../server';
import { ConnectionInitMessage } from '../common';
/**
* The extra that will be put in the `Context`.
*
* @category Server/uWebSockets
*/
export interface Extra extends UpgradeData {
/**
* The actual socket connection between the server and the client
* with the upgrade data.
*/
readonly socket: uWS.WebSocket<unknown> & UpgradeData;
}
/**
* Data acquired during the HTTP upgrade callback from uWS.
*
* @category Server/uWebSockets
*/
export interface UpgradeData {
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*
* uWS's request is stack allocated and cannot be accessed
* from outside of the internal upgrade; therefore, the persisted
* request holds the relevant values extracted from the uWS's request
* while it is accessible.
*/
readonly persistedRequest: PersistedRequest;
}
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*
* uWS's request is stack allocated and cannot be accessed
* from outside of the internal upgrade; therefore, the persisted
* request holds relevant values extracted from the uWS's request
* while it is accessible.
*
* @category Server/uWebSockets
*/
export interface PersistedRequest {
method: string;
url: string;
/** The raw query string (after the `?` sign) or empty string. */
query: string;
headers: http.IncomingHttpHeaders;
}
/**
* Make the behaviour for using a [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js) WebSocket server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/uWebSockets
*/
export declare function makeBehavior<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>, behavior?: uWS.WebSocketBehavior<unknown>,
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): uWS.WebSocketBehavior<unknown>;

View File

@@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeBehavior = void 0;
const server_1 = require("../server");
const common_1 = require("../common");
const utils_1 = require("../utils");
/**
* Make the behaviour for using a [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js) WebSocket server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/uWebSockets
*/
function makeBehavior(options, behavior = {},
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = (0, server_1.makeServer)(options);
const clients = new Map();
let onDrain = () => {
// gets called when backpressure drains
};
return Object.assign(Object.assign({}, behavior), { pong(...args) {
var _a;
(_a = behavior.pong) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Pong received for a missing client');
if (client.pongWaitTimeout) {
clearTimeout(client.pongWaitTimeout);
client.pongWaitTimeout = null;
}
},
upgrade(...args) {
var _a;
(_a = behavior.upgrade) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [res, req, context] = args;
const headers = {};
req.forEach((key, value) => {
headers[key] = value;
});
res.upgrade({
persistedRequest: {
method: req.getMethod(),
url: req.getUrl(),
query: req.getQuery(),
headers,
},
}, req.getHeader('sec-websocket-key'), (0, server_1.handleProtocols)(req.getHeader('sec-websocket-protocol')) ||
new Uint8Array(), req.getHeader('sec-websocket-extensions'), context);
},
open(...args) {
var _a;
(_a = behavior.open) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const socket = args[0];
const persistedRequest = socket.persistedRequest;
// prepare client object
const client = {
pingInterval: null,
pongWaitTimeout: null,
handleMessage: () => {
throw new Error('Message received before handler was registered');
},
closed: () => {
throw new Error('Closed before handler was registered');
},
};
client.closed = server.opened({
protocol: (0, server_1.handleProtocols)(persistedRequest.headers['sec-websocket-protocol'] || '') || '',
send: async (message) => {
// the socket might have been destroyed in the meantime
if (!clients.has(socket))
return;
if (!socket.send(message))
// if backpressure is built up wait for drain
await new Promise((resolve) => (onDrain = resolve));
},
close: (code, reason) => {
// end socket in next tick making sure the client is registered
setImmediate(() => {
// the socket might have been destroyed before issuing a close
if (clients.has(socket))
socket.end(code, reason);
});
},
onMessage: (cb) => (client.handleMessage = cb),
}, { socket, persistedRequest });
if (keepAlive > 0 && isFinite(keepAlive)) {
client.pingInterval = setInterval(() => {
// terminate the connection after pong wait has passed because the client is idle
client.pongWaitTimeout = setTimeout(() => socket.close(), keepAlive);
socket.ping();
}, keepAlive);
}
clients.set(socket, client);
},
drain(...args) {
var _a;
(_a = behavior.drain) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
onDrain();
},
async message(...args) {
var _a;
(_a = behavior.message) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket, message] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Message received for a missing client');
try {
await client.handleMessage(Buffer.from(message).toString());
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.end(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
},
close(...args) {
var _a;
(_a = behavior.close) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket, code, message] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Closing a missing client');
if (client.pongWaitTimeout)
clearTimeout(client.pongWaitTimeout);
if (client.pingInterval)
clearTimeout(client.pingInterval);
client.closed(code, Buffer.from(message).toString());
clients.delete(socket);
} });
}
exports.makeBehavior = makeBehavior;

View File

@@ -0,0 +1,137 @@
import { handleProtocols, makeServer } from '../server.mjs';
import { CloseCode } from '../common.mjs';
import { limitCloseReason } from '../utils.mjs';
/**
* Make the behaviour for using a [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js) WebSocket server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/uWebSockets
*/
export function makeBehavior(options, behavior = {},
/**
* The timout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = makeServer(options);
const clients = new Map();
let onDrain = () => {
// gets called when backpressure drains
};
return Object.assign(Object.assign({}, behavior), { pong(...args) {
var _a;
(_a = behavior.pong) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Pong received for a missing client');
if (client.pongWaitTimeout) {
clearTimeout(client.pongWaitTimeout);
client.pongWaitTimeout = null;
}
},
upgrade(...args) {
var _a;
(_a = behavior.upgrade) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [res, req, context] = args;
const headers = {};
req.forEach((key, value) => {
headers[key] = value;
});
res.upgrade({
persistedRequest: {
method: req.getMethod(),
url: req.getUrl(),
query: req.getQuery(),
headers,
},
}, req.getHeader('sec-websocket-key'), handleProtocols(req.getHeader('sec-websocket-protocol')) ||
new Uint8Array(), req.getHeader('sec-websocket-extensions'), context);
},
open(...args) {
var _a;
(_a = behavior.open) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const socket = args[0];
const persistedRequest = socket.persistedRequest;
// prepare client object
const client = {
pingInterval: null,
pongWaitTimeout: null,
handleMessage: () => {
throw new Error('Message received before handler was registered');
},
closed: () => {
throw new Error('Closed before handler was registered');
},
};
client.closed = server.opened({
protocol: handleProtocols(persistedRequest.headers['sec-websocket-protocol'] || '') || '',
send: async (message) => {
// the socket might have been destroyed in the meantime
if (!clients.has(socket))
return;
if (!socket.send(message))
// if backpressure is built up wait for drain
await new Promise((resolve) => (onDrain = resolve));
},
close: (code, reason) => {
// end socket in next tick making sure the client is registered
setImmediate(() => {
// the socket might have been destroyed before issuing a close
if (clients.has(socket))
socket.end(code, reason);
});
},
onMessage: (cb) => (client.handleMessage = cb),
}, { socket, persistedRequest });
if (keepAlive > 0 && isFinite(keepAlive)) {
client.pingInterval = setInterval(() => {
// terminate the connection after pong wait has passed because the client is idle
client.pongWaitTimeout = setTimeout(() => socket.close(), keepAlive);
socket.ping();
}, keepAlive);
}
clients.set(socket, client);
},
drain(...args) {
var _a;
(_a = behavior.drain) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
onDrain();
},
async message(...args) {
var _a;
(_a = behavior.message) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket, message] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Message received for a missing client');
try {
await client.handleMessage(Buffer.from(message).toString());
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.end(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
},
close(...args) {
var _a;
(_a = behavior.close) === null || _a === void 0 ? void 0 : _a.call(behavior, ...args);
const [socket, code, message] = args;
const client = clients.get(socket);
if (!client)
throw new Error('Closing a missing client');
if (client.pongWaitTimeout)
clearTimeout(client.pongWaitTimeout);
if (client.pingInterval)
clearTimeout(client.pingInterval);
client.closed(code, Buffer.from(message).toString());
clients.delete(socket);
} });
}

View File

@@ -0,0 +1,38 @@
import type * as http from 'http';
import type * as ws from 'ws';
import { ServerOptions } from '../server.mjs';
import { ConnectionInitMessage, Disposable } from '../common.mjs';
type WebSocket = typeof ws.prototype;
type WebSocketServer = ws.Server;
/**
* The extra that will be put in the `Context`.
*
* @category Server/ws
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: WebSocket;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: http.IncomingMessage;
}
/**
* Use the server on a [ws](https://github.com/websockets/ws) ws server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/ws
*/
export declare function useServer<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>, ws: WebSocketServer,
/**
* The timeout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): Disposable;
export {};

View File

@@ -0,0 +1,38 @@
import type * as http from 'http';
import type * as ws from 'ws';
import { ServerOptions } from '../server';
import { ConnectionInitMessage, Disposable } from '../common';
type WebSocket = typeof ws.prototype;
type WebSocketServer = ws.Server;
/**
* The extra that will be put in the `Context`.
*
* @category Server/ws
*/
export interface Extra {
/**
* The actual socket connection between the server and the client.
*/
readonly socket: WebSocket;
/**
* The initial HTTP upgrade request before the actual
* socket and connection is established.
*/
readonly request: http.IncomingMessage;
}
/**
* Use the server on a [ws](https://github.com/websockets/ws) ws server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/ws
*/
export declare function useServer<P extends ConnectionInitMessage['payload'] = ConnectionInitMessage['payload'], E extends Record<PropertyKey, unknown> = Record<PropertyKey, never>>(options: ServerOptions<P, Extra & Partial<E>>, ws: WebSocketServer,
/**
* The timeout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive?: number): Disposable;
export {};

View File

@@ -0,0 +1,119 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useServer = void 0;
const server_1 = require("../server");
const common_1 = require("../common");
const utils_1 = require("../utils");
/**
* Use the server on a [ws](https://github.com/websockets/ws) ws server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/ws
*/
function useServer(options, ws,
/**
* The timeout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = (0, server_1.makeServer)(options);
ws.options.handleProtocols = server_1.handleProtocols;
ws.once('error', (err) => {
console.error('Internal error emitted on the WebSocket server. ' +
'Please check your implementation.', err);
// catch the first thrown error and re-throw it once all clients have been notified
let firstErr = null;
// report server errors by erroring out all clients with the same error
for (const client of ws.clients) {
try {
client.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
catch (err) {
firstErr = firstErr !== null && firstErr !== void 0 ? firstErr : err;
}
}
if (firstErr)
throw firstErr;
});
ws.on('connection', (socket, request) => {
socket.once('error', (err) => {
console.error('Internal error emitted on a WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
});
// keep alive through ping-pong messages
let pongWait = null;
const pingInterval = keepAlive > 0 && isFinite(keepAlive)
? setInterval(() => {
// ping pong on open sockets only
if (socket.readyState === socket.OPEN) {
// terminate the connection after pong wait has passed because the client is idle
pongWait = setTimeout(() => {
socket.terminate();
}, keepAlive);
// listen for client's pong and stop socket termination
socket.once('pong', () => {
if (pongWait) {
clearTimeout(pongWait);
pongWait = null;
}
});
socket.ping();
}
}, keepAlive)
: null;
const closed = server.opened({
protocol: socket.protocol,
send: (data) => new Promise((resolve, reject) => {
if (socket.readyState !== socket.OPEN)
return resolve();
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => socket.on('message', async (event) => {
try {
await cb(String(event));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(common_1.CloseCode.InternalServerError, isProd
? 'Internal server error'
: (0, utils_1.limitCloseReason)(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
}),
}, { socket, request });
socket.once('close', (code, reason) => {
if (pongWait)
clearTimeout(pongWait);
if (pingInterval)
clearInterval(pingInterval);
if (!isProd &&
code === common_1.CloseCode.SubprotocolNotAcceptable &&
socket.protocol === common_1.DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(code, String(reason));
});
});
return {
dispose: async () => {
for (const client of ws.clients) {
client.close(1001, 'Going away');
}
ws.removeAllListeners();
await new Promise((resolve, reject) => {
ws.close((err) => (err ? reject(err) : resolve()));
});
},
};
}
exports.useServer = useServer;

View File

@@ -0,0 +1,115 @@
import { handleProtocols, makeServer } from '../server.mjs';
import { DEPRECATED_GRAPHQL_WS_PROTOCOL, CloseCode, } from '../common.mjs';
import { limitCloseReason } from '../utils.mjs';
/**
* Use the server on a [ws](https://github.com/websockets/ws) ws server.
* This is a basic starter, feel free to copy the code over and adjust it to your needs
*
* @category Server/ws
*/
export function useServer(options, ws,
/**
* The timeout between dispatched keep-alive messages. Internally uses the [ws Ping and Pongs]((https://developer.mozilla.org/en-US/docs/Web/API/wss_API/Writing_ws_servers#Pings_and_Pongs_The_Heartbeat_of_wss))
* to check that the link between the clients and the server is operating and to prevent the link
* from being broken due to idling.
*
* @default 12_000 // 12 seconds
*/
keepAlive = 12000) {
const isProd = process.env.NODE_ENV === 'production';
const server = makeServer(options);
ws.options.handleProtocols = handleProtocols;
ws.once('error', (err) => {
console.error('Internal error emitted on the WebSocket server. ' +
'Please check your implementation.', err);
// catch the first thrown error and re-throw it once all clients have been notified
let firstErr = null;
// report server errors by erroring out all clients with the same error
for (const client of ws.clients) {
try {
client.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
catch (err) {
firstErr = firstErr !== null && firstErr !== void 0 ? firstErr : err;
}
}
if (firstErr)
throw firstErr;
});
ws.on('connection', (socket, request) => {
socket.once('error', (err) => {
console.error('Internal error emitted on a WebSocket socket. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
});
// keep alive through ping-pong messages
let pongWait = null;
const pingInterval = keepAlive > 0 && isFinite(keepAlive)
? setInterval(() => {
// ping pong on open sockets only
if (socket.readyState === socket.OPEN) {
// terminate the connection after pong wait has passed because the client is idle
pongWait = setTimeout(() => {
socket.terminate();
}, keepAlive);
// listen for client's pong and stop socket termination
socket.once('pong', () => {
if (pongWait) {
clearTimeout(pongWait);
pongWait = null;
}
});
socket.ping();
}
}, keepAlive)
: null;
const closed = server.opened({
protocol: socket.protocol,
send: (data) => new Promise((resolve, reject) => {
if (socket.readyState !== socket.OPEN)
return resolve();
socket.send(data, (err) => (err ? reject(err) : resolve()));
}),
close: (code, reason) => socket.close(code, reason),
onMessage: (cb) => socket.on('message', async (event) => {
try {
await cb(String(event));
}
catch (err) {
console.error('Internal error occurred during message handling. ' +
'Please check your implementation.', err);
socket.close(CloseCode.InternalServerError, isProd
? 'Internal server error'
: limitCloseReason(err instanceof Error ? err.message : String(err), 'Internal server error'));
}
}),
}, { socket, request });
socket.once('close', (code, reason) => {
if (pongWait)
clearTimeout(pongWait);
if (pingInterval)
clearInterval(pingInterval);
if (!isProd &&
code === CloseCode.SubprotocolNotAcceptable &&
socket.protocol === DEPRECATED_GRAPHQL_WS_PROTOCOL)
console.warn(`Client provided the unsupported and deprecated subprotocol "${socket.protocol}" used by subscriptions-transport-ws.` +
'Please see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws.');
closed(code, String(reason));
});
});
return {
dispose: async () => {
for (const client of ws.clients) {
client.close(1001, 'Going away');
}
ws.removeAllListeners();
await new Promise((resolve, reject) => {
ws.close((err) => (err ? reject(err) : resolve()));
});
},
};
}

View File

@@ -0,0 +1,23 @@
/**
*
* utils
*
*/
import { GraphQLError } from 'graphql';
/** @private */
export declare function extendedTypeof(val: unknown): 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'null';
/** @private */
export declare function isObject(val: unknown): val is Record<PropertyKey, unknown>;
/** @private */
export declare function isAsyncIterable<T = unknown>(val: unknown): val is AsyncIterable<T>;
/** @private */
export declare function isAsyncGenerator<T = unknown>(val: unknown): val is AsyncGenerator<T>;
/** @private */
export declare function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[];
/**
* Limits the WebSocket close event reason to not exceed a length of one frame.
* Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.
*
* @private
*/
export declare function limitCloseReason(reason: string, whenTooLong: string): string;

View File

@@ -0,0 +1,23 @@
/**
*
* utils
*
*/
import { GraphQLError } from 'graphql';
/** @private */
export declare function extendedTypeof(val: unknown): 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'null';
/** @private */
export declare function isObject(val: unknown): val is Record<PropertyKey, unknown>;
/** @private */
export declare function isAsyncIterable<T = unknown>(val: unknown): val is AsyncIterable<T>;
/** @private */
export declare function isAsyncGenerator<T = unknown>(val: unknown): val is AsyncGenerator<T>;
/** @private */
export declare function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[];
/**
* Limits the WebSocket close event reason to not exceed a length of one frame.
* Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.
*
* @private
*/
export declare function limitCloseReason(reason: string, whenTooLong: string): string;

View File

@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.limitCloseReason = exports.areGraphQLErrors = exports.isAsyncGenerator = exports.isAsyncIterable = exports.isObject = exports.extendedTypeof = void 0;
/** @private */
function extendedTypeof(val) {
if (val === null) {
return 'null';
}
if (Array.isArray(val)) {
return 'array';
}
return typeof val;
}
exports.extendedTypeof = extendedTypeof;
/** @private */
function isObject(val) {
return extendedTypeof(val) === 'object';
}
exports.isObject = isObject;
/** @private */
function isAsyncIterable(val) {
return typeof Object(val)[Symbol.asyncIterator] === 'function';
}
exports.isAsyncIterable = isAsyncIterable;
/** @private */
function isAsyncGenerator(val) {
return (isObject(val) &&
typeof Object(val)[Symbol.asyncIterator] === 'function' &&
typeof val.return === 'function'
// for lazy ones, we only need the return anyway
// typeof val.throw === 'function' &&
// typeof val.next === 'function'
);
}
exports.isAsyncGenerator = isAsyncGenerator;
/** @private */
function areGraphQLErrors(obj) {
return (Array.isArray(obj) &&
// must be at least one error
obj.length > 0 &&
// error has at least a message
obj.every((ob) => 'message' in ob));
}
exports.areGraphQLErrors = areGraphQLErrors;
/**
* Limits the WebSocket close event reason to not exceed a length of one frame.
* Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.
*
* @private
*/
function limitCloseReason(reason, whenTooLong) {
return reason.length < 124 ? reason : whenTooLong;
}
exports.limitCloseReason = limitCloseReason;

View File

@@ -0,0 +1,45 @@
/** @private */
export function extendedTypeof(val) {
if (val === null) {
return 'null';
}
if (Array.isArray(val)) {
return 'array';
}
return typeof val;
}
/** @private */
export function isObject(val) {
return extendedTypeof(val) === 'object';
}
/** @private */
export function isAsyncIterable(val) {
return typeof Object(val)[Symbol.asyncIterator] === 'function';
}
/** @private */
export function isAsyncGenerator(val) {
return (isObject(val) &&
typeof Object(val)[Symbol.asyncIterator] === 'function' &&
typeof val.return === 'function'
// for lazy ones, we only need the return anyway
// typeof val.throw === 'function' &&
// typeof val.next === 'function'
);
}
/** @private */
export function areGraphQLErrors(obj) {
return (Array.isArray(obj) &&
// must be at least one error
obj.length > 0 &&
// error has at least a message
obj.every((ob) => 'message' in ob));
}
/**
* Limits the WebSocket close event reason to not exceed a length of one frame.
* Reference: https://datatracker.ietf.org/doc/html/rfc6455#section-5.2.
*
* @private
*/
export function limitCloseReason(reason, whenTooLong) {
return reason.length < 124 ? reason : whenTooLong;
}