diff --git a/packages/client/package.json b/packages/client/package.json index 62c280241..65810f495 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -33,7 +33,6 @@ "@protobuf-ts/twirp-transport": "^2.9.4", "@types/ws": "^8.5.7", "axios": "^1.6.0", - "base64-js": "^1.5.1", "isomorphic-ws": "^5.0.0", "rxjs": "~7.8.1", "sdp-transform": "^2.14.1", diff --git a/packages/client/src/coordinator/connection/base64.ts b/packages/client/src/coordinator/connection/base64.ts index 070e041b8..e3eebfd0e 100644 --- a/packages/client/src/coordinator/connection/base64.ts +++ b/packages/client/src/coordinator/connection/base64.ts @@ -1,55 +1,3 @@ -import { fromByteArray } from 'base64-js'; - -function isString(arrayOrString: string | T[]): arrayOrString is string { - return typeof (arrayOrString as string) === 'string'; -} - -type MapGenericCallback = (value: T, index: number, array: T[]) => U; -type MapStringCallback = (value: string, index: number, string: string) => U; - -function isMapStringCallback( - arrayOrString: string | T[], - callback: MapGenericCallback | MapStringCallback, -): callback is MapStringCallback { - return !!callback && isString(arrayOrString); -} - -// source - https://github.com/beatgammit/base64-js/blob/master/test/convert.js#L72 -function map(array: T[], callback: MapGenericCallback): U[]; -function map(string: string, callback: MapStringCallback): U[]; -function map( - arrayOrString: string | T[], - callback: MapGenericCallback | MapStringCallback, -): U[] { - const res = []; - - if (isString(arrayOrString) && isMapStringCallback(arrayOrString, callback)) { - for (let k = 0, len = arrayOrString.length; k < len; k++) { - if (arrayOrString.charAt(k)) { - const kValue = arrayOrString.charAt(k); - const mappedValue = callback(kValue, k, arrayOrString); - res[k] = mappedValue; - } - } - } else if ( - !isString(arrayOrString) && - !isMapStringCallback(arrayOrString, callback) - ) { - for (let k = 0, len = arrayOrString.length; k < len; k++) { - if (k in arrayOrString) { - const kValue = arrayOrString[k]; - const mappedValue = callback(kValue, k, arrayOrString); - res[k] = mappedValue; - } - } - } - - return res; -} - -export const encodeBase64 = (data: string): string => - fromByteArray(new Uint8Array(map(data, (char) => char.charCodeAt(0)))); - // base-64 decoder throws exception if encoded string is not padded by '=' to make string length // in multiples of 4. So gonna use our own method for this purpose to keep backwards compatibility // https://github.com/beatgammit/base64-js/blob/master/index.js#L26 diff --git a/packages/client/src/coordinator/connection/client.ts b/packages/client/src/coordinator/connection/client.ts index 80e909395..32884fcc1 100644 --- a/packages/client/src/coordinator/connection/client.ts +++ b/packages/client/src/coordinator/connection/client.ts @@ -1,27 +1,22 @@ import axios, { AxiosError, - AxiosHeaders, AxiosInstance, AxiosRequestConfig, AxiosResponse, } from 'axios'; import https from 'https'; import { StableWSConnection } from './connection'; -import { DevToken } from './signing'; import { TokenManager } from './token_manager'; -import { WSConnectionFallback } from './connection_fallback'; -import { isErrorResponse, isWSFailure } from './errors'; import { addConnectionEventListeners, + isErrorResponse, isFunction, - isOnline, KnownCodes, randomId, removeConnectionEventListeners, retryInterval, sleep, } from './utils'; - import { AllClientEvents, AllClientEventTypes, @@ -36,7 +31,6 @@ import { User, UserWithId, } from './types'; -import { InsightMetrics, postInsights } from './insights'; import { getLocationHint } from './location'; import { CreateGuestRequest, CreateGuestResponse } from '../../gen/coordinator'; @@ -66,11 +60,8 @@ export class StreamClient { userID?: string; wsBaseURL?: string; wsConnection: StableWSConnection | null; - wsFallback?: WSConnectionFallback; wsPromise: ConnectAPIResponse | null; consecutiveFailures: number; - insightMetrics: InsightMetrics; - defaultWSTimeoutWithFallback: number; defaultWSTimeout: number; resolveConnectionId?: Function; rejectConnectionId?: Function; @@ -117,7 +108,6 @@ export class StreamClient { this.options = { timeout: 5000, withCredentials: false, // making sure cookies are not sent - warmUp: false, ...inputOptions, }; @@ -132,22 +122,6 @@ export class StreamClient { this.options.baseURL || 'https://video.stream-io-api.com/video', ); - if ( - typeof process !== 'undefined' && - 'env' in process && - process.env.STREAM_LOCAL_TEST_RUN - ) { - this.setBaseURL('http://localhost:3030/video'); - } - - if ( - typeof process !== 'undefined' && - 'env' in process && - process.env.STREAM_LOCAL_TEST_HOST - ) { - this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`); - } - this.axiosInstance = axios.create({ ...this.options, baseURL: this.baseURL, @@ -167,9 +141,7 @@ export class StreamClient { // generated from secret. this.tokenManager = new TokenManager(this.secret); this.consecutiveFailures = 0; - this.insightMetrics = new InsightMetrics(); - this.defaultWSTimeoutWithFallback = 6000; this.defaultWSTimeout = 15000; this.logger = isFunction(inputOptions.logger) @@ -177,10 +149,6 @@ export class StreamClient { : () => null; } - devToken = (userID: string) => { - return DevToken(userID); - }; - getAuthType = () => { return this.anonymous ? 'anonymous' : 'jwt'; }; @@ -207,8 +175,7 @@ export class StreamClient { return hint; }; - _getConnectionID = () => - this.wsConnection?.connectionID || this.wsFallback?.connectionID; + _getConnectionID = () => this.wsConnection?.connectionID; _hasConnectionID = () => Boolean(this._getConnectionID()); @@ -323,11 +290,7 @@ export class StreamClient { * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent */ closeConnection = async (timeout?: number) => { - await Promise.all([ - this.wsConnection?.disconnect(timeout), - this.wsFallback?.disconnect(timeout), - ]); - return Promise.resolve(); + await this.wsConnection?.disconnect(timeout); }; /** @@ -348,10 +311,7 @@ export class StreamClient { return this.wsPromise; } - if ( - (this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) && - this._hasConnectionID() - ) { + if (this.wsConnection?.isHealthy && this._hasConnectionID()) { this.logger( 'info', 'client:openConnection() - openConnection called twice, healthy connection already exists', @@ -686,89 +646,20 @@ export class StreamClient { 'Call connectUser or connectAnonymousUser before starting the connection', ); } - if (!this.wsBaseURL) { - throw Error('Websocket base url not set'); - } - if (!this.clientID) { - throw Error('clientID is not set'); - } + if (!this.wsBaseURL) throw Error('Websocket base url not set'); + if (!this.clientID) throw Error('clientID is not set'); - if ( - !this.wsConnection && - (this.options.warmUp || this.options.enableInsights) - ) { - this._sayHi(); - } // The StableWSConnection handles all the reconnection logic. if (this.options.wsConnection && this.node) { // Intentionally avoiding adding ts generics on wsConnection in options since its only useful for unit test purpose. - (this.options.wsConnection as unknown as StableWSConnection).setClient( - this, - ); - this.wsConnection = this.options - .wsConnection as unknown as StableWSConnection; + this.options.wsConnection.setClient(this); + this.wsConnection = this.options.wsConnection; } else { this.wsConnection = new StableWSConnection(this); } - try { - // if fallback is used before, continue using it instead of waiting for WS to fail - if (this.wsFallback) { - return await this.wsFallback.connect(); - } - this.logger('info', 'StreamClient.connect: this.wsConnection.connect()'); - // if WSFallback is enabled, ws connect should timeout faster so fallback can try - return await this.wsConnection.connect( - this.options.enableWSFallback - ? this.defaultWSTimeoutWithFallback - : this.defaultWSTimeout, - ); - } catch (err) { - // run fallback only if it's WS/Network error and not a normal API error - // make sure browser is online before even trying the longpoll - if ( - this.options.enableWSFallback && - // @ts-ignore - isWSFailure(err) && - isOnline(this.logger) - ) { - this.logger( - 'warn', - 'client:connect() - WS failed, fallback to longpoll', - ); - this.dispatchEvent({ type: 'transport.changed', mode: 'longpoll' }); - - this.wsConnection._destroyCurrentWSConnection(); - this.wsConnection.disconnect().then(); // close WS so no retry - this.wsFallback = new WSConnectionFallback(this); - return await this.wsFallback.connect(); - } - - throw err; - } - }; - - /** - * Check the connectivity with server for warmup purpose. - * - * @private - */ - _sayHi = () => { - const client_request_id = randomId(); - const opts = { - headers: AxiosHeaders.from({ - 'x-client-request-id': client_request_id, - }), - }; - this.doAxiosRequest('get', this.baseURL + '/hi', null, opts).catch((e) => { - if (this.options.enableInsights) { - postInsights('http_hi_failed', { - api_key: this.key, - err: e, - client_request_id, - }); - } - }); + this.logger('info', 'StreamClient.connect: this.wsConnection.connect()'); + return await this.wsConnection.connect(this.defaultWSTimeout); }; getUserAgent = () => { @@ -837,19 +728,6 @@ export class StreamClient { return this.tokenManager.getToken(); }; - /** - * encode ws url payload - * @private - * @returns json string - */ - _buildWSPayload = (client_request_id?: string) => { - return JSON.stringify({ - user_id: this.userID, - user_details: this._user, - client_request_id, - }); - }; - updateNetworkConnectionStatus = ( event: { type: 'online' | 'offline' } | Event, ) => { diff --git a/packages/client/src/coordinator/connection/connection.ts b/packages/client/src/coordinator/connection/connection.ts index 9a2f04bdb..aadfea679 100644 --- a/packages/client/src/coordinator/connection/connection.ts +++ b/packages/client/src/coordinator/connection/connection.ts @@ -1,13 +1,7 @@ import WebSocket from 'isomorphic-ws'; import { StreamClient } from './client'; -import { - buildWsFatalInsight, - buildWsSuccessAfterFailureInsight, - postInsights, -} from './insights'; import { addConnectionEventListeners, - convertErrorToJson, isPromisePending, KnownCodes, randomId, @@ -21,19 +15,18 @@ import type { StreamVideoEvent, UR, } from './types'; -import type { ConnectedEvent, WSAuthMessage } from '../../gen/coordinator'; +import { + ConnectedEvent, + ConnectionErrorEvent, + WSAuthMessage, +} from '../../gen/coordinator'; // Type guards to check WebSocket error type const isCloseEvent = ( - res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, + res: WebSocket.CloseEvent | ConnectionErrorEvent, ): res is WebSocket.CloseEvent => (res as WebSocket.CloseEvent).code !== undefined; -const isErrorEvent = ( - res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, -): res is WebSocket.ErrorEvent => - (res as WebSocket.ErrorEvent).error !== undefined; - /** * StableWSConnection - A WS connection that reconnects upon failure. * - the browser will sometimes report that you're online or offline @@ -55,7 +48,6 @@ export class StableWSConnection { // local vars connectionID?: string; connectionOpen?: ConnectAPIResponse; - authenticationSent: boolean; consecutiveFailures: number; pingInterval: number; healthCheckTimeoutRef?: NodeJS.Timeout; @@ -89,8 +81,6 @@ export class StableWSConnection { this.totalFailures = 0; /** We only make 1 attempt to reconnect at the same time.. */ this.isConnecting = false; - /** True after the auth payload is sent to the server */ - this.authenticationSent = false; /** To avoid reconnect if client is disconnected */ this.isDisconnected = false; /** Boolean that indicates if the connection promise is resolved */ @@ -224,12 +214,9 @@ export class StableWSConnection { */ _buildUrl = () => { const params = new URLSearchParams(); - // const qs = encodeURIComponent(this.client._buildWSPayload(this.requestID)); - // params.set('json', qs); params.set('api_key', this.client.key); params.set('stream-auth-type', this.client.getAuthType()); params.set('X-Stream-Client', this.client.getUserAgent()); - // params.append('authorization', this.client._getToken()!); return `${this.client.wsBaseURL}/connect?${params.toString()}`; }; @@ -313,14 +300,9 @@ export class StableWSConnection { * @return {ConnectAPIResponse} Promise that completes once the first health check message is received */ async _connect() { - if ( - this.isConnecting || - (this.isDisconnected && this.client.options.enableWSFallback) - ) - return; // simply ignore _connect if it's currently trying to connect + if (this.isConnecting) return; // simply ignore _connect if it's currently trying to connect this.isConnecting = true; this.requestID = randomId(); - this.client.insightMetrics.connectionStartTimestamp = new Date().getTime(); let isTokenReady = false; try { this._log(`_connect() - waiting for token`); @@ -364,18 +346,6 @@ export class StableWSConnection { if (response) { this.connectionID = response.connection_id; this.client.resolveConnectionId?.(this.connectionID); - if ( - this.client.insightMetrics.wsConsecutiveFailures > 0 && - this.client.options.enableInsights - ) { - postInsights( - 'ws_success_after_failure', - buildWsSuccessAfterFailureInsight( - this as unknown as StableWSConnection, - ), - ); - this.client.insightMetrics.wsConsecutiveFailures = 0; - } return response; } } catch (err) { @@ -383,16 +353,6 @@ export class StableWSConnection { this.isConnecting = false; // @ts-ignore this._log(`_connect() - Error - `, err); - if (this.client.options.enableInsights) { - this.client.insightMetrics.wsConsecutiveFailures++; - this.client.insightMetrics.wsTotalFailures++; - - const insights = buildWsFatalInsight( - this as unknown as StableWSConnection, - convertErrorToJson(err as Error), - ); - postInsights?.('ws_fatal', insights); - } this.client.rejectConnectionId?.(err); throw err; } @@ -433,7 +393,7 @@ export class StableWSConnection { return; } - if (this.isDisconnected && this.client.options.enableWSFallback) { + if (this.isDisconnected) { this._log('_reconnect() - Abort (3) since disconnect() is called'); return; } @@ -529,7 +489,6 @@ export class StableWSConnection { }, }; - this.authenticationSent = true; this.ws?.send(JSON.stringify(authMessage)); this._log('onopen() - onopen callback', { wsID }); }; @@ -549,7 +508,6 @@ export class StableWSConnection { if (!this.isResolved && data && data.type === 'connection.error') { this.isResolved = true; if (data.error) { - // @ts-expect-error - the types of _errorFromWSEvent are incorrect this.rejectPromise?.(this._errorFromWSEvent(data, false)); return; } @@ -640,7 +598,7 @@ export class StableWSConnection { this.totalFailures += 1; this._setHealth(false); this.isConnecting = false; - this.rejectPromise?.(this._errorFromWSEvent(event)); + this.rejectPromise?.(new Error(event.message)); this._log(`onerror() - WS connection resulted into error`, { event }); this._reconnect(); @@ -679,40 +637,30 @@ export class StableWSConnection { /** * _errorFromWSEvent - Creates an error object for the WS event - * */ - _errorFromWSEvent = ( - event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, + private _errorFromWSEvent = ( + event: WebSocket.CloseEvent | ConnectionErrorEvent, isWSFailure = true, ) => { - let code; - let statusCode; - let message; + let code: number; + let statusCode: number; + let message: string; if (isCloseEvent(event)) { code = event.code; - statusCode = 'unknown'; message = event.reason; - } - - if (isErrorEvent(event)) { + statusCode = 0; + } else { code = event.error.code; - statusCode = event.error.StatusCode; message = event.error.message; + statusCode = event.error.StatusCode; } - // Keeping this `warn` level log, to avoid cluttering of error logs from ws failures. - this._log( - `_errorFromWSEvent() - WS failed with code ${code}`, - { event }, - 'warn', - ); - - const error = new Error( - `WS failed with code ${code} and reason - ${message}`, - ) as Error & { - code?: string | number; + const msg = `WS failed with code: ${code} and reason: ${message}`; + this._log(msg, { event }, 'warn'); + const error = new Error(msg) as Error & { + code?: number; isWSFailure?: boolean; - StatusCode?: string | number; + StatusCode?: number; }; error.code = code; /** diff --git a/packages/client/src/coordinator/connection/connection_fallback.ts b/packages/client/src/coordinator/connection/connection_fallback.ts deleted file mode 100644 index 15fb03c3f..000000000 --- a/packages/client/src/coordinator/connection/connection_fallback.ts +++ /dev/null @@ -1,242 +0,0 @@ -import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'; -import { StreamClient } from './client'; -import { - addConnectionEventListeners, - removeConnectionEventListeners, - retryInterval, - sleep, -} from './utils'; -import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors'; -import { LogLevel, StreamVideoEvent, UR } from './types'; -import { ConnectedEvent } from '../../gen/coordinator'; - -export enum ConnectionState { - Closed = 'CLOSED', - Connected = 'CONNECTED', - Connecting = 'CONNECTING', - Disconnected = 'DISCONNECTED', - Init = 'INIT', -} - -export class WSConnectionFallback { - client: StreamClient; - state: ConnectionState; - consecutiveFailures: number; - connectionID?: string; - cancelToken?: CancelTokenSource; - - constructor(client: StreamClient) { - this.client = client; - this.state = ConnectionState.Init; - this.consecutiveFailures = 0; - - addConnectionEventListeners(this._onlineStatusChanged); - } - - _log(msg: string, extra: UR = {}, level: LogLevel = 'info') { - this.client.logger(level, 'WSConnectionFallback:' + msg, { - ...extra, - }); - } - - _setState(state: ConnectionState) { - this._log(`_setState() - ${state}`); - - // transition from connecting => connected - if ( - this.state === ConnectionState.Connecting && - state === ConnectionState.Connected - ) { - this.client.dispatchEvent({ type: 'connection.changed', online: true }); - } - - if ( - state === ConnectionState.Closed || - state === ConnectionState.Disconnected - ) { - this.client.dispatchEvent({ type: 'connection.changed', online: false }); - } - - this.state = state; - } - - /** @private */ - _onlineStatusChanged = (event: { type: string }) => { - this._log(`_onlineStatusChanged() - ${event.type}`); - - if (event.type === 'offline') { - this._setState(ConnectionState.Closed); - this.cancelToken?.cancel('disconnect() is called'); - this.cancelToken = undefined; - return; - } - - if (event.type === 'online' && this.state === ConnectionState.Closed) { - this.connect(true); - } - }; - - /** @private */ - _req = async ( - params: UR, - config: AxiosRequestConfig, - retry: boolean, - ): Promise => { - if (!this.cancelToken && !params.close) { - this.cancelToken = axios.CancelToken.source(); - } - - try { - const res = await this.client.doAxiosRequest( - 'get', - (this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API - undefined, - { - config: { ...config, cancelToken: this.cancelToken?.token }, - params, - publicEndpoint: true, - }, - ); - - this.consecutiveFailures = 0; // always reset in case of no error - return res; - } catch (err) { - this.consecutiveFailures += 1; - - // @ts-ignore - if (retry && isErrorRetryable(err)) { - this._log(`_req() - Retryable error, retrying request`); - await sleep(retryInterval(this.consecutiveFailures)); - return this._req(params, config, retry); - } - - throw err; - } - }; - - /** @private */ - _poll = async () => { - while (this.state === ConnectionState.Connected) { - try { - const data = await this._req<{ - events: StreamVideoEvent[]; - }>( - {}, - { - timeout: 30000, - }, - true, - ); // 30s => API responds in 20s if there is no event - - if (data.events?.length) { - for (let i = 0; i < data.events.length; i++) { - this.client.dispatchEvent(data.events[i]); - } - } - } catch (err) { - if (axios.isCancel(err)) { - this._log(`_poll() - axios canceled request`); - return; - } - - /** client.doAxiosRequest will take care of TOKEN_EXPIRED error */ - - // @ts-ignore - if (isConnectionIDError(err)) { - this._log(`_poll() - ConnectionID error, connecting without ID...`); - this._setState(ConnectionState.Disconnected); - this.connect(true); - return; - } - - // @ts-ignore - if (isAPIError(err) && !isErrorRetryable(err)) { - this._setState(ConnectionState.Closed); - return; - } - - await sleep(retryInterval(this.consecutiveFailures)); - } - } - }; - - /** - * connect try to open a longpoll request - * @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState - */ - connect = async (reconnect = false) => { - if (this.state === ConnectionState.Connecting) { - this._log( - 'connect() - connecting already in progress', - { reconnect }, - 'warn', - ); - return; - } - if (this.state === ConnectionState.Connected) { - this._log( - 'connect() - already connected and polling', - { reconnect }, - 'warn', - ); - return; - } - - this._setState(ConnectionState.Connecting); - this.connectionID = undefined; // connect should be sent with empty connection_id so API creates one - try { - const { event } = await this._req<{ - event: ConnectedEvent; - }>( - { json: this.client._buildWSPayload() }, - { - timeout: 8000, // 8s - }, - reconnect, - ); - - this._setState(ConnectionState.Connected); - this.connectionID = event.connection_id; - this.client.resolveConnectionId?.(); - // @ts-expect-error - this.client.dispatchEvent(event); - this._poll(); - return event; - } catch (err) { - this._setState(ConnectionState.Closed); - this.client.rejectConnectionId?.(); - throw err; - } - }; - - /** - * isHealthy checks if there is a connectionID and connection is in Connected state - */ - isHealthy = () => { - return !!this.connectionID && this.state === ConnectionState.Connected; - }; - - disconnect = async (timeout = 2000) => { - removeConnectionEventListeners(this._onlineStatusChanged); - - this._setState(ConnectionState.Disconnected); - this.cancelToken?.cancel('disconnect() is called'); - this.cancelToken = undefined; - - const connection_id = this.connectionID; - this.connectionID = undefined; - - try { - await this._req( - { close: true, connection_id }, - { - timeout, - }, - false, - ); - this._log(`disconnect() - Closed connectionID`); - } catch (err) { - this._log(`disconnect() - Failed`, { err }, 'error'); - } - }; -} diff --git a/packages/client/src/coordinator/connection/errors.ts b/packages/client/src/coordinator/connection/errors.ts deleted file mode 100644 index 34c8b4f64..000000000 --- a/packages/client/src/coordinator/connection/errors.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AxiosResponse } from 'axios'; -import { APIErrorResponse } from './types'; - -export const APIErrorCodes: Record< - string, - { name: string; retryable: boolean } -> = { - '-1': { name: 'InternalSystemError', retryable: true }, - '2': { name: 'AccessKeyError', retryable: false }, - '3': { name: 'AuthenticationFailedError', retryable: true }, - '4': { name: 'InputError', retryable: false }, - '6': { name: 'DuplicateUsernameError', retryable: false }, - '9': { name: 'RateLimitError', retryable: true }, - '16': { name: 'DoesNotExistError', retryable: false }, - '17': { name: 'NotAllowedError', retryable: false }, - '18': { name: 'EventNotSupportedError', retryable: false }, - '19': { name: 'ChannelFeatureNotSupportedError', retryable: false }, - '20': { name: 'MessageTooLongError', retryable: false }, - '21': { name: 'MultipleNestingLevelError', retryable: false }, - '22': { name: 'PayloadTooBigError', retryable: false }, - '23': { name: 'RequestTimeoutError', retryable: true }, - '24': { name: 'MaxHeaderSizeExceededError', retryable: false }, - '40': { name: 'AuthErrorTokenExpired', retryable: false }, - '41': { name: 'AuthErrorTokenNotValidYet', retryable: false }, - '42': { name: 'AuthErrorTokenUsedBeforeIssuedAt', retryable: false }, - '43': { name: 'AuthErrorTokenSignatureInvalid', retryable: false }, - '44': { name: 'CustomCommandEndpointMissingError', retryable: false }, - '45': { name: 'CustomCommandEndpointCallError', retryable: true }, - '46': { name: 'ConnectionIDNotFoundError', retryable: false }, - '60': { name: 'CoolDownError', retryable: true }, - '69': { name: 'ErrWrongRegion', retryable: false }, - '70': { name: 'ErrQueryChannelPermissions', retryable: false }, - '71': { name: 'ErrTooManyConnections', retryable: true }, - '99': { name: 'AppSuspendedError', retryable: false }, -}; - -// todo: this is not a correct error declaration. /recordings endpoint returns error objects as follows: -// { -// "code": 16, -// "message": "ListRecordings failed with error: \"Can't find call with id default:bbbb\"", -// "StatusCode": 404, -// "duration": "0.00ms", -// "more_info": "https://getstream.io/chat/docs/api_errors_response", -// "details": [] -// } - -type APIError = Error & { code: number; isWSFailure?: boolean }; - -export function isAPIError(error: Error): error is APIError { - return (error as APIError).code !== undefined; -} - -export function isErrorRetryable(error: APIError) { - if (!error.code) return false; - const err = APIErrorCodes[`${error.code}`]; - if (!err) return false; - return err.retryable; -} - -export function isConnectionIDError(error: APIError) { - return error.code === 46; // ConnectionIDNotFoundError -} - -export function isWSFailure(err: APIError): boolean { - if (typeof err.isWSFailure === 'boolean') { - return err.isWSFailure; - } - - try { - return JSON.parse(err.message).isWSFailure; - } catch (_) { - return false; - } -} - -export function isErrorResponse( - res: AxiosResponse, -): res is AxiosResponse { - return !res.status || res.status < 200 || 300 <= res.status; -} diff --git a/packages/client/src/coordinator/connection/insights.ts b/packages/client/src/coordinator/connection/insights.ts deleted file mode 100644 index efc996b8a..000000000 --- a/packages/client/src/coordinator/connection/insights.ts +++ /dev/null @@ -1,88 +0,0 @@ -import axios from 'axios'; -import { StableWSConnection } from './connection'; -import { randomId, sleep } from './utils'; - -export type InsightTypes = - | 'ws_fatal' - | 'ws_success_after_failure' - | 'http_hi_failed'; -export class InsightMetrics { - connectionStartTimestamp: number | null; - wsConsecutiveFailures: number; - wsTotalFailures: number; - instanceClientId: string; - - constructor() { - this.connectionStartTimestamp = null; - this.wsTotalFailures = 0; - this.wsConsecutiveFailures = 0; - this.instanceClientId = randomId(); - } -} - -/** - * postInsights is not supposed to be used by end users directly within chat application, and thus is kept isolated - * from all the client/connection code/logic. - * - * @param insightType - * @param insights - */ -export const postInsights = async ( - insightType: InsightTypes, - insights: Record, -) => { - const maxAttempts = 3; - for (let i = 0; i < maxAttempts; i++) { - try { - await axios.post( - `https://chat-insights.getstream.io/insights/${insightType}`, - insights, - ); - } catch (e) { - await sleep((i + 1) * 3000); - continue; - } - break; - } -}; - -export function buildWsFatalInsight( - connection: StableWSConnection, - event: Record, -) { - return { - ...event, - ...buildWsBaseInsight(connection), - }; -} - -function buildWsBaseInsight(connection: StableWSConnection) { - const { client } = connection; - return { - ready_state: connection.ws?.readyState, - url: connection._buildUrl(), - api_key: client.key, - start_ts: client.insightMetrics.connectionStartTimestamp, - end_ts: new Date().getTime(), - auth_type: client.getAuthType(), - token: client.tokenManager.token, - user_id: client.userID, - user_details: client._user, - // device: client.options.device, - device: 'browser', - client_id: connection.connectionID, - ws_details: connection.ws, - ws_consecutive_failures: client.insightMetrics.wsConsecutiveFailures, - ws_total_failures: client.insightMetrics.wsTotalFailures, - request_id: connection.requestID, - online: typeof navigator !== 'undefined' ? navigator?.onLine : null, - user_agent: typeof navigator !== 'undefined' ? navigator?.userAgent : null, - instance_client_id: client.insightMetrics.instanceClientId, - }; -} - -export function buildWsSuccessAfterFailureInsight( - connection: StableWSConnection, -) { - return buildWsBaseInsight(connection); -} diff --git a/packages/client/src/coordinator/connection/signing.ts b/packages/client/src/coordinator/connection/signing.ts index 3710c4220..f89de3a5f 100644 --- a/packages/client/src/coordinator/connection/signing.ts +++ b/packages/client/src/coordinator/connection/signing.ts @@ -1,19 +1,6 @@ -import { decodeBase64, encodeBase64 } from './base64'; +import { decodeBase64 } from './base64'; -/** - * - * @param {string} userId the id of the user - * @return {string} - */ -export function DevToken(userId: string) { - return [ - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', //{"alg": "HS256", "typ": "JWT"} - encodeBase64(JSON.stringify({ user_id: userId })), - 'devtoken', // hardcoded signature - ].join('.'); -} - -export function UserFromToken(token: string) { +export function getUserFromToken(token: string) { const fragments = token.split('.'); if (fragments.length !== 3) { return ''; diff --git a/packages/client/src/coordinator/connection/token_manager.ts b/packages/client/src/coordinator/connection/token_manager.ts index 31af61f55..f02d4b2b3 100644 --- a/packages/client/src/coordinator/connection/token_manager.ts +++ b/packages/client/src/coordinator/connection/token_manager.ts @@ -1,4 +1,4 @@ -import { UserFromToken } from './signing'; +import { getUserFromToken } from './signing'; import { isFunction } from './utils'; import type { TokenOrProvider, UserWithId } from './types'; @@ -93,7 +93,7 @@ export class TokenManager { // Allow empty token for anonymous users if (isAnonymous && tokenOrProvider === '') return; - const tokenUserId = UserFromToken(tokenOrProvider); + const tokenUserId = getUserFromToken(tokenOrProvider); if ( tokenOrProvider != null && (tokenUserId == null || diff --git a/packages/client/src/coordinator/connection/types.ts b/packages/client/src/coordinator/connection/types.ts index beb933c85..b6526439e 100644 --- a/packages/client/src/coordinator/connection/types.ts +++ b/packages/client/src/coordinator/connection/types.ts @@ -115,10 +115,6 @@ export type StreamClientOptions = Partial & { */ baseURL?: string; browser?: boolean; - // device?: BaseDeviceFields; - enableInsights?: boolean; - /** experimental feature, please contact support if you want this feature enabled for you */ - enableWSFallback?: boolean; logger?: Logger; logLevel?: LogLevel; /** @@ -146,7 +142,6 @@ export type StreamClientOptions = Partial & { */ secret?: string; - warmUp?: boolean; // Set the instance of StableWSConnection on chat client. Its purely for testing purpose and should // not be used in production apps. wsConnection?: StableWSConnection; diff --git a/packages/client/src/coordinator/connection/utils.ts b/packages/client/src/coordinator/connection/utils.ts index 2276e7c00..f53d90402 100644 --- a/packages/client/src/coordinator/connection/utils.ts +++ b/packages/client/src/coordinator/connection/utils.ts @@ -1,7 +1,7 @@ -import { Logger } from './types'; +import { AxiosResponse } from 'axios'; +import type { APIErrorResponse, Logger } from './types'; -export const sleep = (m: number): Promise => - new Promise((r) => setTimeout(r, m)); +export const sleep = (m: number) => new Promise((r) => setTimeout(r, m)); export function isFunction(value: Function | T): value is Function { return ( @@ -92,24 +92,6 @@ function getRandomBytes(length: number): Uint8Array { return bytes; } -export function convertErrorToJson(err: Error) { - const jsonObj = {} as Record; - - if (!err) return jsonObj; - - try { - Object.getOwnPropertyNames(err).forEach((key) => { - jsonObj[key] = Object.getOwnPropertyDescriptor(err, key); - }); - } catch (_) { - return { - error: 'failed to serialize the error', - }; - } - - return jsonObj; -} - /** * Informs if a promise is yet to be resolved or rejected */ @@ -165,3 +147,9 @@ export function removeConnectionEventListeners(cb: (e: Event) => void) { window.removeEventListener('online', cb); } } + +export function isErrorResponse( + res: AxiosResponse, +): res is AxiosResponse { + return !res.status || res.status < 200 || 300 <= res.status; +} diff --git a/yarn.lock b/yarn.lock index b5cfadc65..02bf15db7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7827,7 +7827,6 @@ __metadata: "@types/ws": ^8.5.7 "@vitest/coverage-v8": ^0.34.4 axios: ^1.6.0 - base64-js: ^1.5.1 dotenv: ^16.3.1 happy-dom: ^11.0.2 isomorphic-ws: ^5.0.0