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: internal reconnection mechanism #76

Merged
merged 3 commits into from
Aug 20, 2024
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
24 changes: 19 additions & 5 deletions src/client-library/deriv-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
TSocketSubscribeResponseData,
} from '../types/api.types';

export type DerivAPIClientOptions = {
export interface DerivAPIClientOptions {
onOpen?: (e: Event) => void;
onClose?: (e: CloseEvent) => void;
};
}

type DataHandler<T extends TSocketEndpointNames> = (data: TSocketResponseData<T>) => void;

Expand Down Expand Up @@ -57,6 +57,8 @@ export type UnsubscribeHandlerArgs = {
};

export class DerivAPIClient {
options?: DerivAPIClientOptions;
endpoint: string;
websocket: WebSocket;
requestHandler: RequestMap;
subscribeHandler: SubscriptionMap;
Expand All @@ -70,20 +72,25 @@ export class DerivAPIClient {
keepAliveIntervalId: NodeJS.Timeout | null = null;

constructor(endpoint: string, options?: DerivAPIClientOptions) {
this.websocket = new WebSocket(endpoint);
this.options = options;
this.endpoint = endpoint;
this.websocket = new WebSocket(this.endpoint);
this.req_id = 0;
this.requestHandler = new Map();
this.subscribeHandler = new Map();
this.waitForWebSocketOpen = PromiseUtils.createPromise();
this.init();
}

async init() {
this.websocket.addEventListener('open', e => {
if (typeof options?.onOpen === 'function') options.onOpen(e);
if (typeof this.options?.onOpen === 'function') this.options.onOpen(e);
const { resolve } = this.waitForWebSocketOpen;
resolve({});
});

this.websocket.addEventListener('close', e => {
if (typeof options?.onClose === 'function') options.onClose(e);
if (typeof this.options?.onClose === 'function') this.options.onClose(e);
});

this.websocket.addEventListener('message', async response => {
Expand Down Expand Up @@ -258,6 +265,13 @@ export class DerivAPIClient {
}
}

async reconnect() {
this.websocket = new WebSocket(this.endpoint);
this.waitForWebSocketOpen = PromiseUtils.createPromise();
this.init();
this.reinitializeSubscriptions(this.subscribeHandler, this.authorizePayload);
}

isSocketClosingOrClosed() {
return ![2, 3].includes(this.websocket.readyState);
}
Expand Down
29 changes: 27 additions & 2 deletions src/client-library/deriv-api-manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { DerivAPIClient, DerivAPIClientOptions } from './deriv-api-client';

export interface DerivAPIManagerOptions extends DerivAPIClientOptions {
reconnect?: boolean;
}

export class DerivAPIManager {
is_reconnecting: boolean;
options?: DerivAPIClientOptions;
activeClient: DerivAPIClient;
clientList: Map<string, DerivAPIClient> = new Map();

constructor(endpoint: string, options?: DerivAPIClientOptions) {
const client = new DerivAPIClient(endpoint, options);
constructor(endpoint: string, options?: DerivAPIClientOptions & DerivAPIManagerOptions) {
const { reconnect = true, ...api_options } = options || {};

const client = new DerivAPIClient(endpoint, {
onClose: async () => {
api_options?.onClose;
// Only reconnect if the reconnect option is true and is not already reconnecting.
if (reconnect && !this.is_reconnecting) {
this.is_reconnecting = true;
await this.handleReconnect();
// Wait before WebSocket is open before setting is_reconnecting to false
await this.activeClient.waitForWebSocketOpen;
this.is_reconnecting = false;
}
},
onOpen: api_options?.onOpen,
});
this.is_reconnecting = false;
this.clientList.set(endpoint, client);
this.activeClient = client;
this.options = options;
Expand All @@ -16,6 +37,10 @@ export class DerivAPIManager {
return this.activeClient;
}

async handleReconnect() {
await this.activeClient.reconnect();
}

/**
* Creates a new connection and swap out the current active connection with it.
* Brings authorize and subscription context to the new connection.
Expand Down
6 changes: 3 additions & 3 deletions src/context/api-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { createContext, PropsWithChildren } from 'react';
import { URLUtils } from '@deriv-com/utils';
import { DerivAPIManager } from '../client-library/deriv-api-manager';

export const derivAPIClient = new DerivAPIManager(URLUtils.getWebsocketURL());

type APIData = {
derivAPIClient: DerivAPIManager;
};

export const APIDataContext = createContext<APIData | null>(null);

const derivApiManager = new DerivAPIManager(URLUtils.getWebsocketURL());

/**
* Provides a React Query client and API data context to its child components.
*
Expand All @@ -20,5 +20,5 @@ export const APIDataContext = createContext<APIData | null>(null);
* @returns {JSX.Element} The provider component wrapping its children with API data context and React Query client.
*/
export const APIProvider = ({ children }: PropsWithChildren) => {
return <APIDataContext.Provider value={{ derivAPIClient }}>{children}</APIDataContext.Provider>;
return <APIDataContext.Provider value={{ derivAPIClient: derivApiManager }}>{children}</APIDataContext.Provider>;
};