itender/web/src/utils/WebSocketHandler.ts
2025-05-21 10:27:20 +02:00

295 lines
12 KiB
TypeScript

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<any>) => 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<E extends WebSocketEvent>(
event: E,
fn: (payload: WebSocketPayload<WebSocketEventPayloadMap[E]>) => void
): cleanup {
// The type assertion 'as any' is necessary here because the eventRegister array
// is typed to accept WebSocketPayload<any> 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<WebSocketPayload<RequestTypeResponseMap[T]>> 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<T extends RequestType>(
type: T,
content: object | any = null,
timeout: number = 30
): Promise<RequestTypeResponseMap[T]> { // 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<any>): Promise<void> {
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<boolean> {
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<br>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<br>iTender synchronisiert die Datenbank mit der Cloud...`;
modal.addContent(txt);
modal.loader = true;
modal.open();
setTimeout(() => {
if (txt) {
txt.innerHTML = txt.innerHTML + "<br><br>Der Vorgang dauert länger als gewöhnlich.<br>Ü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.<br><br>`;
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.<br>Die Verbindung wird wiederhergestellt...<br>`;
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();
}
}