/* eslint-disable no-use-before-define, @typescript-eslint/no-use-before-define, max-lines */ import { PRODUCTION } from 'pob-babel'; import socketio from 'socket.io-client'; import Logger from 'nightingale-logger'; import { BrowserApplication } from 'alp-types'; const logger = new Logger('alp:websocket'); type Socket = ReturnType; let socket: Socket | undefined; let successfulConnection: boolean | null = null; let connected = false; interface Websocket { connected: boolean; isConnected: () => boolean; isDisconnected: () => boolean; on: typeof on; off: typeof off; emit: typeof emit; socket?: Socket; } declare module 'alp-types' { interface BrowserApplication { websocket: Websocket; } } declare global { interface Window { __VERSION__: string; } } const callbackFirstConnectionError = () => { successfulConnection = false; }; export const websocket: Websocket = { get connected() { return connected; }, on, off, emit, isConnected, isDisconnected, socket, }; function start(app: BrowserApplication, namespaceName: string): Socket { const { config, context } = app; if (socket) { throw new Error('WebSocket already started'); } const webSocketConfig = config.get('webSocket') || config.get('websocket'); if (!webSocketConfig) { throw new Error('Missing config webSocket'); } if (!webSocketConfig.has('port')) { throw new Error('Missing config webSocket.port'); } const secure = webSocketConfig.get('secure'); const port = webSocketConfig.get('port'); const createdSocket = socketio( `http${secure ? 's' : ''}://${ window.location.hostname }:${port}/${namespaceName}`, { reconnectionDelay: 500, reconnectionDelayMax: 2500, timeout: 4000, transports: ['websocket'], }, ); socket = createdSocket; createdSocket.on('connect_error', callbackFirstConnectionError); createdSocket.on('connect', () => { createdSocket.off('connect_error', callbackFirstConnectionError); logger.success('connected'); successfulConnection = true; connected = true; }); createdSocket.on('reconnect', () => { logger.success('reconnected'); connected = true; }); createdSocket.on('disconnect', () => { logger.warn('disconnected'); connected = false; }); createdSocket.on('hello', ({ version }: { version: string }) => { if (version !== window.__VERSION__) { // eslint-disable-next-line no-alert if (PRODUCTION && window.confirm(context.t('newversion'))) { return window.location.reload(true); } else { console.warn('Version mismatch', { serverVersion: version, clientVersion: window.__VERSION__, }); } } }); createdSocket.on('redux:action', (action: any) => { logger.debug('dispatch action from websocket', action); // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore app.store.dispatch(action); }); return createdSocket; } function emit(event: string, ...args: any[]): Promise { if (!socket) throw new Error('Cannot call emit() before start()'); const existingSocket = socket; logger.debug('emit', { args }); return new Promise((resolve, reject) => { const resolved = setTimeout(() => { logger.warn('websocket emit timeout', { args }); reject(new Error('websocket response timeout')); }, 10000); existingSocket.emit( event, ...args, (error: Error | string | null, result: Result): void => { clearTimeout(resolved); if (error != null) { return reject(typeof error === 'string' ? new Error(error) : error); } resolve(result); }, ); }); } function on(event: string, handler: T): T { if (!socket) throw new Error('Cannot call on() before start()'); socket.on(event, handler); return handler; } function off(event: string, handler: Function): void { if (!socket) throw new Error('Cannot call off() before start()'); socket.off(event, handler); } function isConnected(): boolean { // socket.connected is not updated after reconnect event return !!socket && connected; } function isDisconnected(): boolean { return !!successfulConnection && !isConnected(); } export default function alpWebsocket( app: BrowserApplication, namespaceName = '', ): Socket { const socket = start(app, namespaceName); app.websocket = websocket; websocket.socket = socket; return socket; }