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<SignalingEventMap> implements ISignalingClient {
private client?: Client;
private connectPromise?: Promise<void>;
private subscriptions: Map<Topic, StompSubscription> = new Map();
private async _doConnect(): Promise<void> {
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<void> {
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<T = unknown>(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}`);
}
}
}