295 lines
12 KiB
TypeScript
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();
|
|
}
|
|
|
|
|
|
} |