From 37f979e91f8702106dececc89e431cbad98b001b Mon Sep 17 00:00:00 2001 From: shafin-deriv Date: Wed, 18 Dec 2024 11:08:32 +0800 Subject: [PATCH 1/4] chore: restart bot on network error --- src/app/app-root.tsx | 2 +- .../layout/header/account-switcher.tsx | 2 +- src/external/bot-skeleton/scratch/dbot.js | 22 ++- .../bot-skeleton/services/api/api-base.ts | 153 +++++++++++++----- .../bot-skeleton/services/api/appId.js | 10 +- .../services/api/ticks_service.js | 1 + .../tradeEngine/trade/OpenContract.js | 1 + .../services/tradeEngine/trade/Ticks.js | 5 +- src/pages/main/main.tsx | 8 +- src/stores/app-store.ts | 2 + 10 files changed, 153 insertions(+), 53 deletions(-) diff --git a/src/app/app-root.tsx b/src/app/app-root.tsx index 7ea13967..d415f351 100644 --- a/src/app/app-root.tsx +++ b/src/app/app-root.tsx @@ -42,7 +42,7 @@ const AppRoot = () => { useEffect(() => { const initializeApi = async () => { if (!api_base_initialized.current) { - await api_base.init(); + await api_base.initApi(); api_base_initialized.current = true; setIsApiInitialized(true); } diff --git a/src/components/layout/header/account-switcher.tsx b/src/components/layout/header/account-switcher.tsx index 0f36c9a1..09f2212a 100644 --- a/src/components/layout/header/account-switcher.tsx +++ b/src/components/layout/header/account-switcher.tsx @@ -138,7 +138,7 @@ const AccountSwitcher = observer(({ activeAccount }: TAccountSwitcher) => { if (!token) return; localStorage.setItem('authToken', token); localStorage.setItem('active_loginid', loginId.toString()); - await api_base?.init(true); + await api_base?.initApi(true); }; return ( diff --git a/src/external/bot-skeleton/scratch/dbot.js b/src/external/bot-skeleton/scratch/dbot.js index 58b9a353..c40d66d6 100644 --- a/src/external/bot-skeleton/scratch/dbot.js +++ b/src/external/bot-skeleton/scratch/dbot.js @@ -25,6 +25,7 @@ class DBot { * Initialises the workspace and mounts it to a container element (app_contents). */ async initWorkspace(public_path, store, api_helpers_store, is_mobile, is_dark_mode) { + console.log('test initWorkspace ---------', this.interpreter); await loadBlockly(is_dark_mode); const recent_files = await getSavedWorkspaces(); this.interpreter = Interpreter(); @@ -246,10 +247,25 @@ class DBot { } async initializeInterpreter() { - if (this.interpreter) { - await this.interpreter.terminateSession(); + console.log('test initializeInterpreter ---------', this.interpreter, this.is_bot_running); + if (this.is_bot_running) { + this.interpreter?.unsubscribeFromTicksService?.()?.then(async () => { + await this.interpreter?.bot?.tradeEngine?.watchTicks(this.interpreter?.bot?.tradeEngine?.symbol, true); + console.log('test making proposal open contract request ---------', { + proposal_open_contract: 1, + contract_id: this.interpreter.bot.tradeEngine.contractId, + }); + api_base.api.send({ + proposal_open_contract: 1, + contract_id: this.interpreter.bot.tradeEngine.contractId, + }); + }); + } else { + if (this.interpreter) { + await this.interpreter.terminateSession(); + } + this.interpreter = Interpreter(); } - this.interpreter = Interpreter(); } /** * Runs the bot. Does a sanity check before attempting to generate the diff --git a/src/external/bot-skeleton/services/api/api-base.ts b/src/external/bot-skeleton/services/api/api-base.ts index 75ac4081..e2507026 100644 --- a/src/external/bot-skeleton/services/api/api-base.ts +++ b/src/external/bot-skeleton/services/api/api-base.ts @@ -11,8 +11,6 @@ import { setIsAuthorizing, } from './observables/connection-status-stream'; import ApiHelpers from './api-helpers'; -import { generateDerivApiInstance, V2GetActiveClientId, V2GetActiveToken } from './appId'; -import chart_api from './chart-api'; type CurrentSubscription = { id: string; @@ -40,6 +38,8 @@ type TApiBaseApi = { }; } & ReturnType; +let reconnect_timeout: NodeJS.Timeout | null = null; + class APIBase { api: TApiBaseApi | null = null; token: string = ''; @@ -57,64 +57,135 @@ class APIBase { active_symbols_promise: Promise | null = null; common_store: CommonStore | undefined; landing_company: string | null = null; + //TODO : Need to remove this api call because we have it in client store + async getLandingCompany() { + if (!this.api || !this.account_info?.country) { + return null; + } + try { + const landing_company = await this.api.send({ landing_company: this.account_info.country }); + this.landing_company = landing_company; + } catch (error) { + console.error('Error fetching landing company:', error); + this.landing_company = null; + } + return this.landing_company; + } unsubscribeAllSubscriptions = () => { - this.current_auth_subscriptions?.forEach(subscription_promise => { - subscription_promise.then(({ subscription }) => { - if (subscription?.id) { - this.api?.send({ - forget: subscription.id, - }); - } - }); - }); + // this.current_auth_subscriptions?.forEach(subscription_promise => { + // subscription_promise.then(({ subscription }) => { + // if (subscription?.id) { + // this.api?.send({ + // forget: subscription.id, + // }); + // } + // }); + // }); this.current_auth_subscriptions = []; }; onsocketopen() { setConnectionStatus(CONNECTION_STATUS.OPENED); + console.log('test on open ---------'); } onsocketclose() { setConnectionStatus(CONNECTION_STATUS.CLOSED); this.reconnectIfNotConnected(); + const error = { + error: { + code: 'DisconnectError', + message: 'Connection lost', + }, + }; + globalObserver?.emit('Error', error); } - async init(force_create_connection = false) { + async initApi(force_create_connection = false) { this.toggleRunButton(true); if (this.api) { this.unsubscribeAllSubscriptions(); } - if (!this.api || this.api?.connection.readyState !== 1 || force_create_connection) { - if (this.api?.connection) { + if (!this.api || this.api?.connection.readyState > 1 || force_create_connection) { + if (this.api?.connection && !force_create_connection) { ApiHelpers.disposeInstance(); setConnectionStatus(CONNECTION_STATUS.CLOSED); - this.api.disconnect(); - this.api.connection.removeEventListener('open', this.onsocketopen.bind(this)); - this.api.connection.removeEventListener('close', this.onsocketclose.bind(this)); - } - this.api = generateDerivApiInstance(); - this.api?.connection.addEventListener('open', this.onsocketopen.bind(this)); - this.api?.connection.addEventListener('close', this.onsocketclose.bind(this)); - } + console.log('test ------------------- reconnect ---------'); + await this.api.reconnect(); + // this.api.connection.removeEventListener('open', this.onsocketopen.bind(this)); + // this.api.connection.removeEventListener('close', this.onsocketclose.bind(this)); + } else { + this.api?.disconnect(); + this.api = generateDerivApiInstance(); + console.log('test ------------------- new ---------', this.api.onOpen); + // Example: WebSocket API that returns Observable for 'open' events + api_base.api.onOpen().subscribe({ + next: openEvent => { + // This block is executed when WebSocket opens + console.log('WebSocket connection opened:', openEvent); + if (!this.has_active_symbols) { + this.active_symbols_promise = this.getActiveSymbols(); + } + + this.initEventListeners(); + + if (this.time_interval) clearInterval(this.time_interval); + this.time_interval = null; + + if (V2GetActiveToken()) { + setIsAuthorizing(true); + this.authorizeAndSubscribe(); + } + this.onsocketopen(); + }, + error: err => { + // Handle any errors (unlikely for WebSocket "open", but error handling is always good) + console.error('Error occurred while opening WebSocket:', err); + }, + complete: () => { + // Complete is called when the Observable completes or the connection closes + console.log('WebSocket onOpen event stream completed'); + }, + }); + + this.api.onClose().subscribe({ + next: closeEvent => { + console.log('WebSocket connection closed:', closeEvent); + this.onsocketclose(); + // throw new Error('DisconnectError'); + }, + error: err => { + console.error('Error occurred while closing WebSocket:', err); + }, + complete: () => { + console.log('WebSocket onClose event stream completed'); + }, + }); - if (!this.has_active_symbols) { - this.active_symbols_promise = this.getActiveSymbols(); + // Subscribing to the 'open' event + // const openSubscription: Subscription = openObservable; + // this.api?.connection.addEventListener('open', this.onsocketopen.bind(this)); + // this.api?.connection.addEventListener('close', this.onsocketclose.bind(this)); + // this.api.onClose(() => { + // console.log('test on close ---------'); + // }); + } } - this.initEventListeners(); - if (this.time_interval) clearInterval(this.time_interval); - this.time_interval = null; + // chart_api.init(force_create_connection); - if (V2GetActiveToken()) { - setIsAuthorizing(true); - await this.authorizeAndSubscribe(); + if (!reconnect_timeout) { + setInterval(() => { + console.log('-------- Disconnecting the socket ---------'); + // this.api?.disconnect(); + }, 30000); + } else { + reconnect_timeout = null; } - - chart_api.init(force_create_connection); } getConnectionStatus() { @@ -127,7 +198,7 @@ class APIBase { terminate() { // eslint-disable-next-line no-console - if (this.api) this.api.disconnect(); + // if (this.api) this.api.disconnect(); } initEventListeners() { @@ -139,7 +210,7 @@ class APIBase { async createNewInstance(account_id: string) { if (this.account_id !== account_id) { - await this.init(); + await this.initApi(true); } } @@ -147,9 +218,15 @@ class APIBase { // eslint-disable-next-line no-console console.log('connection state: ', this.api?.connection?.readyState); if (this.api?.connection?.readyState && this.api?.connection?.readyState > 1) { - // eslint-disable-next-line no-console - console.log('Info: Connection to the server was closed, trying to reconnect.'); - this.init(true); + // Debounce reconnection attempts to avoid multiple rapid reconnects + if (reconnect_timeout) { + clearTimeout(reconnect_timeout as NodeJS.Timeout); + } + reconnect_timeout = setTimeout(() => { + // eslint-disable-next-line no-console + console.log('Info: Connection to the server was closed, trying to reconnect.'); + this.initApi(); + }, 3000); } }; @@ -208,7 +285,7 @@ class APIBase { return subscription; }, [], - this + this.api ); }; diff --git a/src/external/bot-skeleton/services/api/appId.js b/src/external/bot-skeleton/services/api/appId.js index 318451e4..73219628 100644 --- a/src/external/bot-skeleton/services/api/appId.js +++ b/src/external/bot-skeleton/services/api/appId.js @@ -5,12 +5,14 @@ import { getInitialLanguage } from '@deriv-com/translations'; import APIMiddleware from './api-middleware'; export const generateDerivApiInstance = () => { - const cleanedServer = getSocketURL().replace(/[^a-zA-Z0-9.]/g, ''); + // const cleanedServer = getSocketURL().replace(/[^a-zA-Z0-9.]/g, ''); const cleanedAppId = getAppId()?.replace?.(/[^a-zA-Z0-9]/g, '') ?? getAppId(); - const socket_url = `wss://${cleanedServer}/websockets/v3?app_id=${cleanedAppId}&l=${getInitialLanguage()}&brand=${website_name.toLowerCase()}`; - const deriv_socket = new WebSocket(socket_url); + // const socket_url = `wss://${cleanedServer}/websockets/v3?app_id=${cleanedAppId}&l=${getInitialLanguage()}&brand=${website_name.toLowerCase()}`; + // const deriv_socket = new WebSocket(socket_url); const deriv_api = new DerivAPIBasic({ - connection: deriv_socket, + // connection: deriv_socket, + app_id: cleanedAppId, + endpoint: 'ws.derivws.com', middleware: new APIMiddleware({}), }); return deriv_api; diff --git a/src/external/bot-skeleton/services/api/ticks_service.js b/src/external/bot-skeleton/services/api/ticks_service.js index 22b36e50..459dedcd 100644 --- a/src/external/bot-skeleton/services/api/ticks_service.js +++ b/src/external/bot-skeleton/services/api/ticks_service.js @@ -265,6 +265,7 @@ export default class TicksService { }; return new Promise((resolve, reject) => { if (!api_base.api) resolve([]); + console.log('test ------------------- requestTicks ---------', request_object); doUntilDone(() => api_base.api.send(request_object), [], api_base) .then(r => { if (style === 'ticks') { diff --git a/src/external/bot-skeleton/services/tradeEngine/trade/OpenContract.js b/src/external/bot-skeleton/services/tradeEngine/trade/OpenContract.js index 2634b14a..718ecf5e 100644 --- a/src/external/bot-skeleton/services/tradeEngine/trade/OpenContract.js +++ b/src/external/bot-skeleton/services/tradeEngine/trade/OpenContract.js @@ -9,6 +9,7 @@ export default Engine => if (!api_base.api) return; const subscription = api_base.api.onMessage().subscribe(({ data }) => { if (data.msg_type === 'proposal_open_contract') { + console.log('test ------------------- observeOpenContract ---------', data?.msg_type); const contract = data.proposal_open_contract; if (!contract || !this.expectedContractId(contract?.contract_id)) { diff --git a/src/external/bot-skeleton/services/tradeEngine/trade/Ticks.js b/src/external/bot-skeleton/services/tradeEngine/trade/Ticks.js index eea64dca..7cae4707 100644 --- a/src/external/bot-skeleton/services/tradeEngine/trade/Ticks.js +++ b/src/external/bot-skeleton/services/tradeEngine/trade/Ticks.js @@ -12,8 +12,9 @@ let tickListenerKey; export default Engine => class Ticks extends Engine { - async watchTicks(symbol) { - if (symbol && this.symbol !== symbol) { + async watchTicks(symbol, restartTrade = false) { + console.log('test ------------------- watchTicks ---------', symbol); + if (symbol && (this.symbol !== symbol || restartTrade)) { this.symbol = symbol; const { ticksService } = this.$scope; diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index 58877e91..dd789582 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -75,10 +75,10 @@ const AppWrapper = observer(() => { if (connectionStatus !== CONNECTION_STATUS.OPENED) { const is_bot_running = document.getElementById('db-animation__stop-button') !== null; if (is_bot_running) { - clear(); - stopBot(); - api_base.setIsRunning(false); - setWebSocketState(false); + // clear(); + // stopBot(); + // api_base.setIsRunning(false); + // setWebSocketState(false); } } }, [clear, connectionStatus, setWebSocketState, stopBot]); diff --git a/src/stores/app-store.ts b/src/stores/app-store.ts index cc320ec8..f18128cb 100644 --- a/src/stores/app-store.ts +++ b/src/stores/app-store.ts @@ -139,6 +139,7 @@ export default class AppStore { }; onMount = async () => { + console.log('test onMount ---------'); const { blockly_store, run_panel } = this.root_store; const { client, ui } = this.core; this.showDigitalOptionsMaltainvestError(); @@ -186,6 +187,7 @@ export default class AppStore { }; onUnmount = () => { + console.log('test onUnmount ---------'); DBot.terminateBot(); DBot.terminateConnection(); if (window.Blockly?.derivWorkspace) { From 28e3a76a278579a8856cb35d36be5037f2679f5e Mon Sep 17 00:00:00 2001 From: shafin-deriv Date: Wed, 18 Dec 2024 11:15:38 +0800 Subject: [PATCH 2/4] chore: fix unused vars --- src/pages/main/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx index dd789582..3cbd0600 100644 --- a/src/pages/main/main.tsx +++ b/src/pages/main/main.tsx @@ -9,7 +9,7 @@ import MobileWrapper from '@/components/shared_ui/mobile-wrapper'; import Tabs from '@/components/shared_ui/tabs/tabs'; import TradingViewModal from '@/components/trading-view-chart/trading-view-modal'; import { DBOT_TABS, TAB_IDS } from '@/constants/bot-contents'; -import { api_base, updateWorkspaceName } from '@/external/bot-skeleton'; +import { updateWorkspaceName } from '@/external/bot-skeleton'; import { CONNECTION_STATUS } from '@/external/bot-skeleton/services/api/observables/connection-status-stream'; import { isDbotRTL } from '@/external/bot-skeleton/utils/workspace'; import { useApiBase } from '@/hooks/useApiBase'; From d36e81b70eebd3d193b6eb5bdd1f290332b496ba Mon Sep 17 00:00:00 2001 From: shafin-deriv Date: Wed, 18 Dec 2024 11:20:54 +0800 Subject: [PATCH 3/4] chore: fix lint issues --- src/external/bot-skeleton/services/api/appId.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/external/bot-skeleton/services/api/appId.js b/src/external/bot-skeleton/services/api/appId.js index 73219628..cc3243f1 100644 --- a/src/external/bot-skeleton/services/api/appId.js +++ b/src/external/bot-skeleton/services/api/appId.js @@ -1,7 +1,5 @@ -import { getAppId, getSocketURL } from '@/components/shared'; -import { website_name } from '@/utils/site-config'; +import { getAppId } from '@/components/shared'; import DerivAPIBasic from '@deriv/deriv-api/dist/DerivAPIBasic'; -import { getInitialLanguage } from '@deriv-com/translations'; import APIMiddleware from './api-middleware'; export const generateDerivApiInstance = () => { @@ -12,7 +10,7 @@ export const generateDerivApiInstance = () => { const deriv_api = new DerivAPIBasic({ // connection: deriv_socket, app_id: cleanedAppId, - endpoint: 'ws.derivws.com', + endpoint: 'ws.derivws.com', middleware: new APIMiddleware({}), }); return deriv_api; From 3b54a7a63b99cec3f603280d80d97e0f5f61e787 Mon Sep 17 00:00:00 2001 From: shafin-deriv Date: Wed, 18 Dec 2024 11:30:14 +0800 Subject: [PATCH 4/4] fix: missing imports --- src/external/bot-skeleton/services/api/api-base.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/external/bot-skeleton/services/api/api-base.ts b/src/external/bot-skeleton/services/api/api-base.ts index e2507026..8bd7ceaf 100644 --- a/src/external/bot-skeleton/services/api/api-base.ts +++ b/src/external/bot-skeleton/services/api/api-base.ts @@ -11,6 +11,7 @@ import { setIsAuthorizing, } from './observables/connection-status-stream'; import ApiHelpers from './api-helpers'; +import { generateDerivApiInstance, V2GetActiveClientId, V2GetActiveToken } from './appId'; type CurrentSubscription = { id: string; @@ -129,12 +130,12 @@ class APIBase { if (!this.has_active_symbols) { this.active_symbols_promise = this.getActiveSymbols(); } - + this.initEventListeners(); - + if (this.time_interval) clearInterval(this.time_interval); this.time_interval = null; - + if (V2GetActiveToken()) { setIsAuthorizing(true); this.authorizeAndSubscribe(); @@ -175,7 +176,6 @@ class APIBase { } } - // chart_api.init(force_create_connection); if (!reconnect_timeout) {