// ============================================================ // WebSocket 连接管理 // 职责:心跳保活、指数退避重连、类型安全的消息收发 // 来源:docs/03-接口文档.md §六 连接管理 // ============================================================ import type { ClientMessage, ServerMessage, WsMessage } from "../types"; const WS_URL = "ws://localhost:8080/ws"; const PING_INTERVAL = 30_000; // 30 秒心跳 const MAX_RECONNECT_DELAY = 30_000; // 最大重连延迟 30 秒 type MessageHandler = (msg: ServerMessage) => void; type StatusHandler = (status: ConnectionStatus) => void; export type ConnectionStatus = "connecting" | "connected" | "disconnected"; export class CamTalkWebSocket { private ws: WebSocket | null = null; private pingTimer: ReturnType | null = null; private reconnectAttempt = 0; private reconnectTimer: ReturnType | null = null; private shouldReconnect = true; private messageHandlers = new Set(); private statusHandlers = new Set(); private _status: ConnectionStatus = "disconnected"; get status(): ConnectionStatus { return this._status; } /** 注册消息回调 */ onMessage(handler: MessageHandler): () => void { this.messageHandlers.add(handler); return () => this.messageHandlers.delete(handler); } /** 注册连接状态回调 */ onStatusChange(handler: StatusHandler): () => void { this.statusHandlers.add(handler); return () => this.statusHandlers.delete(handler); } /** 建立连接 */ connect(): void { if (this.ws?.readyState === WebSocket.OPEN) return; this.shouldReconnect = true; this.setStatus("connecting"); const ws = new WebSocket(WS_URL); ws.onopen = () => { this.reconnectAttempt = 0; this.setStatus("connected"); this.startPing(); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data) as WsMessage; // 忽略 pong,心跳由服务端自动回复 if (msg.type === "pong") return; this.messageHandlers.forEach((h) => h(msg as ServerMessage)); } catch { console.error("[WS] 无法解析消息:", event.data); } }; ws.onclose = () => { this.stopPing(); this.setStatus("disconnected"); if (this.shouldReconnect) { this.scheduleReconnect(); } }; ws.onerror = (err) => { console.error("[WS] 连接错误:", err); ws.close(); }; this.ws = ws; } /** 断开连接,不再自动重连 */ disconnect(): void { this.shouldReconnect = false; this.clearTimers(); this.ws?.close(); this.ws = null; this.setStatus("disconnected"); } /** 发送客户端消息 */ send(msg: ClientMessage): void { if (this.ws?.readyState !== WebSocket.OPEN) { console.warn("[WS] 连接未就绪,消息丢弃:", msg.type); return; } this.ws.send(JSON.stringify(msg)); } /** 发送 ping 心跳 */ private startPing(): void { this.stopPing(); this.pingTimer = setInterval(() => { this.send({ type: "ping" }); }, PING_INTERVAL); } private stopPing(): void { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = null; } } /** 指数退避 + 抖动重连 */ private scheduleReconnect(): void { const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempt), MAX_RECONNECT_DELAY); const jitter = Math.random() * 1000; const totalDelay = delay + jitter; console.log(`[WS] ${totalDelay.toFixed(0)}ms 后重连 (attempt ${this.reconnectAttempt})`); this.reconnectTimer = setTimeout(() => { this.reconnectAttempt++; this.connect(); }, totalDelay); } private setStatus(status: ConnectionStatus): void { this._status = status; this.statusHandlers.forEach((h) => h(status)); } private clearTimers(): void { this.stopPing(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } } /** 创建单例实例 */ export const wsClient = new CamTalkWebSocket();