import {useIngame} from "@/composables/useIngame.ts"; import type {GameInfo, LobbyInfo, TieInfo, TrumpInfo, WonInfo} from "@/types/GameTypes.ts"; const api = window.__RUNTIME_CONFIG__?.API_URL; // ---------- Types --------------------------------------------------------- export type ServerMessage = { id?: string; event?: string; status?: "success" | "error"; state?: "Lobby" | "InGame" | "SelectTrump" | "TieBreak" | "FinishedMatch"; stateData?: GameInfo | LobbyInfo | TieInfo | TrumpInfo | WonInfo; data?: T; error?: string; }; export type ClientMessage = { id: string; event: string; data: T; }; export type HandlerFn = (data: T) => unknown | Promise; interface PendingEntry { resolve: (data: any) => void; reject: (err: Error) => void; timer: ReturnType; } let ws: WebSocket | null = null; const pending = new Map(); const handlers = new Map(); const uState = useIngame(); let heartbeatTimer: ReturnType | null = null; let defaultHandler: HandlerFn | null = null; function uuid(): string { return crypto.randomUUID(); } function failAllPending(reason: string) { for (const [, entry] of pending.entries()) { clearTimeout(entry.timer); entry.reject(new Error(reason)); } pending.clear(); } function startHeartbeat() { stopHeartbeat(); heartbeatTimer = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { sendEventAndWait("ping", {}, 4000) .then(() => console.debug("[WS] Heartbeat OK")) .catch((err) => console.warn("[WS] Heartbeat failed:", err.message)); } }, 5000); } function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } function setupSocketHandlers(socket: WebSocket) { socket.onmessage = async (raw) => { console.debug("[WS] MESSAGE:", raw.data); let msg: ServerMessage; try { msg = JSON.parse(raw.data); } catch (err) { console.warn("[WS] Bad JSON:", raw.data, err); return; } const { id, event, state, stateData, status, data } = msg; // RPC response branch if (id && status) { const entry = pending.get(id); if (!entry) return; pending.delete(id); clearTimeout(entry.timer); if (status === "success") { entry.resolve(data ?? {}); } else { entry.reject(new Error(msg.error || "Server returned error")); } return; } if (state && stateData) { uState.setIngame(state, stateData); } // Server event → handler branch if (id && event) { const handler = handlers.get(event); const reply = (status: "success" | "error", error?: string) => { if (socket.readyState !== WebSocket.OPEN) return; const resp = { id, event, status, error }; socket.send(JSON.stringify(resp)); }; if (!handler) { console.warn("[WS] No handler for event:", event); if (defaultHandler) { try { await defaultHandler(data ?? {}); reply("success"); } catch (err) { reply("error", (err as Error).message); } } else { reply("error", `No handler for '${event}'`); } return; } try { await handler(data ?? {}); reply("success"); } catch (err) { reply("error", (err as Error).message); } } }; socket.onerror = (err) => { console.error("[WS] ERROR:", err); stopHeartbeat(); failAllPending("WebSocket error"); }; socket.onclose = (ev) => { stopHeartbeat(); failAllPending("WebSocket closed"); if (ev.wasClean) { console.log(`[WS] Closed cleanly: code=${ev.code} reason=${ev.reason}`); } else { console.warn("[WS] Connection died"); } // You redirect here — if you don’t want auto reconnect, keep as is. location.href = "/mainmenu"; }; } export function connectWebSocket(url?: string): Promise { if (!url) { const loc = window.location; const protocol = loc.protocol === "https:" ? "wss:" : "ws:"; url = `${protocol}//${api}/websocket`; } if (ws && ws.readyState === WebSocket.OPEN) { return Promise.resolve(); } if (ws && ws.readyState === WebSocket.CONNECTING) { return new Promise((resolve, reject) => { const prevOnOpen = ws!.onopen; const prevOnError = ws!.onerror; ws!.onopen = (ev) => { if (prevOnOpen) prevOnOpen.call(ws!, ev); resolve(); }; ws!.onerror = (err) => { if (prevOnError) prevOnError.call(ws!, err); reject(err); }; }); } // New connection ws = new WebSocket(url); setupSocketHandlers(ws); return new Promise((resolve, reject) => { ws!.onopen = () => { console.log("[WS] Connected"); startHeartbeat(); resolve(); }; ws!.onerror = (err) => reject(err); }); } export function disconnectWebSocket(code = 1000, reason = "Client disconnect") { stopHeartbeat(); failAllPending("Disconnected"); if (ws) { try { ws.close(code, reason); } catch {} ws = null; } } export function sendEvent(event: string, data: any) { if (!ws || ws.readyState !== WebSocket.OPEN) { console.warn("[WS] Send failed: not open"); return; } const message: ClientMessage = { id: uuid(), event, data }; console.debug("[WS] SEND:", message); ws.send(JSON.stringify(message)); } export function sendEventAndWait( event: string, data: any, timeoutMs = 10000 ): Promise { if (!ws || ws.readyState !== WebSocket.OPEN) { return Promise.reject(new Error("WebSocket is not open")); } const id = uuid(); const message: ClientMessage = { id, event, data }; const promise = new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout waiting for '${event}'`)); }, timeoutMs); pending.set(id, { resolve, reject, timer }); }); console.debug("[WS] SEND (await):", message); ws.send(JSON.stringify(message)); return promise; } export function onEvent(event: string, handler: HandlerFn) { handlers.set(event, handler); } export function setDefaultHandler(handler: HandlerFn) { defaultHandler = handler; } export const isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;