import { Socket } from 'socket.io-client'; import { get } from 'lodash'; import { isBrowser } from '../helpers/browser.helper'; import { getSocketIOWithProxy } from '@phystack/socket.io-proxy'; import { PhyHubDirectConnection } from './phyhub-direct-connection.service'; interface UrlConfig { url: string; timeout: number; options: Record; } export class PhyHubConnection { private static instance: PhyHubConnection | null = null; private phygridSocketSingleton: Socket | null = null; private socketUrls: UrlConfig[] = []; private instanceId: string | undefined; private moduleName: string | undefined; private directConnection: PhyHubDirectConnection | null = null; private constructor( params: { instanceId?: string; moduleName?: string; dataResidency?: string } = {} ) { this.instanceId = params.instanceId; this.moduleName = params.moduleName; // Check for direct connection mode (for testing) if (PhyHubDirectConnection.isEnabled()) { this.directConnection = PhyHubDirectConnection.fromEnv(); if (this.directConnection) { console.log('[PhyHubConnection] Direct connection mode enabled'); } } this.setSocketUrls(params.dataResidency); this.fetchPhygridSocketInstance(); } private getTopWindow(): Window & typeof globalThis { let topWindow: Window & typeof globalThis = window; while (topWindow.parent !== topWindow) { try { topWindow = topWindow.parent as Window & typeof globalThis; } catch (e) { break; } } return topWindow; } private fetchPhygridSocketInstance = () => { if (isBrowser) { const topWindow = this.getTopWindow(); if ((topWindow as any).edgeHub) { this.phygridSocketSingleton = (topWindow as any).edgeHub; } else if ((topWindow as any).phygridSocketSingleton) { this.phygridSocketSingleton = (topWindow as any).phygridSocketSingleton; } } }; private setSocketUrls(dataResidency: string | undefined = undefined): void { // If PHYSTACK_SIMULATOR_URL is set, connect only to the simulator const simulatorUrl = process.env.PHYSTACK_SIMULATOR_URL; if (simulatorUrl) { this.socketUrls = [{ url: simulatorUrl, timeout: 3000, options: {} }]; console.log('socketUrls (simulator)', this.socketUrls); return; } // Production URLs if (!isBrowser) { this.socketUrls = [{ url: 'http://phyos:55000', timeout: 5000, options: {} }]; } else { this.socketUrls.push({ url: 'https://phyos:55500', timeout: 5000, options: {} }); // if (dataResidency !== undefined) { // TODO: tmp disabled since we will need this in mobile apps later this.socketUrls.push({ url: `https://phyhub.${dataResidency || 'eu'}.omborigrid.net:443`, timeout: 10000, options: {}, }); // } } console.log('socketUrls', this.socketUrls); } private async getSocketIoInstance(): Promise { while (true) { // Keep trying until we connect or explicitly throw for (const { url, timeout, options } of this.socketUrls) { let socket: Socket | null = null; try { console.info(`Attempting to connect to ${url}...`); const auth = { instanceId: this.instanceId, moduleName: this.moduleName }; socket = await getSocketIOWithProxy(url, { ...options, auth, reconnectionAttempts: 1, timeout: timeout, debug: true, }); // Wait for the connection to be established await new Promise((resolve, reject) => { socket!.on('connect', () => { console.info(`Successfully connected to ${url}`); resolve(true); }); socket!.on('connect_error', error => { console.info(`Failed to connect to ${url}: ${error.message}`); reject(error); }); // Add timeout for the connection attempt setTimeout(() => { reject(new Error(`Connection timeout to ${url}`)); }, timeout); }); // If we get here, the connection was successful return socket; // Exit both the loop and the while(true) } catch (error) { // console.info(`Failed to connect to ${url}. Trying next option...`); // Cleanup the failed socket try { socket?.disconnect(); } catch (e) { // Ignore cleanup errors } } } // console.info('Failed to connect to any socket.io server. Retrying in 1 second...'); await new Promise(resolve => setTimeout(resolve, 1000)); } } public static getInstance( params: { instanceId?: string; moduleName?: string; dataResidency?: string } = {} ): PhyHubConnection { if (!PhyHubConnection.instance) { PhyHubConnection.instance = new PhyHubConnection(params); } else if ( (params.instanceId && PhyHubConnection.instance.instanceId !== params.instanceId) || (params.moduleName && PhyHubConnection.instance.moduleName !== params.moduleName) ) { // Update instance parameters if they are different PhyHubConnection.instance.instanceId = params.instanceId; PhyHubConnection.instance.moduleName = params.moduleName; if (params.dataResidency) { PhyHubConnection.instance.setSocketUrls(params.dataResidency); } } return PhyHubConnection.instance; } public async getPhyHubSocket(): Promise { console.info(`getPhyHubSocket(): Getting phyhub socket`); // Use direct connection if enabled (for testing) if (this.directConnection) { console.log('[PhyHubConnection] Using direct connection'); const socket = await this.directConnection.connect(); this.phygridSocketSingleton = socket; return socket; } // Always check top window first if (isBrowser) { const topWindow = this.getTopWindow(); console.log('Checking for edgeHub in top window...', { hasEdgeHub: !!(topWindow as any).edgeHub, hasPhygridSocket: !!(topWindow as any).phygridSocketSingleton, }); if ((topWindow as any).edgeHub) { console.log('Found edgeHub in top window, using it instead of creating new connection'); this.phygridSocketSingleton = (topWindow as any).edgeHub; if (this.phygridSocketSingleton) { console.log('Successfully initialized edgeHub socket'); return this.phygridSocketSingleton; } } if ((topWindow as any).phygridSocketSingleton) { console.log('Found existing socket in top window, reusing it'); this.phygridSocketSingleton = (topWindow as any).phygridSocketSingleton; if (this.phygridSocketSingleton) { console.log('Successfully initialized existing socket'); return this.phygridSocketSingleton; } } } // Only proceed with new connection if we haven't found an existing one if (!this.phygridSocketSingleton) { console.log('No existing socket found, creating new connection...'); while (true) { try { const tmpSocket = await this.getSocketIoInstance(); const PHYHUB_URL = get(tmpSocket, 'io.uri', false); tmpSocket.disconnect(); if (PHYHUB_URL) { const { instanceId, moduleName } = this; console.log('PHYHUB_URL', PHYHUB_URL); const auth = { instanceId, moduleName }; console.log('auth', JSON.stringify(auth, null, 2)); const socket = await getSocketIOWithProxy(PHYHUB_URL, { auth, }); await new Promise((resolve, reject) => { socket.on('connect', () => { console.info(`Successfully connected to final socket at ${PHYHUB_URL}`); if (isBrowser) { // Store in top window immediately upon successful connection const topWindow = this.getTopWindow(); (topWindow as any).phygridSocketSingleton = socket; } this.phygridSocketSingleton = socket; resolve(true); }); socket.on('connect_error', error => { this.phygridSocketSingleton = null; reject(error); }); }); break; // Connection successful } } catch (error) { console.error('Failed to connect to PhyHub', error); await new Promise(resolve => setTimeout(resolve, 5000)); } } } return this.phygridSocketSingleton as Socket; } }