diff --git a/docs/enums/common.CloseCode.md b/docs/enums/common.CloseCode.md index 56f3e3e6..be09deb9 100644 --- a/docs/enums/common.CloseCode.md +++ b/docs/enums/common.CloseCode.md @@ -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) @@ -27,6 +28,12 @@ ___ +### ConnectionAcknowledgementTimeout + +• **ConnectionAcknowledgementTimeout** = `4504` + +___ + ### ConnectionInitialisationTimeout • **ConnectionInitialisationTimeout** = `4408` diff --git a/docs/interfaces/client.ClientOptions.md b/docs/interfaces/client.ClientOptions.md index 41655403..66ff6063 100644 --- a/docs/interfaces/client.ClientOptions.md +++ b/docs/interfaces/client.ClientOptions.md @@ -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) @@ -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`\>\> diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 8b3d3cd4..7c965e08 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -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); diff --git a/src/client.ts b/src/client.ts index 26755806..fc215a62 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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. * @@ -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 @@ -562,7 +577,8 @@ export function createClient(options: ClientOptions): Client { GRAPHQL_TRANSPORT_WS_PROTOCOL, ); - let queuedPing: ReturnType; + let connectionAckTimeout: ReturnType, + queuedPing: ReturnType; function enqueuePing() { if (isFinite(keepAlive) && keepAlive > 0) { clearTimeout(queuedPing); // in case where a pong was received before a ping (this is valid behaviour) @@ -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); @@ -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( @@ -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 @@ -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)) diff --git a/src/common.ts b/src/common.ts index ae08b48d..b6bfa84e 100644 --- a/src/common.ts +++ b/src/common.ts @@ -33,6 +33,7 @@ export enum CloseCode { Forbidden = 4403, SubprotocolNotAcceptable = 4406, ConnectionInitialisationTimeout = 4408, + ConnectionAcknowledgementTimeout = 4504, /** Subscriber distinction is very important */ SubscriberAlreadyExists = 4409, TooManyInitialisationRequests = 4429,