Files
snake-python/templates/files/js/DashboardWebSocket.js
T
2026-04-07 03:25:10 +02:00

171 lines
5.2 KiB
JavaScript

class DashboardWebSocket {
constructor({ onGamesUpdate, onShutdown } = {}) {
this._socket = null;
this._reconnectTimer = null;
this._shuttingDown = false;
this._pendingRequests = new Map();
this._requestSeq = 0;
this._onGamesUpdate = onGamesUpdate || (() => {});
this._onShutdown = onShutdown || (() => {});
}
get isShuttingDown() { return this._shuttingDown; }
connect() {
if (this._shuttingDown) return;
if (this._socket && (
this._socket.readyState === WebSocket.OPEN ||
this._socket.readyState === WebSocket.CONNECTING
)) return;
const wsUrl = this._buildUrl();
try {
this._socket = new WebSocket(wsUrl);
} catch {
this._scheduleReconnect();
return;
}
this._socket.addEventListener("message", (event) => {
let payload = null;
try { payload = JSON.parse(event.data); } catch { return; }
if (!payload || !payload.type) return;
if (payload.type === "dashboard_ws_shutdown") {
this._shuttingDown = true;
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
this._rejectAll("Server shutting down");
if (this._socket) this._socket.close();
this._onShutdown();
return;
}
if (payload.type === "dashboard_game_replay") {
const requestId = String(payload.request_id || "");
if (!requestId) return;
const pending = this._pendingRequests.get(requestId);
if (!pending) return;
this._pendingRequests.delete(requestId);
window.clearTimeout(pending.timeoutId);
if (payload.error) {
pending.reject(new Error(String(payload.error)));
return;
}
pending.resolve(payload.replay || null);
return;
}
if (payload.type === "dashboard_games_update") {
this._onGamesUpdate(payload);
}
});
this._socket.addEventListener("close", () => {
this._socket = null;
this._rejectAll("Dashboard websocket disconnected");
if (!this._shuttingDown) this._scheduleReconnect();
});
this._socket.addEventListener("error", () => {
if (this._socket) this._socket.close();
});
}
waitForOpen(timeoutMs = 4000) {
if (this._shuttingDown) return Promise.resolve(false);
if (!this._socket || this._socket.readyState === WebSocket.CLOSED) this.connect();
if (this._socket && this._socket.readyState === WebSocket.OPEN) return Promise.resolve(true);
return new Promise((resolve) => {
if (!this._socket) { resolve(false); return; }
const socketRef = this._socket;
let settled = false;
const cleanup = () => {
socketRef.removeEventListener("open", onOpen);
socketRef.removeEventListener("close", onClose);
socketRef.removeEventListener("error", onError);
window.clearTimeout(timeoutId);
};
const finish = (value) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
};
const onOpen = () => finish(true);
const onClose = () => finish(false);
const onError = () => finish(false);
const timeoutId = window.setTimeout(() => finish(false), timeoutMs);
socketRef.addEventListener("open", onOpen);
socketRef.addEventListener("close", onClose);
socketRef.addEventListener("error", onError);
});
}
async requestReplay(gameId) {
const isOpen = await this.waitForOpen();
if (!isOpen || !this._socket || this._socket.readyState !== WebSocket.OPEN) {
throw new Error("Dashboard websocket unavailable");
}
const requestId = `replay-${Date.now()}-${this._requestSeq++}`;
return await new Promise((resolve, reject) => {
const timeoutId = window.setTimeout(() => {
this._pendingRequests.delete(requestId);
reject(new Error(`Replay websocket timeout for ${gameId}`));
}, 4000);
this._pendingRequests.set(requestId, { resolve, reject, timeoutId });
try {
this._socket.send(JSON.stringify({
type: "dashboard_game_replay_request",
request_id: requestId,
game_id: gameId,
}));
} catch (error) {
window.clearTimeout(timeoutId);
this._pendingRequests.delete(requestId);
reject(error);
}
});
}
shutdown() {
this._shuttingDown = true;
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
this._rejectAll("Dashboard unloading");
if (this._socket) {
this._socket.close();
this._socket = null;
}
}
_buildUrl() {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
return `${protocol}://${window.location.host}/dashboard/ws/games`;
}
_scheduleReconnect() {
if (this._shuttingDown) return;
if (this._reconnectTimer) return;
this._reconnectTimer = window.setTimeout(() => {
this._reconnectTimer = null;
this.connect();
}, 1500);
}
_rejectAll(message) {
for (const pending of this._pendingRequests.values()) {
window.clearTimeout(pending.timeoutId);
pending.reject(new Error(message));
}
this._pendingRequests.clear();
}
}