import request from '../request'; import type { SocketConfig, SocketMessageInterface, SocketURLConfig, WebSocketConfigModel, WebSocketLifeCycle } from '../interface/socket'; import { MAX_SOCKET_RETRY_COUNT, SDK_VERSION, SocketError, WS_MSG_TYPE } from '../interface/socket'; import type { LogFunc } from '../interface/common'; import { LogType } from '../interface/common'; import { isSocketMsg } from '../utils/socket'; import { v4 as uuid } from 'uuid'; import type { OpenSessionInterface } from '../request/socket'; import HeartbeatManager from './heart-beat-manager'; import EventManager from './event-manager'; import type { RetryOptions } from '../utils/common'; import { retry } from '../utils/common'; /** * Socket 类 * 用于管理 socket 链接,以及 socket 的 ping pong 逻辑,后续可以迁移到 SSE * * @export * @class Socket */ export default class Socket { /** * 用于 socket 的参数配置 * * @private * @type {SocketConfig} * @memberof Socket */ private _config: SocketConfig; /** * 用于管理 socket 的实例 * * @private * @type {WebSocket} * @memberof Socket */ private _webSocket?: WebSocket; /** * 用于管理 socket 的配置信息 * * @private * @type {WebSocketConfigModel} * @memberof Socket */ private _webSocketData?: WebSocketConfigModel; /** * 用于在 ws 的时候出现异常的时候(比如说后端 WS 服务挂了)重试上限 * * @private * @memberof Socket */ private _retryCount = 0; /** * 处理心跳逻辑 */ private _heartbeatManager?: HeartbeatManager; /** * Websocket 生命周期 */ private _eventManager?: EventManager; /** * 遇到ws断开重连的时候,用于标记是否正在重连,与最开始的新建socket区分开 */ private _isChangingSocket = false; /** getter 方法 */ /** * socket 配置的 url * * @readonly * @private * @type {SocketURLConfig} * @memberof Socket */ private get _socketUrlConfig(): SocketURLConfig { return this._config.urlConfig; } /** * socket 生命周期,方便内部调用 * * @readonly * @private * @type {(WebSocketLifeCycle | undefined)} * @memberof Socket */ private get _lifeCycle(): WebSocketLifeCycle | undefined { return this._config.lifeCycle; } private get _socketLifeCycle(): WebSocketLifeCycle | undefined { const originLifeCycle = this._lifeCycle; const lifeCycle: WebSocketLifeCycle = { onOpen: (ev, webSocket) => { this._isChangingSocket = false; // 开启成功,重试清零 this._retryCount = 0; if (this._webSocket) { this._heartbeatManager = new HeartbeatManager(this.wsId, this._webSocket, 5000, this._log) this._heartbeatManager.start() } originLifeCycle?.onOpen?.(ev, webSocket) }, onClose: (ev, webSocket) => { this._isChangingSocket = true; this._heartbeatManager?.stop(); this._heartbeatManager = undefined; this._eventManager = undefined; originLifeCycle?.onClose?.(ev, webSocket) // 多次进来可能是因为 后端 ws 服务挂了导致 ws 无法链接成功 if (this._retryCount >= MAX_SOCKET_RETRY_COUNT) { this._config.log?.(LogType.warning, `[socket] retry exceed limit, url: ${this._webSocket?.url}`); const event: CloseEvent = new CloseEvent('close', { ...ev, code: SocketError.EXCEED_MAX_COUNT }); originLifeCycle?.onError?.(event); // 处理完成失败逻辑,重试清零 this._retryCount = 0; return; } // 如果因为异常关闭,那么需要重新开启 // 重新开启后不会自动变成 isOpen 的状态,需要将信息暂存起来 this._retryCount += 1; setTimeout(() => { this.build().catch((error) => { this._config.log?.( LogType.error, `[socket] Exceeding the maximum limit of reconnection attempts (${MAX_SOCKET_RETRY_COUNT}), ignore this reconnection request.`, error ); }) }, 1000 * 2 ** this._retryCount) }, onMessage: (message) => { // 仅接收当前 wsId 的消息 if (message.wsId === this.wsId) { // ping pong 重制 this._heartbeatManager?.clear(); if (!isSocketMsg(message)) { originLifeCycle?.onMessage?.(message) } } else { this._log?.( LogType.error, `[socket] The received message's wsId (${message.wsId}) does not match the current wsId (${this.wsId}).`, message ); } }, onError: originLifeCycle?.onError } return lifeCycle; } /** * socket 的 log,方便内部调用 * * @readonly * @private * @type {(LogFunc | undefined)} * @memberof Socket */ private get _log(): LogFunc | undefined { return this._config.log; } /** * socket 的 wsId * * @readonly * @type {string} * @memberof Socket */ get wsId(): string { return this._webSocketData?.wsId ?? ''; } /** * socket 链接的 url 地址 * * @readonly * @private * @type {string} * @memberof Socket */ private get _address(): string { return this._webSocketData?.address ?? ''; } get isChangingSocket(): boolean { return this._isChangingSocket; } constructor(data: SocketConfig) { this._config = data; window.addEventListener('online', () => { if (this.isOpen()) { return; } this.build(); }); } private _isSetupSocket = false; /************** PUBLIC METHOD **************/ /** * 创建 Socket * @returns */ build = async (): Promise => { if (this.isOpen()) { return; } this._log?.(LogType.info, '[socket] build socket'); this._isSetupSocket = true; // 每次触发构建先检查是否需要重连 // 根据isOpen判断状态,不再通过http请求获取服务端状态 try { return await this._buildSocket(); } finally { this._isSetupSocket = false; } }; /** * 关闭 socket * * @memberof Socket */ close(): void { this._log?.(LogType.info, '[socket] close socket'); const stopHearBeat = () => { // 关闭 websocket this._webSocket?.close(); }; stopHearBeat(); } /** * 发送消息 * * @param {SocketMessageInterface} data * @memberof Socket */ sendMsg = (data: SocketMessageInterface, resend?: boolean): Promise => { const timestamp = Date.now(); const messageId = `${uuid()}_____${timestamp}`; const sendData = { timestamp: timestamp, seqId: messageId, messageId, wsId: this.wsId, ...data }; this._log?.(LogType.info, '[socket] will send msg: ', sendData); if (this.isOpen()) { this._webSocket?.send(JSON.stringify(sendData)); if (!resend && data.type !== WS_MSG_TYPE.ack) { // 发送消息之后,告知 session 添加消息 this._lifeCycle?.onMessage?.(sendData); } return Promise.resolve(); } // 如果没有开启,那么就需要重新开启 return this.build(); }; isOpen = (): boolean => { // 检查 ws 连接状态 + 是否正在请求。保证幂等。 return this._webSocket?.readyState === WebSocket.OPEN || this._webSocket?.readyState === WebSocket.CONNECTING || this._isSetupSocket; }; /** * 清空重试次数 */ clearRetryCount = (): void => { this._retryCount = 0; } /** * 发送 Ack 消息 * @param seqId seqId */ sendAck(seqId: string): void { // 收到消息之后立刻 ack this.sendMsg({ seqId: seqId, type: WS_MSG_TYPE.ack }); } /** * 创建长链接 * @returns WS 的配置信息 */ private _createWebsocket = async () => { const path = this._socketUrlConfig.socketPath; const data = { sdkVersion: SDK_VERSION, apiVersion: this._config.APIVersion, refUserId: this._config.refUserId, channel: this._config.urlConfig.channel }; const res = await request({ baseUrl: this._config.baseUrl, url: path, header: this._config.header, method: 'POST', data }) if (res) { this._log?.(LogType.info, '[socket] create long link: ', data, res); return res; } // 失败 this._log?.( LogType.error, '[socket] create long link failed with response: ', res ); throw new Error('[socket] create long link failed'); }; /** * 多次重试创建 websocket,避免因为网络问题导致的 websocket 创建失败,这里仅是为了找后端获取对应的 socket 字段。 * @returns 初始化 websocket 配置,如果失败则重试 */ private async _retryCreateWebSocket(): Promise { const options: RetryOptions = { maxAttempts: MAX_SOCKET_RETRY_COUNT, onAttemptFailure: (error) => { this._log?.(LogType.error, error); }, onAllAttemptsFailed: () => { const event: CloseEvent = new CloseEvent('close', { code: SocketError.EXCEED_MAX_COUNT }); this._lifeCycle?.onError?.(event); throw new Error(`[socket] Exceeding the maximum limit of reconnection attempts (${MAX_SOCKET_RETRY_COUNT}), ignore this reconnection request.`); } }; return retry(this._createWebsocket, options) } /** * 创建 websocket,配置相关回调,开启心跳 * @returns 创建 websocket */ private _buildSocket = async () => { const wsData = await this._retryCreateWebSocket(); if (wsData.address.length === 0 || wsData.wsId.length === 0) { throw new Error('[socket] address or wsId is empty'); } this._webSocketData = wsData; this._webSocket = new WebSocket(this._address); this._eventManager = new EventManager(this._webSocket, this._socketLifeCycle, this._log) this._eventManager.setupEventHandlers(); }; }