import type { App, ListenProcessParams } from '@kevisual/router'; import { EventEmitter } from 'eventemitter3'; type RemoteAppOptions = { app?: App; url?: string; token?: string; username?: string; emitter?: EventEmitter; id?: string; /** 是否启用自动重连,默认 true */ autoReconnect?: boolean; /** 最大重连次数,默认 Infinity */ maxReconnectAttempts?: number; /** 初始重连延迟(毫秒),默认 1000 */ reconnectDelay?: number; /** 重连延迟最大值(毫秒),默认 30000 */ maxReconnectDelay?: number; /** 是否启用指数退避,默认 true */ enableBackoff?: boolean; }; /** * 远程共享地址类似:https://kevisual.cn/ws/proxy */ export class RemoteApp { mainApp: App; url: string; id: string; username: string; emitter: EventEmitter; isConnected: boolean; ws: WebSocket; remoteIsConnected: boolean; isError: boolean = false; // 重连相关属性 autoReconnect: boolean; maxReconnectAttempts: number; reconnectDelay: number; maxReconnectDelay: number; enableBackoff: boolean; reconnectAttempts: number = 0; reconnectTimer: NodeJS.Timeout | null = null; isManuallyClosed: boolean = false; constructor(opts?: RemoteAppOptions) { this.mainApp = opts?.app; const token = opts.token; const url = opts.url; const id = opts.id; const username = opts.username; this.username = username; this.emitter = opts?.emitter || new EventEmitter(); const _url = new URL(url); if (token) { _url.searchParams.set('token', token); } _url.searchParams.set('id', id); if (!token && !username) { console.error(`[remote-app] 不存在用户名和token ${id}. 权限认证会失败。`); } this.url = _url.toString(); this.id = id; // 初始化重连相关配置 this.autoReconnect = opts?.autoReconnect ?? true; this.maxReconnectAttempts = opts?.maxReconnectAttempts ?? Infinity; this.reconnectDelay = opts?.reconnectDelay ?? 1000; this.maxReconnectDelay = opts?.maxReconnectDelay ?? 30000; this.enableBackoff = opts?.enableBackoff ?? true; this.init(); } async isConnect(): Promise { const that = this; if (this.isConnected) { return true; } // 如果正在进行重连,等待连接成功 if (this.reconnectTimer !== null) { console.log(`远程应用 ${this.id} 正在重连中...`); } // 等待连接成功(支持初次连接和重连场景) return new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); that.emitter.off('open', listenOnce); }, 5000); const listenOnce = () => { clearTimeout(timeout); that.isConnected = true; that.remoteIsConnected = true; resolve(true); }; that.emitter.once('open', listenOnce); }); } getWsURL(url: string) { const { protocol } = new URL(url); if (protocol.startsWith('ws')) { return url.toString() } const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:'; const wsURL = url.toString().replace(protocol, wsProtocol); return wsURL; } async init() { if (!this.url) { throw new Error('No url provided for remote app'); } if (!this.id) { throw new Error('No id provided for remote app'); } this.isError = false; // 关闭已有连接 if (this.ws) { this.ws.close(); } const ws = new WebSocket(this.getWsURL(this.url)); const that = this; ws.onopen = function () { that.isConnected = true; that.onOpen(); console.log('[remote-app] WebSocket connection opened'); }; ws.onclose = function () { that.isConnected = false; that.onClose(); } ws.onmessage = function (event) { that.onMessage(event.data); } ws.onerror = function (error) { that.onError(error); } this.ws = ws; } onOpen() { this.isError = false; this.reconnectAttempts = 0; // 清除可能存在的重连定时器 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } this.emitter.emit('open', this.id); } onClose() { console.log('远程应用关闭:', this.id); this.isConnected = false; this.emitter.emit('close', this.id); // 触发自动重连逻辑 if (this.autoReconnect && !this.isManuallyClosed) { this.scheduleReconnect(); } } /** 计算下一次重连延迟 */ calculateReconnectDelay(): number { if (!this.enableBackoff) { return this.reconnectDelay; } // 指数退避算法:delay = initialDelay * 2^(attempts - 1) const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts); return Math.min(delay, this.maxReconnectDelay); } /** 安排重连 */ scheduleReconnect() { // 检查是否达到最大重连次数 if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error(`远程应用 ${this.id} 已达到最大重连次数 ${this.maxReconnectAttempts},停止重连`); this.emitter.emit('maxReconnectAttemptsReached', this.id); return; } // 清除可能存在的定时器 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } const delay = this.calculateReconnectDelay(); this.reconnectAttempts++; console.log(`远程应用 ${this.id} 将在 ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; try { this.init(); } catch (error) { console.error(`远程应用 ${this.id} 重连失败:`, error); this.emitter.emit('reconnectFailed', { id: this.id, attempt: this.reconnectAttempts, error }); // 重连失败后继续尝试重连 this.scheduleReconnect(); } }, delay); } /** 手动关闭连接,停止自动重连 */ disconnect() { this.isManuallyClosed = true; this.autoReconnect = false; // 清除重连定时器 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } // 关闭 WebSocket if (this.ws) { this.ws.close(); } } /** 手动重连 */ reconnect() { this.isManuallyClosed = false; this.reconnectAttempts = 0; // 清除可能存在的定时器 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } this.init(); } onMessage(data: any) { this.emitter.emit('message', data); } onError(error: any) { console.error(`[remote-app] 远程应用错误: ${this.id}`, error); this.isError = true; this.emitter.emit('error', error); } on(event: 'open' | 'close' | 'message' | 'error' | 'maxReconnectAttemptsReached' | 'reconnectFailed', listener: (data: any) => void) { this.emitter.on(event, listener); return () => { this.emitter.off(event, listener); }; } json(data: any) { this.ws.send(JSON.stringify(data)); } listenProxy() { const remoteApp = this; const app = this.mainApp; const username = this.username; const listenFn = async (event: any) => { try { const data = event.toString(); const body = JSON.parse(data) const bodyData = body?.data as ListenProcessParams; const message = bodyData?.message || {}; const context = bodyData?.context || {}; if (body?.code === 401) { console.error('远程应用认证失败,请检查 token 是否正确'); this.isError = true; return; } if (body?.type !== 'proxy') return; if (!body.id) { remoteApp.json({ id: body.id, data: { code: 400, message: 'id is required', }, }); return; } const res = await app.run(message, context); remoteApp.json({ id: body.id, data: { code: res.code, data: res.data, message: res.message, }, }); } catch (error) { console.error('处理远程代理请求出错:', error); } }; remoteApp.json({ id: this.id, type: 'registryClient', username: username, }); console.log(`远程应用 ${this.id} (${username}) 已注册到主应用,等待消息...`); remoteApp.emitter.on('message', listenFn); const closeMessage = () => { remoteApp.emitter.off('message', listenFn); } remoteApp.emitter.once('close', () => { closeMessage(); }); return () => { closeMessage(); }; } }