import {WebSocketEvent, WebSocketEventPayloadMap} from "../interfaces/WebSocketEvent.ts"; import {WebSocketPayload} from "../interfaces/WebSocketPayload.ts"; import {RequestType, RequestTypeResponseMap} from "../interfaces/RequestType.ts"; // Cleanup function type cleanup = () => void; export class WebSocketHandler { public static isConnected: boolean = false; private static socket: WebSocket; private static readonly url = (window.location.protocol == "http:" ? "ws://" : "wss://") + window.location.hostname + ":3005"; private static eventRegister: { event: WebSocketEvent, fn: (payload: WebSocketPayload) => void }[] = []; public static connect(onConnect: (() => void), onDisconnect: (wasClean: boolean) => void) { if (this.socket) { try { this.socket.close(0, "reconnect"); } catch { // ignored } } console.log("[WS] Connecting..."); WebSocketHandler.socket = new WebSocket(WebSocketHandler.url); WebSocketHandler.socket.onopen = (x) => { this.isConnected = true; this.onOpen(x); onConnect(); }; WebSocketHandler.socket.onclose = (ev) => { this.isConnected = false; this.onClose(); onDisconnect(ev.wasClean); } WebSocketHandler.socket.onerror = this.onError; WebSocketHandler.socket.onmessage = this.onMessage; } /** * Registers for an event, returns a cleanup function * The payload type is inferred from the WebSocketEventPayloadMap * @param event The WebSocketEvent to register for * @param fn The callback function to handle the event payload * @return Cleanup-Function */ public static registerForEvent( event: E, fn: (payload: WebSocketPayload) => void ): cleanup { // The type assertion 'as any' is necessary here because the eventRegister array // is typed to accept WebSocketPayload for flexibility in the onMessage handler. // TypeScript cannot statically guarantee that the payload type for a specific event // matches the generic constraint E at this point, but we handle the type safety // at the call site of registerForEvent. let obj = {event: event, fn: fn as any}; WebSocketHandler.eventRegister.push(obj); /** * cleanup function */ return () => { WebSocketHandler.eventRegister = WebSocketHandler.eventRegister.filter((e) => e != obj); } } /** * Request and response * The response payload data type is inferred from the RequestTypeResponseMap * @return Promise> A promise that resolves with the response payload data * @param type The RequestType * @param content The request content * @param timeout Time in seconds for timeout */ public static request( type: T, content: object | any = null, timeout: number = 30 ): Promise { // Use the mapped response type here console.log("[WS] Request to " + type) return new Promise((resolve, reject) => { let cancel = setTimeout(() => { // Check if cleanup is defined before calling it if (cleanup) cleanup(); reject(new Error("timeout")); }, timeout * 1000); // Use registerForEvent with the specific RESPONSE event type let cleanup = WebSocketHandler.registerForEvent(WebSocketEvent.RESPONSE, (payload) => { // The payload.data here is typed as WebSocketEventPayloadMap[WebSocketEvent.RESPONSE] (which is 'any' in our current map) // We rely on the check `(payload.data["type"] as RequestType) == type` to match the request type // and then assert the data type based on the RequestTypeResponseMap. if (payload.data && (payload.data["type"] as RequestType) == type) { clearTimeout(cancel); // Check if cleanup is defined before calling it if (cleanup) cleanup(); // Assert the data type based on the RequestTypeResponseMap resolve(payload.data.data as RequestTypeResponseMap[T]); // Resolve with the actual data part of the response } }); WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.REQUEST, { type: type, data: content })).catch(reject); }); } public static async send(payload: WebSocketPayload): Promise { console.log("[WS] Sending " + payload.event + " Event", payload); if (this.socket && this.socket.readyState == 1) { this.socket.send(payload.toString()); } else { console.warn("[WS] No socket or readyState is not 1"); } } private static checkConnection(): Promise { return new Promise(async resolve => { const xhr = new XMLHttpRequest(); xhr.open("GET", '/status', true); //Send the proper header information along with the request xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onreadystatechange = () => { // Call a function when the state changes. if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { resolve(true); } else if (xhr.readyState == XMLHttpRequest.DONE) { resolve(false); } } try { xhr.send(); } catch (e) { resolve(false); } }); } private static onMessage(msgEvent: MessageEvent) { let payload = WebSocketPayload.parseFromBase64Json(msgEvent.data); if (!payload) { console.log("[WS] Could not parse message: ", msgEvent); return; } console.log("[WS] Received " + payload.event + " Event", payload); for (let evReg of WebSocketHandler.eventRegister) { if (evReg.event == payload.event) evReg.fn(payload); } /*switch (payload.event) { case WebSocketEvent.CONFIG: { Setup.onConfigUpdate(payload); break; } case WebSocketEvent.DRINKS: { WebHandler.onDrinkUpdate(payload); break; } case WebSocketEvent.ERROR: { /!* let modal = new Modal("error", "Aww crap!"); let txt = document.createElement("p"); txt.innerHTML = payload.data; modal.addContent(txt); modal.addContent(document.createElement("br")); modal.addButton(ButtonType.SECONDARY, "Schließen", () => modal.close()); modal.open(); Settings.inUpdate = false;*!/ console.error(payload); break; } // Incoming WebSocketStatus case WebSocketEvent.STATUS: { let statusElement = document.getElementById("status"); if (statusElement) statusElement.innerText = payload.data.status; let status: iTenderStatus = payload.data.status; switch (status) { case iTenderStatus.READY: { Modal.close("start"); Modal.close("setup"); Modal.close("fill"); Modal.close("download"); if (WebHandler.currentPane != Pane.MENU) WebHandler.openPane(Pane.MAIN); (document.getElementById("menuBtn") as HTMLButtonElement).disabled = false; break; } case iTenderStatus.STARTING: { let modal = new Modal("start", "Willkommen!"); let txt = document.createElement("p"); txt.innerHTML = `Einen Augenblick bitte
iTender startet...`; modal.addContent(txt); modal.loader = true; modal.open(); break; } case iTenderStatus.DOWNLOADING: { let modal = new Modal("download", "Aktualisieren"); let txt = document.createElement("p"); txt.innerHTML = `Einen Augenblick bitte
iTender synchronisiert die Datenbank mit der Cloud...`; modal.addContent(txt); modal.loader = true; modal.open(); setTimeout(() => { if (txt) { txt.innerHTML = txt.innerHTML + "

Der Vorgang dauert länger als gewöhnlich.
Überprüfe deine Internetverbindung!" } }, 1000 * 15) break; } case iTenderStatus.SETUP: { Modal.close("start"); Setup.openSetup(); break; } case iTenderStatus.FILLING: { Fill.onFillEvent(payload); break; } default: { console.log("Unknown to handle " + status); } } break; } }*/ } private static onOpen(event: Event) { console.log("[WS] Connected", event); } private static onClose() { console.error("[WS] Closed!"); /*if (event.wasClean) { let modal = new Modal("socketClosed", "Sitzung beendet!"); let txt = document.createElement("p"); txt.innerHTML = `Diese Sitzung wurde beendet, da der iTender nun an einem anderen Gerät bzw. an dem Hauptgerät gesteuert wird.

`; modal.addContent(txt); modal.addButton(ButtonType.PRIMARY, "Sitzung wiederherstellen", () => { window.location.reload(); }); modal.open(); } else { setInterval(async () => { if ((await WebWebSocketHandler.checkConnection())) window.location.reload(); }, 2000); if (Settings.inUpdate) return; let modal = new Modal("socketClosed", "Verbindungsproblem!"); let txt = document.createElement("p"); txt.innerHTML = `Die Benutzeroberfläche hat die Verbindung mit dem Gerät verloren.
Die Verbindung wird wiederhergestellt...
`; modal.addContent(txt); modal.loader = true; modal.open(); } /!* let connectionElement = document.getElementById("right"); if (connectionElement) { connectionElement.innerText = "Getrennt"; connectionElement.style.color = "red"; }*!/*/ } private static onError(event: any) { console.error("[WS] Error", event); /*let connectionElement = document.getElementById("right"); if (connectionElement) connectionElement.innerText = "Fehler";*/ //openModal("Einen Augenblick...", `Es wurde ein kritischer Fehler festgestellt.\nBitte warten Sie, während der Prozess neu gestartet wird...` ); //window.location.reload(); } }