graphql-ws
Advanced tools
Comparing version 4.9.0 to 5.0.0
@@ -0,1 +1,51 @@ | ||
# [5.0.0](https://github.com/enisdenjo/graphql-ws/compare/v4.9.0...v5.0.0) (2021-06-08) | ||
### Features | ||
* Bidirectional ping/pong message types ([#201](https://github.com/enisdenjo/graphql-ws/issues/201)) ([1efaf83](https://github.com/enisdenjo/graphql-ws/commit/1efaf8347dd199687393e8074ab70362727591f2)) | ||
* **client:** Rename `keepAlive` option to `lazyCloseTimeout` ([3c1f13c](https://github.com/enisdenjo/graphql-ws/commit/3c1f13cd49ee00d7da80f3950eef8f414d909d58)) | ||
* **uWebSockets:** Drop deprecated `request` context extra ([02ea5ee](https://github.com/enisdenjo/graphql-ws/commit/02ea5ee8cfe918d547608c69482911e3d6091290)) | ||
### BREAKING CHANGES | ||
* Because of the Protocol's strictness, an instant connection termination will happen whenever an invalid message is identified; meaning, all previous implementations will fail when receiving the new subprotocol ping/pong messages. | ||
**Beware,** the client will NOT ping the server by default. Please make sure to upgrade your stack in order to support the new ping/pong message types. | ||
A simple recipe showcasing a client that times out if no pong is received and measures latency, looks like this: | ||
```js | ||
import { createClient } from 'graphql-ws'; | ||
let activeSocket, | ||
timedOut, | ||
pingSentAt = 0, | ||
latency = 0; | ||
createClient({ | ||
url: 'ws://i.time.out:4000/and-measure/latency', | ||
keepAlive: 10_000, // ping server every 10 seconds | ||
on: { | ||
connected: (socket) => (activeSocket = socket), | ||
ping: (received) => { | ||
if (!received /* sent */) { | ||
pingSentAt = Date.now(); | ||
timedOut = setTimeout(() => { | ||
if (activeSocket.readyState === WebSocket.OPEN) | ||
activeSocket.close(4408, 'Request Timeout'); | ||
}, 5_000); // wait 5 seconds for the pong and then close the connection | ||
} | ||
}, | ||
pong: (received) => { | ||
if (received) { | ||
latency = Date.now() - pingSentAt; | ||
clearTimeout(timedOut); // pong is received, clear connection close timeout | ||
} | ||
}, | ||
}, | ||
}); | ||
``` | ||
* **uWebSockets:** The deprecated uWebSockets `request` context extra field has been dropped because it is stack allocated and cannot be used ouside the internal `upgrade` callback. | ||
* **client:** Client `keepAlive` option has been renamed to `lazyCloseTimeout` in order to eliminate ambiguity with the client to server pings keep-alive option. | ||
# [4.9.0](https://github.com/enisdenjo/graphql-ws/compare/v4.8.0...v4.9.0) (2021-06-06) | ||
@@ -2,0 +52,0 @@ |
@@ -14,2 +14,6 @@ /** | ||
/** @category Client */ | ||
export declare type EventPing = 'ping'; | ||
/** @category Client */ | ||
export declare type EventPong = 'pong'; | ||
/** @category Client */ | ||
export declare type EventMessage = 'message'; | ||
@@ -21,3 +25,3 @@ /** @category Client */ | ||
/** @category Client */ | ||
export declare type Event = EventConnecting | EventConnected | EventMessage | EventClosed | EventError; | ||
export declare type Event = EventConnecting | EventConnected | EventPing | EventPong | EventMessage | EventClosed | EventError; | ||
/** | ||
@@ -37,2 +41,16 @@ * The first argument is actually the `WebSocket`, but to avoid | ||
/** | ||
* The first argument communicates whether the ping was received from the server. | ||
* If `false`, the ping was sent by the client. | ||
* | ||
* @category Client | ||
*/ | ||
export declare type EventPingListener = (received: boolean) => 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 declare type EventPongListener = (received: boolean) => void; | ||
/** | ||
* Called for all **valid** messages received by the client. Mainly useful for | ||
@@ -62,3 +80,3 @@ * debugging and logging received messages. | ||
/** @category Client */ | ||
export declare type EventListener<E extends Event> = E extends EventConnecting ? EventConnectingListener : E extends EventConnected ? EventConnectedListener : E extends EventMessage ? EventMessageListener : E extends EventClosed ? EventClosedListener : E extends EventError ? EventErrorListener : never; | ||
export declare type EventListener<E extends Event> = E extends EventConnecting ? EventConnectingListener : 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; | ||
/** | ||
@@ -131,2 +149,40 @@ * Configuration used for the GraphQL over WebSocket client. | ||
*/ | ||
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; | ||
@@ -133,0 +189,0 @@ /** |
@@ -29,3 +29,3 @@ "use strict"; | ||
function createClient(options) { | ||
const { url, connectionParams, lazy = true, onNonLazyError = console.error, keepAlive = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) { | ||
const { url, connectionParams, lazy = true, onNonLazyError = console.error, lazyCloseTimeout = 0, keepAlive = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) { | ||
let retryDelay = 1000; // start with 1s delay | ||
@@ -100,2 +100,4 @@ for (let i = 0; i < retries; i++) { | ||
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], | ||
@@ -136,2 +138,14 @@ closed: (on === null || on === void 0 ? void 0 : on.closed) ? [on.closed] : [], | ||
const socket = new WebSocketImpl(typeof url === 'function' ? await url() : url, common_1.GRAPHQL_TRANSPORT_WS_PROTOCOL); | ||
let 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(common_1.stringifyMessage({ type: common_1.MessageType.Ping })); | ||
emitter.emit('ping', false); | ||
} | ||
}, keepAlive); | ||
} | ||
} | ||
socket.onerror = (err) => { | ||
@@ -143,2 +157,3 @@ // we let the onclose reject the promise for correct retry handling | ||
connecting = undefined; | ||
clearTimeout(queuedPing); | ||
emitter.emit('closed', event); | ||
@@ -155,2 +170,3 @@ denied(event); | ||
}, replacer)); | ||
enqueuePing(); // enqueue ping (noop if disabled) | ||
} | ||
@@ -166,2 +182,13 @@ catch (err) { | ||
emitter.emit('message', message); | ||
if (message.type === 'ping' || message.type === 'pong') { | ||
emitter.emit(message.type, true); // received | ||
if (message.type === 'ping') { | ||
// respond with pong on ping | ||
socket.send(common_1.stringifyMessage({ type: common_1.MessageType.Pong })); | ||
emitter.emit('pong', false); | ||
} | ||
else | ||
enqueuePing(); // enqueue next ping on pong (noop if disabled) | ||
return; // ping and pongs can be received whenever | ||
} | ||
if (acknowledged) | ||
@@ -201,3 +228,3 @@ return; // already connected and acknowledged | ||
const complete = () => socket.close(1000, 'Normal Closure'); | ||
if (isFinite(keepAlive) && keepAlive > 0) { | ||
if (isFinite(lazyCloseTimeout) && lazyCloseTimeout > 0) { | ||
// if the keepalive is set, allow for the specified calmdown time and | ||
@@ -209,3 +236,3 @@ // then complete. but only if no lock got created in the meantime and | ||
complete(); | ||
}, keepAlive); | ||
}, lazyCloseTimeout); | ||
} | ||
@@ -212,0 +239,0 @@ else { |
@@ -53,2 +53,4 @@ /** | ||
ConnectionAck = "connection_ack", | ||
Ping = "ping", | ||
Pong = "pong", | ||
Subscribe = "subscribe", | ||
@@ -70,2 +72,10 @@ Next = "next", | ||
/** @category Common */ | ||
export interface PingMessage { | ||
readonly type: MessageType.Ping; | ||
} | ||
/** @category Common */ | ||
export interface PongMessage { | ||
readonly type: MessageType.Pong; | ||
} | ||
/** @category Common */ | ||
export interface SubscribeMessage { | ||
@@ -101,3 +111,3 @@ readonly id: ID; | ||
/** @category Common */ | ||
export declare type Message<T extends MessageType = MessageType> = T extends MessageType.ConnectionAck ? ConnectionAckMessage : T extends MessageType.ConnectionInit ? ConnectionInitMessage : T extends MessageType.Subscribe ? SubscribeMessage : T extends MessageType.Next ? NextMessage : T extends MessageType.Error ? ErrorMessage : T extends MessageType.Complete ? CompleteMessage : never; | ||
export declare 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; | ||
/** | ||
@@ -104,0 +114,0 @@ * Checks if the provided value is a message. |
@@ -25,2 +25,4 @@ "use strict"; | ||
MessageType["ConnectionAck"] = "connection_ack"; | ||
MessageType["Ping"] = "ping"; | ||
MessageType["Pong"] = "pong"; | ||
MessageType["Subscribe"] = "subscribe"; | ||
@@ -54,2 +56,6 @@ MessageType["Next"] = "next"; | ||
utils_1.isObject(val.payload)); | ||
case MessageType.Ping: | ||
case MessageType.Pong: | ||
// ping and pong types are simply valid | ||
return true; | ||
case MessageType.Subscribe: | ||
@@ -56,0 +62,0 @@ return (utils_1.hasOwnStringProperty(val, 'id') && |
@@ -117,3 +117,3 @@ /** | ||
* | ||
* @default 3 * 1000 (3 seconds) | ||
* @default 3_000 // 3 seconds | ||
*/ | ||
@@ -120,0 +120,0 @@ connectionInitWaitTimeout?: number; |
@@ -29,3 +29,3 @@ "use strict"; | ||
function makeServer(options) { | ||
const { schema, context, roots, validate, execute, subscribe, connectionInitWaitTimeout = 3 * 1000, // 3 seconds | ||
const { schema, context, roots, validate, execute, subscribe, connectionInitWaitTimeout = 3000, // 3 seconds | ||
onConnect, onDisconnect, onClose, onSubscribe, onOperation, onNext, onError, onComplete, jsonMessageReviver: reviver, jsonMessageReplacer: replacer, } = options; | ||
@@ -90,2 +90,8 @@ return { | ||
} | ||
case common_1.MessageType.Ping: { | ||
await socket.send(common_1.stringifyMessage({ type: common_1.MessageType.Pong })); | ||
return; | ||
} | ||
case common_1.MessageType.Pong: | ||
return; | ||
case common_1.MessageType.Subscribe: { | ||
@@ -92,0 +98,0 @@ if (!ctx.acknowledged) |
@@ -33,4 +33,4 @@ import type { FastifyRequest } from 'fastify'; | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive?: number): fastifyWebsocket.WebsocketHandler; |
@@ -18,5 +18,5 @@ "use strict"; | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive = 12 * 1000) { | ||
keepAlive = 12000) { | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
@@ -23,0 +23,0 @@ const server = server_1.makeServer(options); |
@@ -24,9 +24,2 @@ /// <reference types="node" /> | ||
/** | ||
* The initial HTTP request before the actual | ||
* socket and connection is established. | ||
* | ||
* @deprecated uWS.HttpRequest is stack allocated and cannot be accessed outside the internal `upgrade` callback. Consider using the `persistedRequest` instead. | ||
*/ | ||
readonly request: uWS.HttpRequest; | ||
/** | ||
* The initial HTTP upgrade request before the actual | ||
@@ -72,4 +65,4 @@ * socket and connection is established. | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive?: number): uWS.WebSocketBehavior; |
@@ -17,5 +17,5 @@ "use strict"; | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive = 12 * 1000) { | ||
keepAlive = 12000) { | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
@@ -48,3 +48,2 @@ const server = server_1.makeServer(options); | ||
res.upgrade({ | ||
request: req, | ||
persistedRequest: { | ||
@@ -93,3 +92,3 @@ method: req.getMethod(), | ||
onMessage: (cb) => (client.handleMessage = cb), | ||
}, { socket, request: socket.request, persistedRequest }); | ||
}, { socket, persistedRequest }); | ||
if (keepAlive > 0 && isFinite(keepAlive)) { | ||
@@ -96,0 +95,0 @@ client.pingInterval = setInterval(() => { |
@@ -36,5 +36,5 @@ /// <reference types="node" /> | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive?: number): Disposable; | ||
export {}; |
@@ -18,5 +18,5 @@ "use strict"; | ||
* | ||
* @default 12 * 1000 // 12 seconds | ||
* @default 12_000 // 12 seconds | ||
*/ | ||
keepAlive = 12 * 1000) { | ||
keepAlive = 12000) { | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
@@ -23,0 +23,0 @@ const server = server_1.makeServer(options); |
{ | ||
"name": "graphql-ws", | ||
"version": "4.9.0", | ||
"version": "5.0.0", | ||
"description": "Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client", | ||
@@ -91,6 +91,6 @@ "keywords": [ | ||
"@types/ws": "^7.4.4", | ||
"@typescript-eslint/eslint-plugin": "^4.26.0", | ||
"@typescript-eslint/parser": "^4.26.0", | ||
"@typescript-eslint/eslint-plugin": "^4.26.1", | ||
"@typescript-eslint/parser": "^4.26.1", | ||
"babel-jest": "^27.0.2", | ||
"eslint": "^7.27.0", | ||
"eslint": "^7.28.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
@@ -103,11 +103,11 @@ "eslint-plugin-prettier": "^3.4.0", | ||
"jest": "^27.0.4", | ||
"prettier": "^2.3.0", | ||
"prettier": "^2.3.1", | ||
"replacestream": "^4.0.3", | ||
"rollup": "^2.50.6", | ||
"rollup": "^2.51.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"semantic-release": "^17.4.3", | ||
"tslib": "^2.2.0", | ||
"typedoc": "^0.20.36", | ||
"typedoc": "^0.21.0-beta.2", | ||
"typedoc-plugin-markdown": "^3.9.0", | ||
"typescript": "4.2.4", | ||
"typescript": "^4.3.2", | ||
"uWebSockets.js": "uNetworking/uWebSockets.js#v19.2.0", | ||
@@ -114,0 +114,0 @@ "ws": "^7.4.6" |
@@ -59,2 +59,32 @@ # GraphQL over WebSocket Protocol | ||
### `Ping` | ||
Direction: **bidirectional** | ||
Useful for detecting failed connections, displaying latency metrics or other types of network probing. | ||
A `Pong` must be sent in response from the receiving party as soon as possible. | ||
The `Ping` message can be sent at any time within the established socket. | ||
```typescript | ||
interface PingMessage { | ||
type: 'ping'; | ||
} | ||
``` | ||
### `Pong` | ||
Direction: **bidirectional** | ||
The response to the `Ping` message. Must be sent as soon as the `Ping` message is received. | ||
The `Pong` message can be sent at any time within the established socket. Furthermore, the `Pong` message may even be sent unsolicited as an unidirectional heartbeat. | ||
```typescript | ||
interface PongMessage { | ||
type: 'pong'; | ||
} | ||
``` | ||
### `Subscribe` | ||
@@ -61,0 +91,0 @@ |
154
README.md
@@ -592,2 +592,38 @@ <div align="center"> | ||
<details id="ping-from-client"> | ||
<summary><a href="#ping-from-client">🔗</a> Client usage with ping/pong timeout and latency metrics</summary> | ||
```typescript | ||
import { createClient } from 'graphql-ws'; | ||
let activeSocket, | ||
timedOut, | ||
pingSentAt = 0, | ||
latency = 0; | ||
createClient({ | ||
url: 'ws://i.time.out:4000/and-measure/latency', | ||
keepAlive: 10_000, // ping server every 10 seconds | ||
on: { | ||
connected: (socket) => (activeSocket = socket), | ||
ping: (received) => { | ||
if (!received /* sent */) { | ||
pingSentAt = Date.now(); | ||
timedOut = setTimeout(() => { | ||
if (activeSocket.readyState === WebSocket.OPEN) | ||
activeSocket.close(4408, 'Request Timeout'); | ||
}, 5_000); // wait 5 seconds for the pong and then close the connection | ||
} | ||
}, | ||
pong: (received) => { | ||
if (received) { | ||
latency = Date.now() - pingSentAt; | ||
clearTimeout(timedOut); // pong is received, clear connection close timeout | ||
} | ||
}, | ||
}, | ||
}); | ||
``` | ||
</details> | ||
<details id="browser"> | ||
@@ -1271,120 +1307,2 @@ <summary><a href="#browser">🔗</a> Client usage in browser</summary> | ||
<details id="ping-from-client"> | ||
<summary><a href="#ping-from-client">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server and client with client to server pings and latency</summary> | ||
```typescript | ||
// 🛸 server | ||
import { | ||
GraphQLSchema, | ||
GraphQLObjectType, | ||
GraphQLNonNull, | ||
GraphQLString, | ||
} from 'graphql'; | ||
import ws from 'ws'; // yarn add ws | ||
import { useServer } from 'graphql-ws/lib/use/ws'; | ||
import { schema } from './my-graphql-schema'; | ||
// a custom graphql schema that holds just the ping query. | ||
// used exclusively when the client sends a ping to the server. | ||
// if you want to send/receive more details, simply adjust the pinger schema. | ||
const pinger = new GraphQLSchema({ | ||
query: new GraphQLObjectType({ | ||
name: 'Query', | ||
fields: { | ||
ping: { | ||
type: new GraphQLNonNull(GraphQLString), | ||
resolve: () => 'pong', | ||
}, | ||
}, | ||
}), | ||
}); | ||
const wsServer = new WebSocket.Server({ | ||
port: 4000, | ||
path: '/graphql', | ||
}); | ||
useServer( | ||
{ | ||
schema: (_ctx, msg) => { | ||
if (msg.payload.query === '{ ping }') return pinger; | ||
return schema; | ||
}, | ||
}, | ||
wsServer, | ||
); | ||
``` | ||
```typescript | ||
// 📺 client | ||
import { createClient } from 'graphql-ws'; | ||
let connection: WebSocket | undefined; | ||
const client = createClient({ | ||
url: 'ws://client.can:4000/send-pings/too', | ||
on: { | ||
connected: (socket) => (connection = socket as WebSocket), | ||
closed: () => (connection = undefined), | ||
}, | ||
}); | ||
async function ping() { | ||
// record the ping sent at moment for calculating latency | ||
const pinged = Date.now(); | ||
// if the client went offline or the server is unresponsive | ||
// close the active WebSocket connection as soon as the pong | ||
// wait timeout expires and have the client silently reconnect. | ||
// there is no need to dispose of the subscription since it | ||
// will eventually settle because either: | ||
// - the client reconnected and a new pong is received | ||
// - the retry attempts were exceeded and the close is reported | ||
// because if this, the latency accounts for retry waits too. | ||
// if you do not want this, simply dispose of the ping subscription | ||
// as soon as the pong timeout is exceeded | ||
const pongTimeout = setTimeout( | ||
() => connection?.close(4408, 'Pong Timeout'), | ||
2000, // expect a pong within 2 seconds of the ping | ||
); | ||
// wait for the pong. the promise is guaranteed to settle | ||
await new Promise<void>((resolve, reject) => { | ||
client.subscribe<{ data: { ping: string } }>( | ||
{ query: '{ ping }' }, | ||
{ | ||
next: () => { | ||
/* not interested in the pong */ | ||
}, | ||
error: reject, | ||
complete: resolve, | ||
}, | ||
); | ||
// whatever happens to the promise, clear the pong timeout | ||
}).finally(() => clearTimeout(pongTimeout)); | ||
// record when pong has been received | ||
const ponged = Date.now(); | ||
// how long it took for the pong to arrive after sending the ping | ||
return ponged - pinged; | ||
} | ||
// keep pinging until a fatal problem occurs | ||
(async () => { | ||
for (;;) { | ||
const latency = await ping(); | ||
// or send to your favourite logger - the user | ||
console.info('GraphQL WebSocket connection latency', latency); | ||
// ping every 3 seconds | ||
await new Promise((resolve) => setTimeout(resolve, 3000)); | ||
} | ||
})(); | ||
``` | ||
</details> | ||
<details id="auth-token"> | ||
@@ -1391,0 +1309,0 @@ <summary><a href="#auth-token">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server and client auth usage with token expiration, validation and refresh</summary> |
@@ -54,2 +54,4 @@ (function (global, factory) { | ||
MessageType["ConnectionAck"] = "connection_ack"; | ||
MessageType["Ping"] = "ping"; | ||
MessageType["Pong"] = "pong"; | ||
MessageType["Subscribe"] = "subscribe"; | ||
@@ -83,2 +85,6 @@ MessageType["Next"] = "next"; | ||
isObject(val.payload)); | ||
case exports.MessageType.Ping: | ||
case exports.MessageType.Pong: | ||
// ping and pong types are simply valid | ||
return true; | ||
case exports.MessageType.Subscribe: | ||
@@ -154,3 +160,3 @@ return (hasOwnStringProperty(val, 'id') && | ||
function createClient(options) { | ||
const { url, connectionParams, lazy = true, onNonLazyError = console.error, keepAlive = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) { | ||
const { url, connectionParams, lazy = true, onNonLazyError = console.error, lazyCloseTimeout = 0, keepAlive = 0, retryAttempts = 5, retryWait = async function randomisedExponentialBackoff(retries) { | ||
let retryDelay = 1000; // start with 1s delay | ||
@@ -225,2 +231,4 @@ for (let i = 0; i < retries; i++) { | ||
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], | ||
@@ -261,2 +269,14 @@ closed: (on === null || on === void 0 ? void 0 : on.closed) ? [on.closed] : [], | ||
const socket = new WebSocketImpl(typeof url === 'function' ? await url() : url, GRAPHQL_TRANSPORT_WS_PROTOCOL); | ||
let 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: exports.MessageType.Ping })); | ||
emitter.emit('ping', false); | ||
} | ||
}, keepAlive); | ||
} | ||
} | ||
socket.onerror = (err) => { | ||
@@ -268,2 +288,3 @@ // we let the onclose reject the promise for correct retry handling | ||
connecting = undefined; | ||
clearTimeout(queuedPing); | ||
emitter.emit('closed', event); | ||
@@ -280,2 +301,3 @@ denied(event); | ||
}, replacer)); | ||
enqueuePing(); // enqueue ping (noop if disabled) | ||
} | ||
@@ -291,2 +313,13 @@ catch (err) { | ||
emitter.emit('message', message); | ||
if (message.type === 'ping' || message.type === 'pong') { | ||
emitter.emit(message.type, true); // received | ||
if (message.type === 'ping') { | ||
// respond with pong on ping | ||
socket.send(stringifyMessage({ type: exports.MessageType.Pong })); | ||
emitter.emit('pong', false); | ||
} | ||
else | ||
enqueuePing(); // enqueue next ping on pong (noop if disabled) | ||
return; // ping and pongs can be received whenever | ||
} | ||
if (acknowledged) | ||
@@ -326,3 +359,3 @@ return; // already connected and acknowledged | ||
const complete = () => socket.close(1000, 'Normal Closure'); | ||
if (isFinite(keepAlive) && keepAlive > 0) { | ||
if (isFinite(lazyCloseTimeout) && lazyCloseTimeout > 0) { | ||
// if the keepalive is set, allow for the specified calmdown time and | ||
@@ -334,3 +367,3 @@ // then complete. but only if no lock got created in the meantime and | ||
complete(); | ||
}, keepAlive); | ||
}, lazyCloseTimeout); | ||
} | ||
@@ -337,0 +370,0 @@ else { |
@@ -1,1 +0,1 @@ | ||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlWs={})}(this,(function(e){"use strict";const n=Object.prototype.hasOwnProperty;function o(e){return"object"==typeof e&&null!==e}function t(e,o){return n.call(e,o)}function r(e,t){return n.call(e,t)&&o(e[t])}function a(e,o){return n.call(e,o)&&"string"==typeof e[o]}const s="graphql-transport-ws";var i;function c(n){if(o(n)){if(!a(n,"type"))return!1;switch(n.type){case e.MessageType.ConnectionInit:case e.MessageType.ConnectionAck:return!t(n,"payload")||void 0===n.payload||o(n.payload);case e.MessageType.Subscribe:return a(n,"id")&&r(n,"payload")&&(!t(n.payload,"operationName")||void 0===n.payload.operationName||null===n.payload.operationName||"string"==typeof n.payload.operationName)&&a(n.payload,"query")&&(!t(n.payload,"variables")||void 0===n.payload.variables||null===n.payload.variables||r(n.payload,"variables"))&&(!t(n.payload,"extensions")||void 0===n.payload.extensions||null===n.payload.extensions||r(n.payload,"extensions"));case e.MessageType.Next:return a(n,"id")&&r(n,"payload");case e.MessageType.Error:return a(n,"id")&&(s=n.payload,Array.isArray(s)&&s.length>0&&s.every((e=>"message"in e)));case e.MessageType.Complete:return a(n,"id");default:return!1}}var s;return!1}function l(e,n){if(c(e))return e;if("string"!=typeof e)throw new Error("Message not parsable");const o=JSON.parse(e,n);if(!c(o))throw new Error("Invalid message");return o}function d(e,n){if(!c(e))throw new Error("Cannot stringify invalid message");return JSON.stringify(e,n)}function p(e){return o(e)&&"code"in e&&"reason"in e}e.MessageType=void 0,(i=e.MessageType||(e.MessageType={})).ConnectionInit="connection_init",i.ConnectionAck="connection_ack",i.Subscribe="subscribe",i.Next="next",i.Error="error",i.Complete="complete",e.GRAPHQL_TRANSPORT_WS_PROTOCOL=s,e.createClient=function(n){const{url:o,connectionParams:t,lazy:r=!0,onNonLazyError:a=console.error,keepAlive:i=0,retryAttempts:c=5,retryWait:y=async function(e){let n=1e3;for(let o=0;o<e;o++)n*=2;await new Promise((e=>setTimeout(e,n+Math.floor(2700*Math.random()+300))))},isFatalConnectionProblem:u=(e=>!p(e)),on:f,webSocketImpl:g,generateID:m=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(e=>{const n=16*Math.random()|0;return("x"==e?n:3&n|8).toString(16)}))},jsonMessageReplacer:x,jsonMessageReviver:w}=n;let b;if(g){if(!("function"==typeof(v=g)&&"constructor"in v&&"CLOSED"in v&&"CLOSING"in v&&"CONNECTING"in v&&"OPEN"in v))throw new Error("Invalid WebSocket implementation provided");b=g}else"undefined"!=typeof WebSocket?b=WebSocket:"undefined"!=typeof global?b=global.WebSocket||global.MozWebSocket:"undefined"!=typeof window&&(b=window.WebSocket||window.MozWebSocket);var v;if(!b)throw new Error("WebSocket implementation missing");const h=b,M=(()=>{const e=(()=>{const e={};return{on:(n,o)=>(e[n]=o,()=>{delete e[n]}),emit(n){var o;"id"in n&&(null===(o=e[n.id])||void 0===o||o.call(e,n))}}})(),n={connecting:(null==f?void 0:f.connecting)?[f.connecting]:[],connected:(null==f?void 0:f.connected)?[f.connected]:[],message:(null==f?void 0:f.message)?[e.emit,f.message]:[e.emit],closed:(null==f?void 0:f.closed)?[f.closed]:[],error:(null==f?void 0:f.error)?[f.error]:[]};return{onMessage:e.on,on(e,o){const t=n[e];return t.push(o),()=>{t.splice(t.indexOf(o),1)}},emit(e,...o){for(const t of n[e])t(...o)}}})();let S,T=0,C=!1,N=0,E=!1;async function O(){const[n,r]=await(null!=S?S:S=new Promise(((n,r)=>(async()=>{if(C){if(await y(N),!T)return S=void 0,r({code:1e3,reason:"All Subscriptions Gone"});N++}M.emit("connecting");const a=new h("function"==typeof o?await o():o,s);a.onerror=e=>{M.emit("error",e)},a.onclose=e=>{S=void 0,M.emit("closed",e),r(e)},a.onopen=async()=>{try{a.send(d({type:e.MessageType.ConnectionInit,payload:"function"==typeof t?await t():t},x))}catch(e){a.close(4400,e instanceof Error?e.message:new Error(e).message)}};let i=!1;a.onmessage=({data:o})=>{try{const t=l(o,w);if(M.emit("message",t),i)return;if(t.type!==e.MessageType.ConnectionAck)throw new Error(`First message cannot be of type ${t.type}`);i=!0,M.emit("connected",a,t.payload),C=!1,N=0,n([a,new Promise(((e,n)=>a.addEventListener("close",n)))])}catch(e){a.close(4400,e instanceof Error?e.message:new Error(e).message)}}})())));n.readyState===h.CLOSING&&await r;let a=()=>{};const c=new Promise((e=>a=e));return[n,a,Promise.race([c.then((()=>{if(!T){const e=()=>n.close(1e3,"Normal Closure");isFinite(i)&&i>0?setTimeout((()=>{T||n.readyState!==h.OPEN||e()}),i):e()}})),r])]}function P(e){if(p(e)&&[1002,1011,4400,4401,4409,4429].includes(e.code))throw e;if(E)return!1;if(p(e)&&1e3===e.code)return T>0;if(!c||N>=c)throw e;if(u(e))throw e;return C=!0}return r||(async()=>{for(T++;;)try{const[,,e]=await O();await e}catch(e){try{if(!P(e))return}catch(e){return null==a?void 0:a(e)}}})(),{on:M.on,subscribe(n,o){const t=m();let r=!1,a=!1,s=()=>{T--,r=!0};return(async()=>{for(T++;;)try{const[i,c,l]=await O();if(r)return c();const p=M.onMessage(t,(n=>{switch(n.type){case e.MessageType.Next:return void o.next(n.payload);case e.MessageType.Error:return a=!0,r=!0,o.error(n.payload),void s();case e.MessageType.Complete:return r=!0,void s()}}));return i.send(d({id:t,type:e.MessageType.Subscribe,payload:n},x)),s=()=>{r||i.readyState!==h.OPEN||i.send(d({id:t,type:e.MessageType.Complete},x)),T--,r=!0,c()},void await l.finally(p)}catch(e){if(!P(e))return}})().catch(o.error).then((()=>{a||o.complete()})),()=>{r||s()}},async dispose(){if(E=!0,S){const[e]=await S;e.close(1e3,"Normal Closure")}}}},e.isMessage=c,e.parseMessage=l,e.stringifyMessage=d,Object.defineProperty(e,"__esModule",{value:!0})})); | ||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlWs={})}(this,(function(e){"use strict";const n=Object.prototype.hasOwnProperty;function o(e){return"object"==typeof e&&null!==e}function t(e,o){return n.call(e,o)}function r(e,t){return n.call(e,t)&&o(e[t])}function i(e,o){return n.call(e,o)&&"string"==typeof e[o]}const a="graphql-transport-ws";var s;function c(n){if(o(n)){if(!i(n,"type"))return!1;switch(n.type){case e.MessageType.ConnectionInit:case e.MessageType.ConnectionAck:return!t(n,"payload")||void 0===n.payload||o(n.payload);case e.MessageType.Ping:case e.MessageType.Pong:return!0;case e.MessageType.Subscribe:return i(n,"id")&&r(n,"payload")&&(!t(n.payload,"operationName")||void 0===n.payload.operationName||null===n.payload.operationName||"string"==typeof n.payload.operationName)&&i(n.payload,"query")&&(!t(n.payload,"variables")||void 0===n.payload.variables||null===n.payload.variables||r(n.payload,"variables"))&&(!t(n.payload,"extensions")||void 0===n.payload.extensions||null===n.payload.extensions||r(n.payload,"extensions"));case e.MessageType.Next:return i(n,"id")&&r(n,"payload");case e.MessageType.Error:return i(n,"id")&&(a=n.payload,Array.isArray(a)&&a.length>0&&a.every((e=>"message"in e)));case e.MessageType.Complete:return i(n,"id");default:return!1}}var a;return!1}function l(e,n){if(c(e))return e;if("string"!=typeof e)throw new Error("Message not parsable");const o=JSON.parse(e,n);if(!c(o))throw new Error("Invalid message");return o}function p(e,n){if(!c(e))throw new Error("Cannot stringify invalid message");return JSON.stringify(e,n)}function d(e){return o(e)&&"code"in e&&"reason"in e}e.MessageType=void 0,(s=e.MessageType||(e.MessageType={})).ConnectionInit="connection_init",s.ConnectionAck="connection_ack",s.Ping="ping",s.Pong="pong",s.Subscribe="subscribe",s.Next="next",s.Error="error",s.Complete="complete",e.GRAPHQL_TRANSPORT_WS_PROTOCOL=a,e.createClient=function(n){const{url:o,connectionParams:t,lazy:r=!0,onNonLazyError:i=console.error,lazyCloseTimeout:s=0,keepAlive:c=0,retryAttempts:y=5,retryWait:u=async function(e){let n=1e3;for(let o=0;o<e;o++)n*=2;await new Promise((e=>setTimeout(e,n+Math.floor(2700*Math.random()+300))))},isFatalConnectionProblem:f=(e=>!d(e)),on:g,webSocketImpl:m,generateID:x=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(e=>{const n=16*Math.random()|0;return("x"==e?n:3&n|8).toString(16)}))},jsonMessageReplacer:w,jsonMessageReviver:v}=n;let b;if(m){if(!("function"==typeof(M=m)&&"constructor"in M&&"CLOSED"in M&&"CLOSING"in M&&"CONNECTING"in M&&"OPEN"in M))throw new Error("Invalid WebSocket implementation provided");b=m}else"undefined"!=typeof WebSocket?b=WebSocket:"undefined"!=typeof global?b=global.WebSocket||global.MozWebSocket:"undefined"!=typeof window&&(b=window.WebSocket||window.MozWebSocket);var M;if(!b)throw new Error("WebSocket implementation missing");const T=b,h=(()=>{const e=(()=>{const e={};return{on:(n,o)=>(e[n]=o,()=>{delete e[n]}),emit(n){var o;"id"in n&&(null===(o=e[n.id])||void 0===o||o.call(e,n))}}})(),n={connecting:(null==g?void 0:g.connecting)?[g.connecting]:[],connected:(null==g?void 0:g.connected)?[g.connected]:[],ping:(null==g?void 0:g.ping)?[g.ping]:[],pong:(null==g?void 0:g.pong)?[g.pong]:[],message:(null==g?void 0:g.message)?[e.emit,g.message]:[e.emit],closed:(null==g?void 0:g.closed)?[g.closed]:[],error:(null==g?void 0:g.error)?[g.error]:[]};return{onMessage:e.on,on(e,o){const t=n[e];return t.push(o),()=>{t.splice(t.indexOf(o),1)}},emit(e,...o){for(const t of n[e])t(...o)}}})();let S,C=0,N=!1,P=0,E=!1;async function O(){const[n,r]=await(null!=S?S:S=new Promise(((n,r)=>(async()=>{if(N){if(await u(P),!C)return S=void 0,r({code:1e3,reason:"All Subscriptions Gone"});P++}h.emit("connecting");const i=new T("function"==typeof o?await o():o,a);let s;function d(){isFinite(c)&&c>0&&(clearTimeout(s),s=setTimeout((()=>{i.readyState===T.OPEN&&(i.send(p({type:e.MessageType.Ping})),h.emit("ping",!1))}),c))}i.onerror=e=>{h.emit("error",e)},i.onclose=e=>{S=void 0,clearTimeout(s),h.emit("closed",e),r(e)},i.onopen=async()=>{try{i.send(p({type:e.MessageType.ConnectionInit,payload:"function"==typeof t?await t():t},w)),d()}catch(e){i.close(4400,e instanceof Error?e.message:new Error(e).message)}};let y=!1;i.onmessage=({data:o})=>{try{const t=l(o,v);if(h.emit("message",t),"ping"===t.type||"pong"===t.type)return h.emit(t.type,!0),void("ping"===t.type?(i.send(p({type:e.MessageType.Pong})),h.emit("pong",!1)):d());if(y)return;if(t.type!==e.MessageType.ConnectionAck)throw new Error(`First message cannot be of type ${t.type}`);y=!0,h.emit("connected",i,t.payload),N=!1,P=0,n([i,new Promise(((e,n)=>i.addEventListener("close",n)))])}catch(e){i.close(4400,e instanceof Error?e.message:new Error(e).message)}}})())));n.readyState===T.CLOSING&&await r;let i=()=>{};const d=new Promise((e=>i=e));return[n,i,Promise.race([d.then((()=>{if(!C){const e=()=>n.close(1e3,"Normal Closure");isFinite(s)&&s>0?setTimeout((()=>{C||n.readyState!==T.OPEN||e()}),s):e()}})),r])]}function k(e){if(d(e)&&[1002,1011,4400,4401,4409,4429].includes(e.code))throw e;if(E)return!1;if(d(e)&&1e3===e.code)return C>0;if(!y||P>=y)throw e;if(f(e))throw e;return N=!0}return r||(async()=>{for(C++;;)try{const[,,e]=await O();await e}catch(e){try{if(!k(e))return}catch(e){return null==i?void 0:i(e)}}})(),{on:h.on,subscribe(n,o){const t=x();let r=!1,i=!1,a=()=>{C--,r=!0};return(async()=>{for(C++;;)try{const[s,c,l]=await O();if(r)return c();const d=h.onMessage(t,(n=>{switch(n.type){case e.MessageType.Next:return void o.next(n.payload);case e.MessageType.Error:return i=!0,r=!0,o.error(n.payload),void a();case e.MessageType.Complete:return r=!0,void a()}}));return s.send(p({id:t,type:e.MessageType.Subscribe,payload:n},w)),a=()=>{r||s.readyState!==T.OPEN||s.send(p({id:t,type:e.MessageType.Complete},w)),C--,r=!0,c()},void await l.finally(d)}catch(e){if(!k(e))return}})().catch(o.error).then((()=>{i||o.complete()})),()=>{r||a()}},async dispose(){if(E=!0,S){const[e]=await S;e.close(1e3,"Normal Closure")}}}},e.isMessage=c,e.parseMessage=l,e.stringifyMessage=p,Object.defineProperty(e,"__esModule",{value:!0})})); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
269760
3737
1426