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<MatchingEventMap> 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<MatchInfo | null> {
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;
}
}