Skip to content

Commit

Permalink
feat: Optional payload for ping/pong message types
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo committed Jun 9, 2021
1 parent 169b47d commit 2fe0345
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 17 deletions.
6 changes: 6 additions & 0 deletions PROTOCOL.md
Expand Up @@ -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<string, unknown>;
}
```

Expand All @@ -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<string, unknown>;
}
```

Expand Down
58 changes: 58 additions & 0 deletions README.md
Expand Up @@ -625,6 +625,64 @@ createClient({

</details>

<details id="custom-client-pinger">
<summary><a href="#custom-client-pinger">🔗</a> Client usage with manual pings and pongs</summary>

```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,
}),
);
},
};
}
```

</details>

<details id="browser">
<summary><a href="#browser">🔗</a> Client usage in browser</summary>

Expand Down
7 changes: 7 additions & 0 deletions docs/interfaces/common.pingmessage.md
Expand Up @@ -8,10 +8,17 @@

### Properties

- [payload](common.pingmessage.md#payload)
- [type](common.pingmessage.md#type)

## Properties

### payload

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

___

### type

`Readonly` **type**: [Ping](../enums/common.messagetype.md#ping)
7 changes: 7 additions & 0 deletions docs/interfaces/common.pongmessage.md
Expand Up @@ -8,10 +8,17 @@

### Properties

- [payload](common.pongmessage.md#payload)
- [type](common.pongmessage.md#type)

## Properties

### payload

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

___

### type

`Readonly` **type**: [Pong](../enums/common.messagetype.md#pong)
10 changes: 6 additions & 4 deletions docs/modules/client.md
Expand Up @@ -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

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/modules/common.md
Expand Up @@ -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.

Expand All @@ -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

___

Expand Down
18 changes: 13 additions & 5 deletions src/client.ts
Expand Up @@ -11,6 +11,8 @@ import {
Disposable,
Message,
MessageType,
PingMessage,
PongMessage,
parseMessage,
stringifyMessage,
SubscribePayload,
Expand Down Expand Up @@ -77,15 +79,21 @@ 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.
* If `false`, the pong was sent by the client.
*
* @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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions src/common.ts
Expand Up @@ -88,11 +88,13 @@ export interface ConnectionAckMessage {
/** @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 */
Expand Down Expand Up @@ -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') &&
Expand Down
12 changes: 11 additions & 1 deletion src/tests/client.ts
Expand Up @@ -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 () => {
Expand All @@ -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<void>((resolve) => {
Expand All @@ -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();
});
});

0 comments on commit 2fe0345

Please sign in to comment.