Improve graphql ws client connection loss handling

- Retry on all disconnect events
- Retry infinitely
- Cap retry backoff strategy delay at heartbeat interval (20s)
- Recreate client in case reconnecting isn't working
This commit is contained in:
schroda
2025-10-10 19:07:15 +02:00
parent 28a2b44e94
commit f961e734bc

View File

@@ -193,6 +193,8 @@ export class GraphQLClient extends BaseClient<
private wsClient!: Client; private wsClient!: Client;
private wsClientAliveCheckInterval: NodeJS.Timeout | undefined = undefined;
constructor(handleRefreshToken: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>) { constructor(handleRefreshToken: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>) {
super(handleRefreshToken); super(handleRefreshToken);
@@ -268,13 +270,22 @@ export class GraphQLClient extends BaseClient<
); );
} }
protected createClient() { private createWSClient(lazy: boolean = true): void {
const heartbeatInterval = 1000 * 20; const heartbeatInterval = 1000 * 20;
this.wsClient = createClient({ this.wsClient = createClient({
lazy,
url: () => this.getBaseUrl().replace(/http(|s)/g, 'ws'), url: () => this.getBaseUrl().replace(/http(|s)/g, 'ws'),
keepAlive: heartbeatInterval, keepAlive: heartbeatInterval,
retryAttempts: 10, retryAttempts: Number.MAX_SAFE_INTEGER,
shouldRetry: () => true,
retryWait: async (retries) => {
const delay = Math.min(1000 * 2 ** retries, heartbeatInterval);
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
},
connectionParams: () => { connectionParams: () => {
const isAuthRequired = AuthManager.isAuthRequired(); const isAuthRequired = AuthManager.isAuthRequired();
const accessToken = AuthManager.getAccessToken(); const accessToken = AuthManager.getAccessToken();
@@ -285,23 +296,26 @@ export class GraphQLClient extends BaseClient<
}, },
}); });
let lastHeartbeat: number = 0; let lastHeartbeat: number = Date.now();
this.wsClient.on('connected', () => {
lastHeartbeat = Date.now();
});
this.wsClient.on('pong', () => { this.wsClient.on('pong', () => {
lastHeartbeat = Date.now(); lastHeartbeat = Date.now();
}); });
const checkHeartbeatInterval = heartbeatInterval + 1000 * 10; const checkHeartbeatInterval = heartbeatInterval + 1000 * 30;
setInterval(() => { clearInterval(this.wsClientAliveCheckInterval);
this.wsClientAliveCheckInterval = setInterval(() => {
const isHeartbeatMissing = Date.now() - lastHeartbeat > checkHeartbeatInterval * 1.1; const isHeartbeatMissing = Date.now() - lastHeartbeat > checkHeartbeatInterval * 1.1;
if (isHeartbeatMissing) { if (isHeartbeatMissing) {
// force a reconnect this.wsClient.dispose();
this.wsClient.terminate();
this.createWSClient(false);
this.client.setLink(this.createLink());
} }
}, checkHeartbeatInterval); }, checkHeartbeatInterval);
}
protected createClient() {
this.createWSClient();
this.client = new ApolloClient({ this.client = new ApolloClient({
cache: new InMemoryCache({ cache: new InMemoryCache({
// for whatever reason there is some weird TypeError complaining that // for whatever reason there is some weird TypeError complaining that