diff --git a/docs/interfaces/client.clientoptions.md b/docs/interfaces/client.clientoptions.md index 54468a0d..821273fc 100644 --- a/docs/interfaces/client.clientoptions.md +++ b/docs/interfaces/client.clientoptions.md @@ -12,6 +12,7 @@ Configuration used for the GraphQL over WebSocket client. - [connectionParams](client.clientoptions.md#connectionparams) - [generateID](client.clientoptions.md#generateid) +- [isFatalConnectionProblem](client.clientoptions.md#isfatalconnectionproblem) - [keepAlive](client.clientoptions.md#keepalive) - [lazy](client.clientoptions.md#lazy) - [on](client.clientoptions.md#on) @@ -53,6 +54,26 @@ Reference: https://stackoverflow.com/a/2117523/709884 ___ +### isFatalConnectionProblem + +• `Optional` **isFatalConnectionProblem**: *undefined* \| (`errOrCloseEvent`: *unknown*) => *boolean* + +Check if the close event or connection error is fatal. If you return `true`, +the client will fail immediately without additional retries; however, if you +return `false`, the client will keep retrying until the `retryAttempts` have +been exceeded. + +The argument is either a WebSocket `CloseEvent` or an error thrown during +the connection phase. + +Beware, the library classifies a few close events as fatal regardless of +what is returned. They are listed in the documentation of the `retryAttempts` +option. + +**`default`** Non close events + +___ + ### keepAlive • `Optional` **keepAlive**: *undefined* \| *number* diff --git a/src/client.ts b/src/client.ts index 3642069f..3a414dc3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -132,6 +132,22 @@ export interface ClientOptions { * @default Randomised exponential backoff */ retryWait?: (retries: number) => Promise; + /** + * Check if the close event or connection error is fatal. If you return `true`, + * the client will fail immediately without additional retries; however, if you + * return `false`, the client will keep retrying until the `retryAttempts` have + * been exceeded. + * + * The argument is either a WebSocket `CloseEvent` or an error thrown during + * the connection phase. + * + * Beware, the library classifies a few close events as fatal regardless of + * what is returned. They are listed in the documentation of the `retryAttempts` + * option. + * + * @default Non close events + */ + isFatalConnectionProblem?: (errOrCloseEvent: unknown) => boolean; /** * Register listeners before initialising the client. This way * you can ensure to catch all client relevant emitted events. @@ -194,6 +210,9 @@ export function createClient(options: ClientOptions): Client { ), ); }, + isFatalConnectionProblem = (errOrCloseEvent) => + // non `CloseEvent`s are fatal by default + !isLikeCloseEvent(errOrCloseEvent), on, webSocketImpl, /** @@ -368,17 +387,12 @@ export function createClient(options: ClientOptions): Client { } /** - * Checks the `connect` problem and evaluates if the client should - * retry. If the problem is worth throwing, it will be thrown immediately. + * Checks the `connect` problem and evaluates if the client should retry. */ function shouldRetryConnectOrThrow(errOrCloseEvent: unknown): boolean { - // throw non `CloseEvent`s immediately, something else is wrong - if (!isLikeCloseEvent(errOrCloseEvent)) { - throw errOrCloseEvent; - } - // some close codes are worth reporting immediately if ( + isLikeCloseEvent(errOrCloseEvent) && [ 1002, // Protocol Error 1011, // Internal Error @@ -392,7 +406,10 @@ export function createClient(options: ClientOptions): Client { } // disposed or normal closure (completed), shouldnt try again - if (disposed || errOrCloseEvent.code === 1000) { + if ( + disposed || + (isLikeCloseEvent(errOrCloseEvent) && errOrCloseEvent.code === 1000) + ) { return false; } @@ -401,6 +418,11 @@ export function createClient(options: ClientOptions): Client { throw errOrCloseEvent; } + // throw fatal connection problems immediately + if (isFatalConnectionProblem(errOrCloseEvent)) { + throw errOrCloseEvent; + } + // looks good, start retrying retrying = true; return true; diff --git a/src/tests/client.ts b/src/tests/client.ts index ea9f8d7a..337d1078 100644 --- a/src/tests/client.ts +++ b/src/tests/client.ts @@ -928,6 +928,7 @@ describe('reconnecting', () => { createClient({ url, retryAttempts: Infinity, // keep retrying forever + isFatalConnectionProblem: () => true, // even if all connection probles are fatal }), { query: 'subscription { ping }', @@ -958,6 +959,33 @@ describe('reconnecting', () => { await testCloseCode(4429); }); + it('should report fatal connection problems immediately', async () => { + const { url, ...server } = await startTServer(); + + const sub = tsubscribe( + createClient({ + url, + retryAttempts: Infinity, // keep retrying forever + isFatalConnectionProblem: (err) => { + expect((err as CloseEvent).code).toBe(4444); + expect((err as CloseEvent).reason).toBe('Is fatal?'); + return true; + }, + }), + { + query: 'subscription { ping }', + }, + ); + + await server.waitForClient((client) => { + client.close(4444, 'Is fatal?'); + }); + + await sub.waitForError((err) => { + expect((err as CloseEvent).code).toBe(4444); + }, 20); + }); + it.todo( 'should attempt reconnecting silently a few times before closing for good', );