diff --git a/PROTOCOL.md b/PROTOCOL.md index 3af2e020..28f70036 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -66,9 +66,12 @@ A `Pong` must be sent in response from the receiving party as soon as possible. The `Ping` message can be sent at any time within the established socket. +The optional `payload` field can be used to transfer additional details about the ping. + ```typescript interface PingMessage { type: 'ping'; + payload?: Record; } ``` @@ -80,9 +83,12 @@ The response to the `Ping` message. Must be sent as soon as the `Ping` message i The `Pong` message can be sent at any time within the established socket. Furthermore, the `Pong` message may even be sent unsolicited as an unidirectional heartbeat. +The optional `payload` field can be used to transfer additional details about the pong. + ```typescript interface PongMessage { type: 'pong'; + payload?: Record; } ``` diff --git a/README.md b/README.md index 5989da94..aa92451f 100644 --- a/README.md +++ b/README.md @@ -625,6 +625,64 @@ createClient({ +
+🔗 Client usage with manual pings and pongs + +```typescript +import { + createClient, + Client, + ClientOptions, + stringifyMessage, + PingMessage, + PongMessage, + MessageType, +} from 'graphql-ws'; + +interface PingerClient extends Client { + ping(payload?: PingMessage['payload']): void; + pong(payload?: PongMessage['payload']): void; +} + +function createPingerClient(options: ClientOptions): PingerClient { + let activeSocket: WebSocket; + + const client = createClient({ + ...options, + on: { + connected: (socket) => { + options.on?.connected?.(socket); + activeSocket = socket; + }, + }, + }); + + return { + ...client, + ping: (payload) => { + if (activeSocket.readyState === WebSocket.OPEN) + activeSocket.send( + stringifyMessage({ + type: MessageType.Ping, + payload, + }), + ); + }, + pong: (payload) => { + if (activeSocket.readyState === WebSocket.OPEN) + activeSocket.send( + stringifyMessage({ + type: MessageType.Pong, + payload, + }), + ); + }, + }; +} +``` + +
+
🔗 Client usage in browser diff --git a/docs/interfaces/common.pingmessage.md b/docs/interfaces/common.pingmessage.md index 23860a81..77ba68b4 100644 --- a/docs/interfaces/common.pingmessage.md +++ b/docs/interfaces/common.pingmessage.md @@ -8,10 +8,17 @@ ### Properties +- [payload](common.pingmessage.md#payload) - [type](common.pingmessage.md#type) ## Properties +### payload + +• `Optional` `Readonly` **payload**: `Record` + +___ + ### type • `Readonly` **type**: [Ping](../enums/common.messagetype.md#ping) diff --git a/docs/interfaces/common.pongmessage.md b/docs/interfaces/common.pongmessage.md index 92d0d509..1f6ac2a2 100644 --- a/docs/interfaces/common.pongmessage.md +++ b/docs/interfaces/common.pongmessage.md @@ -8,10 +8,17 @@ ### Properties +- [payload](common.pongmessage.md#payload) - [type](common.pongmessage.md#type) ## Properties +### payload + +• `Optional` `Readonly` **payload**: `Record` + +___ + ### type • `Readonly` **type**: [Pong](../enums/common.messagetype.md#pong) diff --git a/docs/modules/client.md b/docs/modules/client.md index 54f13581..7701c23e 100644 --- a/docs/modules/client.md +++ b/docs/modules/client.md @@ -227,20 +227,21 @@ ___ ### EventPingListener -Ƭ **EventPingListener**: (`received`: `boolean`) => `void` +Ƭ **EventPingListener**: (`received`: `boolean`, `payload`: [PingMessage](../interfaces/common.pingmessage.md)[``"payload"``]) => `void` The first argument communicates whether the ping was received from the server. If `false`, the ping was sent by the client. #### Type declaration -▸ (`received`): `void` +▸ (`received`, `payload`): `void` ##### Parameters | Name | Type | | :------ | :------ | | `received` | `boolean` | +| `payload` | [PingMessage](../interfaces/common.pingmessage.md)[``"payload"``] | ##### Returns @@ -256,20 +257,21 @@ ___ ### EventPongListener -Ƭ **EventPongListener**: (`received`: `boolean`) => `void` +Ƭ **EventPongListener**: (`received`: `boolean`, `payload`: [PongMessage](../interfaces/common.pongmessage.md)[``"payload"``]) => `void` The first argument communicates whether the pong was received from the server. If `false`, the pong was sent by the client. #### Type declaration -▸ (`received`): `void` +▸ (`received`, `payload`): `void` ##### Parameters | Name | Type | | :------ | :------ | | `received` | `boolean` | +| `payload` | [PongMessage](../interfaces/common.pongmessage.md)[``"payload"``] | ##### Returns diff --git a/docs/modules/common.md b/docs/modules/common.md index 3071fe87..5ba73aff 100644 --- a/docs/modules/common.md +++ b/docs/modules/common.md @@ -129,7 +129,7 @@ ___ ### isMessage -▸ **isMessage**(`val`): val is ConnectionInitMessage \| ConnectionAckMessage \| PingMessage \| PongMessage \| SubscribeMessage \| NextMessage \| ErrorMessage \| CompleteMessage +▸ **isMessage**(`val`): val is PingMessage \| PongMessage \| ConnectionInitMessage \| ConnectionAckMessage \| SubscribeMessage \| NextMessage \| ErrorMessage \| CompleteMessage Checks if the provided value is a message. @@ -141,7 +141,7 @@ Checks if the provided value is a message. #### Returns -val is ConnectionInitMessage \| ConnectionAckMessage \| PingMessage \| PongMessage \| SubscribeMessage \| NextMessage \| ErrorMessage \| CompleteMessage +val is PingMessage \| PongMessage \| ConnectionInitMessage \| ConnectionAckMessage \| SubscribeMessage \| NextMessage \| ErrorMessage \| CompleteMessage ___ diff --git a/src/client.ts b/src/client.ts index ba69d634..b2721a8f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,8 @@ import { Disposable, Message, MessageType, + PingMessage, + PongMessage, parseMessage, stringifyMessage, SubscribePayload, @@ -77,7 +79,10 @@ export type EventConnectingListener = () => void; * * @category Client */ -export type EventPingListener = (received: boolean) => void; +export type EventPingListener = ( + received: boolean, + payload: PingMessage['payload'], +) => void; /** * The first argument communicates whether the pong was received from the server. @@ -85,7 +90,10 @@ export type EventPingListener = (received: boolean) => void; * * @category Client */ -export type EventPongListener = (received: boolean) => void; +export type EventPongListener = ( + received: boolean, + payload: PongMessage['payload'], +) => void; /** * Called for all **valid** messages received by the client. Mainly useful for @@ -490,7 +498,7 @@ export function createClient(options: ClientOptions): Client { queuedPing = setTimeout(() => { if (socket.readyState === WebSocketImpl.OPEN) { socket.send(stringifyMessage({ type: MessageType.Ping })); - emitter.emit('ping', false); + emitter.emit('ping', false, undefined); } }, keepAlive); } @@ -537,11 +545,11 @@ export function createClient(options: ClientOptions): Client { const message = parseMessage(data, reviver); emitter.emit('message', message); if (message.type === 'ping' || message.type === 'pong') { - emitter.emit(message.type, true); // received + emitter.emit(message.type, true, message.payload); // received if (message.type === 'ping') { // respond with pong on ping socket.send(stringifyMessage({ type: MessageType.Pong })); - emitter.emit('pong', false); + emitter.emit('pong', false, undefined); } else enqueuePing(); // enqueue next ping on pong (noop if disabled) return; // ping and pongs can be received whenever } diff --git a/src/common.ts b/src/common.ts index a7b8786d..d03d4247 100644 --- a/src/common.ts +++ b/src/common.ts @@ -88,11 +88,13 @@ export interface ConnectionAckMessage { /** @category Common */ export interface PingMessage { readonly type: MessageType.Ping; + readonly payload?: Record; } /** @category Common */ export interface PongMessage { readonly type: MessageType.Pong; + readonly payload?: Record; } /** @category Common */ @@ -171,16 +173,14 @@ export function isMessage(val: unknown): val is Message { isObject(val.payload) ); case MessageType.ConnectionAck: - // the connection ack message can have optional payload object too + case MessageType.Ping: + case MessageType.Pong: + // the connection ack, ping and pong messages can have optional payload object too return ( !hasOwnProperty(val, 'payload') || val.payload === undefined || isObject(val.payload) ); - case MessageType.Ping: - case MessageType.Pong: - // ping and pong types are simply valid - return true; case MessageType.Subscribe: return ( hasOwnStringProperty(val, 'id') && diff --git a/src/tests/client.ts b/src/tests/client.ts index 3d174d71..52d2c9de 100644 --- a/src/tests/client.ts +++ b/src/tests/client.ts @@ -1596,11 +1596,15 @@ describe('events', () => { expect(pingFn).toBeCalledTimes(2); expect(pingFn.mock.calls[0][0]).toBeFalsy(); + expect(pingFn.mock.calls[0][1]).toBeUndefined(); expect(pingFn.mock.calls[1][0]).toBeFalsy(); + expect(pingFn.mock.calls[1][1]).toBeUndefined(); expect(pongFn).toBeCalledTimes(2); expect(pongFn.mock.calls[0][0]).toBeTruthy(); + expect(pongFn.mock.calls[0][1]).toBeUndefined(); expect(pongFn.mock.calls[1][0]).toBeTruthy(); + expect(pongFn.mock.calls[1][1]).toBeUndefined(); }); it('should emit ping and pong events when receiving server pings', async () => { @@ -1623,7 +1627,9 @@ describe('events', () => { client.on('pong', pongFn); await server.waitForClient((client) => { - client.send(stringifyMessage({ type: MessageType.Ping })); + client.send( + stringifyMessage({ type: MessageType.Ping, payload: { some: 'data' } }), + ); }); await new Promise((resolve) => { @@ -1632,10 +1638,14 @@ describe('events', () => { expect(pingFn).toBeCalledTimes(2); expect(pingFn.mock.calls[0][0]).toBeTruthy(); + expect(pingFn.mock.calls[0][1]).toEqual({ some: 'data' }); expect(pingFn.mock.calls[1][0]).toBeTruthy(); + expect(pingFn.mock.calls[1][1]).toEqual({ some: 'data' }); expect(pongFn).toBeCalledTimes(2); expect(pongFn.mock.calls[0][0]).toBeFalsy(); + expect(pongFn.mock.calls[0][1]).toBeUndefined(); expect(pongFn.mock.calls[1][0]).toBeFalsy(); + expect(pongFn.mock.calls[1][1]).toBeUndefined(); }); });