import { SIGNALING_CONFIG } from "@/config"; import { tokenStorage } from "@/utils"; import { Client, StompSubscription } from "@stomp/stompjs"; import { EventEmitter } from "eventemitter3"; import SockJS from "sockjs-client"; import { ISignalingClient } from "./signaling.interface"; import { Destination, SignalingEventMap, Subscription, Topic } from "./signaling.types"; export class SignalingClient extends EventEmitter implements ISignalingClient { private client?: Client; private connectPromise?: Promise; private subscriptions: Map = new Map(); private async _doConnect(): Promise { return new Promise((resolve, reject) => { this.client = new Client({ webSocketFactory: () => new SockJS(SIGNALING_CONFIG.serverUrl), reconnectDelay: SIGNALING_CONFIG.reconnectDelay, heartbeatIncoming: SIGNALING_CONFIG.heartbeatIncoming, heartbeatOutgoing: SIGNALING_CONFIG.heartbeatOutgoing, connectionTimeout: SIGNALING_CONFIG.connectionTimeout, debug: (str: string) => { if (process.env.NODE_ENV !== 'production') { console.log("[STOMP]", str); } }, beforeConnect: async (client) => { // TODO: 만료 검증 과정은 추후 추가 const token = await tokenStorage.getAccessToken(); client.connectHeaders = token ? { Authorization: `Bearer ${token}` } : {}; }, onConnect: (frame) => { console.log("[SignalingClient] Connected") this.emit("signaling:connected"); resolve(); }, onDisconnect: (frame) => { console.log("[SignalingClient] Disconnected") // this.emit("signaling:disconnected"); // 정상 종료이기에 emit X }, onWebSocketClose: (event) => { console.warn("[SignalingClient] WebSocket closed"); if (this.client?.active) { console.log(`연결 끊김 (Clean: ${event.wasClean}, Code: ${event.code}). 재연결 상태로 전환.`); this.emit("signaling:disconnected"); } }, onStompError: (frame) => { console.error("[SignalingClient] Stomp error:", frame); const error = new Error(`STOMP error: ${frame.headers["message"] || "Unknown error"}`); this.emit("signaling:error", error); reject(error); }, onWebSocketError: (event) => { console.error("[SignalingClient] WebSocket error:", event); const error = new Error("WebSocket error"); this.emit("signaling:error", error); reject(error); }, }); this.client.activate(); }); } private unsubscribeAll(): void { if (this.subscriptions.size === 0) { console.log('[SignalingClient] No active subscriptions to unsubscribe'); return; } console.log(`[SignalingClient] Unsubscribing from ${this.subscriptions.size} subscriptions`); this.subscriptions.forEach((subscription) => { try { subscription.unsubscribe(); } catch (error) { console.error("[SignalingClient] Error unsubscribing:", error); } }); this.subscriptions.clear(); } async connect(): Promise { if (this.client?.connected) { console.log("[SignalingClient] Already connected or connecting"); return; } if (this.connectPromise) { console.log("[SignalingClient] Connection already in progress"); return this.connectPromise; } this.connectPromise = this._doConnect(); try { await this.connectPromise; } catch (error) { console.error("[SignalingClient] Connection error:", error); } finally { this.connectPromise = undefined; } } disconnect(): void { if (!this.client?.connected) { return; } console.log("[SignalingClient] Disconnecting..."); // Unsubscribe from all channels this.unsubscribeAll(); // Deactivate client try { this.client.deactivate(); } catch (error) { console.error("[SignalingClient] Error deactivating client:", error); } this.client = undefined; } isConnected(): boolean { return this.client?.connected || false; } subscribe(channel: Topic): Subscription { if (!this.client?.connected) { throw new Error("Cannot subscribe: client not connected"); } if (this.subscriptions.has(channel)) { console.log(`[SignalingClient] Already subscribed to ${channel}`); return { unsubscribe: () => { const sub = this.subscriptions.get(channel); if (sub) { sub.unsubscribe(); this.subscriptions.delete(channel); } } }; } const subscription = this.client.subscribe(channel, (message) => { try { const body = message.body ? JSON.parse(message.body) : null; console.log(`[SignalingClient] Message received from ${channel}:`, body); this.emit('message', channel, body); } catch (error) { console.error("[SignalingClient] Failed to parse message:", error); } }); this.subscriptions.set(channel, subscription); return { unsubscribe: () => { subscription.unsubscribe(); this.subscriptions.delete(channel); console.log(`[SignalingClient] Unsubscribed from ${channel}`); }, } } send(destination: Destination, payload: T): void { if (!this.client?.connected) { throw new Error("Cannot send message: client not connected"); } console.log(`[SignalingClient] Sending to ${destination}:`, payload); try { this.client.publish({ destination, body: JSON.stringify(payload), headers: { "content-type": "application/json" }, }); } catch (error: any) { console.error("[SignalingClient] Failed to send message:", error); throw new Error(`Failed to send message: ${error.message}`); } } }