Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): isFatalConnectionProblem option for deciding if the connect error should be immediately reported or the connection retried #126

Merged
merged 6 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/interfaces/client.clientoptions.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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