Skip to content

Commit

Permalink
feat(client): isFatalConnectionProblem option for deciding if the c…
Browse files Browse the repository at this point in the history
…onnect error should be immediately reported or the connection retried (enisdenjo#126)

Closes: enisdenjo#122
  • Loading branch information
enisdenjo committed Feb 25, 2021
1 parent d99eac8 commit 8115871
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 8 deletions.
21 changes: 21 additions & 0 deletions docs/interfaces/client.clientoptions.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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*
Expand Down
38 changes: 30 additions & 8 deletions src/client.ts
Expand Up @@ -132,6 +132,22 @@ export interface ClientOptions {
* @default Randomised exponential backoff
*/
retryWait?: (retries: number) => Promise<void>;
/**
* 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.
Expand Down Expand Up @@ -194,6 +210,9 @@ export function createClient(options: ClientOptions): Client {
),
);
},
isFatalConnectionProblem = (errOrCloseEvent) =>
// non `CloseEvent`s are fatal by default
!isLikeCloseEvent(errOrCloseEvent),
on,
webSocketImpl,
/**
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/tests/client.ts
Expand Up @@ -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 }',
Expand Down Expand Up @@ -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',
);
Expand Down

0 comments on commit 8115871

Please sign in to comment.