Skip to content

Commit

Permalink
feat(client): Add connectionAckWaitTimeout option (enisdenjo#228)
Browse files Browse the repository at this point in the history
Co-authored-by: enisdenjo <[email protected]>
  • Loading branch information
D34THWINGS and enisdenjo authored Sep 7, 2021
1 parent 8a0da71 commit 35ce054
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 1 deletion.
7 changes: 7 additions & 0 deletions docs/enums/common.CloseCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Enumeration members

- [BadRequest](common.CloseCode.md#badrequest)
- [ConnectionAcknowledgementTimeout](common.CloseCode.md#connectionacknowledgementtimeout)
- [ConnectionInitialisationTimeout](common.CloseCode.md#connectioninitialisationtimeout)
- [Forbidden](common.CloseCode.md#forbidden)
- [InternalServerError](common.CloseCode.md#internalservererror)
Expand All @@ -27,6 +28,12 @@

___

### ConnectionAcknowledgementTimeout

**ConnectionAcknowledgementTimeout** = `4504`

___

### ConnectionInitialisationTimeout

**ConnectionInitialisationTimeout** = `4408`
Expand Down
19 changes: 19 additions & 0 deletions docs/interfaces/client.ClientOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Configuration used for the GraphQL over WebSocket client.

### Properties

- [connectionAckWaitTimeout](client.ClientOptions.md#connectionackwaittimeout)
- [connectionParams](client.ClientOptions.md#connectionparams)
- [disablePong](client.ClientOptions.md#disablepong)
- [jsonMessageReplacer](client.ClientOptions.md#jsonmessagereplacer)
Expand All @@ -31,6 +32,24 @@ Configuration used for the GraphQL over WebSocket client.

## Properties

### connectionAckWaitTimeout

`Optional` **connectionAckWaitTimeout**: `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

___

### connectionParams

`Optional` **connectionParams**: `Record`<`string`, `unknown`\> \| () => `undefined` \| `Record`<`string`, `unknown`\> \| `Promise`<`undefined` \| `Record`<`string`, `unknown`\>\>
Expand Down
25 changes: 25 additions & 0 deletions src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,31 @@ it('should use a custom JSON message replacer function', async (done) => {
});
});

it('should close socket if connection not acknowledged', async (done) => {
const { url, ...server } = await startTServer({
onConnect: () =>
new Promise(() => {
// never acknowledge
}),
});

const client = createClient({
url,
lazy: false,
retryAttempts: 0,
onNonLazyError: noop,
connectionAckWaitTimeout: 10,
});

client.on('closed', async (err) => {
expect((err as CloseEvent).code).toBe(
CloseCode.ConnectionAcknowledgementTimeout,
);
await server.dispose();
done();
});
});

describe('ping/pong', () => {
it('should respond with a pong to a ping', async () => {
expect.assertions(1);
Expand Down
34 changes: 33 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ export interface ClientOptions {
* @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.
*
Expand Down Expand Up @@ -423,6 +437,7 @@ export function createClient(options: ClientOptions): Client {
lazyCloseTimeout = 0,
keepAlive = 0,
disablePong,
connectionAckWaitTimeout = 0,
retryAttempts = 5,
retryWait = async function randomisedExponentialBackoff(retries) {
let retryDelay = 1000; // start with 1s delay
Expand Down Expand Up @@ -562,7 +577,8 @@ export function createClient(options: ClientOptions): Client {
GRAPHQL_TRANSPORT_WS_PROTOCOL,
);

let queuedPing: ReturnType<typeof setTimeout>;
let connectionAckTimeout: ReturnType<typeof setTimeout>,
queuedPing: ReturnType<typeof setTimeout>;
function enqueuePing() {
if (isFinite(keepAlive) && keepAlive > 0) {
clearTimeout(queuedPing); // in case where a pong was received before a ping (this is valid behaviour)
Expand All @@ -582,6 +598,7 @@ export function createClient(options: ClientOptions): Client {

socket.onclose = (event) => {
connecting = undefined;
clearTimeout(connectionAckTimeout);
clearTimeout(queuedPing);
emitter.emit('closed', event);
denied(event);
Expand All @@ -608,6 +625,19 @@ export function createClient(options: ClientOptions): Client {
replacer,
),
);

if (
isFinite(connectionAckWaitTimeout) &&
connectionAckWaitTimeout > 0
) {
connectionAckTimeout = setTimeout(() => {
socket.close(
CloseCode.ConnectionAcknowledgementTimeout,
'Connection acknowledgement timeout',
);
}, connectionAckWaitTimeout);
}

enqueuePing(); // enqueue ping (noop if disabled)
} catch (err) {
socket.close(
Expand Down Expand Up @@ -651,6 +681,7 @@ export function createClient(options: ClientOptions): Client {
throw new Error(
`First message cannot be of type ${message.type}`,
);
clearTimeout(connectionAckTimeout);
acknowledged = true;
emitter.emit('connected', socket, message.payload); // connected = socket opened + acknowledged
retrying = false; // future lazy connects are not retries
Expand Down Expand Up @@ -723,6 +754,7 @@ export function createClient(options: ClientOptions): Client {
// 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,
].includes(errOrCloseEvent.code))
Expand Down
1 change: 1 addition & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum CloseCode {
Forbidden = 4403,
SubprotocolNotAcceptable = 4406,
ConnectionInitialisationTimeout = 4408,
ConnectionAcknowledgementTimeout = 4504,
/** Subscriber distinction is very important */
SubscriberAlreadyExists = 4409,
TooManyInitialisationRequests = 4429,
Expand Down

0 comments on commit 35ce054

Please sign in to comment.