move dashboard script block content into own files with new classes to update, render to have a better code overview
Build and Push Docker Container / build-and-push (push) Successful in 3m55s
Build and Push Docker Container / build-and-push (push) Successful in 3m55s
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user