import { EventEmitter } from "eventemitter3"; import { Destination, ISignalingClient, Subscription, Topic } from "../signaling"; import { IMatchingService } from "./matching.interface"; import { MatchInfo, MatchingEventMap, MatchRequest, MatchResponse, MatchResponseTemp, MatchStatus } from "./matching.types"; const MATCH_TIMEOUT_MS = 5 * 60000; // 5 분 export class MatchingService extends EventEmitter implements IMatchingService { private isMatching: boolean = false; private matchTimeoutId: any = null; private matchSubscription?: Subscription; private currentMatchHandler?: ((channel: Topic, message: any) => void); constructor(private signalingClient: ISignalingClient) { super(); } // 매칭 핸들러 생성 private createMatchHandler( resolve: (value: MatchInfo | null) => void, reject: (reason?: any) => void ): (channel: Topic, message: any) => void { return (channel: Topic, payload: any) => { if (channel !== Topic.MATCHING) return; console.log("[MatchingService] Match response received:", payload); const response = this.convertMatchResponse(payload); switch (response.status) { case MatchStatus.WAITING: // 매칭 중 유지 break; case MatchStatus.SUCCESS: this.isMatching = false; this.emit("matching:found", response.data!); resolve(response.data!); break; case MatchStatus.FAILED: this.isMatching = false; this.emit("matching:failed", response.message); resolve(null); break; default: console.error("[MatchingService] Unknown match status:", response.status); const error = new Error("Match request failed"); this.isMatching = false; this.emit("matching:error", error); reject(error); break; } }; } // 임시 메서드. 서버 응답 형식 변경되면 삭제 (서버 응답에 아직 sessionId 등 필수 정보 없음) private convertMatchResponse(payload: any): MatchResponse { if (payload && 'data' in payload) { return payload as MatchResponse; } const temp = payload as MatchResponseTemp; // 임시 sessionId 생성 const sessionId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const data: MatchInfo = { sessionId: sessionId, role: temp.role, otherUserId: temp.partnerInfo || "" }; return { status: temp.status, data: data, message: "Match found" }; } // 매칭 취소 (리소스 정리) private cleanupMatchRequest(): void { // timeout 정리 if (this.matchTimeoutId) { clearTimeout(this.matchTimeoutId); this.matchTimeoutId = null; } // message handler 정리 if (this.currentMatchHandler) { this.signalingClient.off("message", this.currentMatchHandler); this.currentMatchHandler = undefined; } } // 매칭 취소 (통신 정리) private cancelMatch(): void { // 취소 요청 전송 if (this.signalingClient.isConnected()) { try { this.signalingClient.send(Destination.CANCEL, {}); } catch (error) { console.error("[MatchingService] Error sending cancel request:", error); } } // 구독 정리 if (this.matchSubscription) { try { this.matchSubscription.unsubscribe(); } catch (error) { console.error("[MatchingService] Error unsubscribing from match channel:", error); } finally { this.matchSubscription = undefined; } } } // 매칭 요청 메서드 async requestMatch(params: MatchRequest): Promise { if (this.isMatching) { throw new Error("Already requesting a match"); } if (!this.signalingClient.isConnected()) { throw new Error("Cannot request match: signaling client not connected"); } this.isMatching = true; return new Promise((resolve, reject) => { try { console.log("[MatchingService] Requesting match with params:", params); // 매칭 채널 구독 this.matchSubscription = this.signalingClient.subscribe(Topic.MATCHING); // 매칭 결과 리스너 (구독 결과) this.currentMatchHandler = this.createMatchHandler(resolve, reject); this.signalingClient.on("message", this.currentMatchHandler); // timeout this.matchTimeoutId = setTimeout(() => { console.warn("[MatchingService] Match request timed out"); this.cleanupMatchRequest(); this.cancelMatch(); this.isMatching = false; const error = new Error("Match request timed out"); this.emit("matching:timeout", error); resolve(null); }, MATCH_TIMEOUT_MS); // 매칭 요청 전송 this.signalingClient.send(Destination.MATCHING, params); } catch (error: any) { console.error("[MatchingService] Failed to request match:", error); this.cleanupMatchRequest(); this.isMatching = false; const webSocketError = error instanceof Error ? error : new Error(`Failed to request match: ${error.message}`,); this.emit("matching:error", webSocketError); reject(webSocketError); } }); } // 매칭 취소 메서드 (외부) abortMatch(): void { if (!this.isMatching) { return; } console.log("[MatchingService] Aborting match request"); this.cleanupMatchRequest(); this.cancelMatch(); this.isMatching = false; } }