From 7cdb62e75cad56098ee81eabbcc63382f93fd218 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri <3846977+santhoshvai@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:41:46 +0200 Subject: [PATCH] fix: do not always error out api calls when web socket initially failed (#1495) ## Problem Currently for all axios requests, we wait for `connectionIdPromise`. This promise is rejected or resolved based on the first WS connect attempt. If subsequent reconnects were successful but the first connect was rejected, this promise would still stay rejected. ## Solution To allow for reconnects to resolve, say for example, in 3rd try the connection got open, we can wait for the `connectionOpen` promise to resolve. To allow for 2 or 3 retries, we use `_waitForHealthy` function here. Alternatively we could await only on `connectionOpen` promise without allowing for timeouts to wait for reconnections also. --- .../src/coordinator/connection/client.ts | 38 ++++++++++++------- .../src/coordinator/connection/connection.ts | 11 ++++++ .../src/coordinator/connection/utils.ts | 11 ++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/client/src/coordinator/connection/client.ts b/packages/client/src/coordinator/connection/client.ts index 99e4ff8b12..d52cec2464 100644 --- a/packages/client/src/coordinator/connection/client.ts +++ b/packages/client/src/coordinator/connection/client.ts @@ -359,12 +359,7 @@ export class StreamClient { return Promise.resolve(); } - this.connectionIdPromise = new Promise( - (resolve, reject) => { - this.resolveConnectionId = resolve; - this.rejectConnectionId = reject; - }, - ); + this._setupConnectionIdPromise(); this.clientID = `${this.userID}--${randomId()}`; this.wsPromise = this.connect(); @@ -428,12 +423,7 @@ export class StreamClient { tokenOrProvider: TokenOrProvider, ) => { addConnectionEventListeners(this.updateNetworkConnectionStatus); - this.connectionIdPromise = new Promise( - (resolve, reject) => { - this.resolveConnectionId = resolve; - this.rejectConnectionId = reject; - }, - ); + this._setupConnectionIdPromise(); this.anonymous = true; await this._setToken(user, tokenOrProvider, this.anonymous); @@ -487,6 +477,19 @@ export class StreamClient { ); }; + /** + * sets up the this.connectionIdPromise + */ + _setupConnectionIdPromise = async () => { + /** a promise that is resolved once connection id is set */ + this.connectionIdPromise = new Promise( + (resolve, reject) => { + this.resolveConnectionId = resolve; + this.rejectConnectionId = reject; + }, + ); + }; + _logApiRequest = ( type: string, url: string, @@ -534,8 +537,17 @@ export class StreamClient { await Promise.all([ this.tokenManager.tokenReady(), this.guestUserCreatePromise, - this.connectionIdPromise, ]); + // we need to wait for presence of connection id before making requests + try { + await this.connectionIdPromise; + } catch (e) { + // in case connection id was rejected + // reconnection maybe in progress + // we can wait for healthy connection to resolve, which rejects when 15s timeout is reached + await this.wsConnection?._waitForHealthy(); + await this.connectionIdPromise; + } } const requestConfig = this._enrichAxiosOptions(options); try { diff --git a/packages/client/src/coordinator/connection/connection.ts b/packages/client/src/coordinator/connection/connection.ts index e95c863a8e..9a2f04bdbb 100644 --- a/packages/client/src/coordinator/connection/connection.ts +++ b/packages/client/src/coordinator/connection/connection.ts @@ -8,6 +8,7 @@ import { import { addConnectionEventListeners, convertErrorToJson, + isPromisePending, KnownCodes, randomId, removeConnectionEventListeners, @@ -337,6 +338,15 @@ export class StableWSConnection { await this.client.tokenManager.loadToken(); } + let mustSetupConnectionIdPromise = true; + if (this.client.connectionIdPromise) { + if (await isPromisePending(this.client.connectionIdPromise)) { + mustSetupConnectionIdPromise = false; + } + } + if (mustSetupConnectionIdPromise) { + this.client._setupConnectionIdPromise(); + } this._setupConnectionPromise(); const wsURL = this._buildUrl(); this._log(`_connect() - Connecting to ${wsURL}`, { @@ -369,6 +379,7 @@ export class StableWSConnection { return response; } } catch (err) { + await this.client._setupConnectionIdPromise(); this.isConnecting = false; // @ts-ignore this._log(`_connect() - Error - `, err); diff --git a/packages/client/src/coordinator/connection/utils.ts b/packages/client/src/coordinator/connection/utils.ts index 73403b6c55..2276e7c006 100644 --- a/packages/client/src/coordinator/connection/utils.ts +++ b/packages/client/src/coordinator/connection/utils.ts @@ -110,6 +110,17 @@ export function convertErrorToJson(err: Error) { return jsonObj; } +/** + * Informs if a promise is yet to be resolved or rejected + */ +export async function isPromisePending(promise: Promise) { + const emptyObj = {}; + return Promise.race([promise, emptyObj]).then( + (value) => (value === emptyObj ? true : false), + () => false, + ); +} + /** * isOnline safely return the navigator.online value for browser env * if navigator is not in global object, it always return true