Files
KnockOutWhist-Frontend/src/services/ws.ts
lq64 39898ed03b feat/FRO-31: Added ingame (#20)
Force push of Janis ingame changes

Co-authored-by: Janis <janis.e.20@gmx.de>
Reviewed-on: #20
2025-12-11 00:15:50 +01:00

263 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<T = any> = {
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<T = any> = {
id: string;
event: string;
data: T;
};
export type HandlerFn<T = any> = (data: T) => unknown | Promise<unknown>;
interface PendingEntry {
resolve: (data: any) => void;
reject: (err: Error) => void;
timer: ReturnType<typeof setTimeout>;
}
let ws: WebSocket | null = null;
const pending = new Map<string, PendingEntry>();
const handlers = new Map<string, HandlerFn>();
const uState = useIngame();
let heartbeatTimer: ReturnType<typeof setInterval> | 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 dont want auto reconnect, keep as is.
location.href = "/mainmenu";
};
}
export function connectWebSocket(url?: string): Promise<void> {
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<any> {
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;