waveterm/frontend/app/store/ws.ts

260 lines
6.9 KiB
TypeScript
Raw Normal View History

2024-06-12 02:42:10 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { type WebSocket, newWebSocket } from "@/util/wsutil";
import debug from "debug";
2024-06-12 02:42:10 +02:00
import { sprintf } from "sprintf-js";
2024-09-18 08:10:09 +02:00
const AuthKeyHeader = "X-AuthKey";
const dlog = debug("wave:ws");
2024-09-18 08:10:09 +02:00
const WarnWebSocketSendSize = 1024 * 1024; // 1MB
const MaxWebSocketSendSize = 5 * 1024 * 1024; // 5MB
const reconnectHandlers: (() => void)[] = [];
const StableConnTime = 2000;
function addWSReconnectHandler(handler: () => void) {
reconnectHandlers.push(handler);
}
function removeWSReconnectHandler(handler: () => void) {
const index = this.reconnectHandlers.indexOf(handler);
if (index > -1) {
reconnectHandlers.splice(index, 1);
}
}
2024-06-12 02:42:10 +02:00
type WSEventCallback = (arg0: WSEventType) => void;
2024-06-12 02:42:10 +02:00
2024-09-19 19:42:53 +02:00
type ElectronOverrideOpts = {
authKey: string;
};
2024-06-12 02:42:10 +02:00
class WSControl {
wsConn: WebSocket;
open: boolean;
2024-06-12 02:42:10 +02:00
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
2024-10-17 23:34:02 +02:00
tabId: string;
messageCallback: WSEventCallback;
2024-06-12 02:42:10 +02:00
watchSessionId: string = null;
watchScreenId: string = null;
wsLog: string[] = [];
baseHostPort: string;
lastReconnectTime: number = 0;
2024-09-19 19:42:53 +02:00
eoOpts: ElectronOverrideOpts;
noReconnect: boolean = false;
onOpenTimeoutId: NodeJS.Timeout = null;
2024-09-19 19:42:53 +02:00
constructor(
baseHostPort: string,
2024-10-17 23:34:02 +02:00
tabId: string,
2024-09-19 19:42:53 +02:00
messageCallback: WSEventCallback,
electronOverrideOpts?: ElectronOverrideOpts
) {
2024-06-12 02:42:10 +02:00
this.baseHostPort = baseHostPort;
this.messageCallback = messageCallback;
2024-10-17 23:34:02 +02:00
this.tabId = tabId;
this.open = false;
2024-09-19 19:42:53 +02:00
this.eoOpts = electronOverrideOpts;
2024-06-12 02:42:10 +02:00
setInterval(this.sendPing.bind(this), 5000);
}
shutdown() {
this.noReconnect = true;
this.wsConn.close();
}
2024-06-12 02:42:10 +02:00
connectNow(desc: string) {
if (this.open || this.noReconnect) {
2024-06-12 02:42:10 +02:00
return;
}
this.lastReconnectTime = Date.now();
dlog("try reconnect:", desc);
2024-06-12 02:42:10 +02:00
this.opening = true;
this.wsConn = newWebSocket(
2024-10-17 23:34:02 +02:00
this.baseHostPort + "/ws?tabid=" + this.tabId,
this.eoOpts
? {
[AuthKeyHeader]: this.eoOpts.authKey,
}
: null
);
this.wsConn.onopen = (e: Event) => {
this.onopen(e);
};
this.wsConn.onmessage = (e: MessageEvent) => {
this.onmessage(e);
};
this.wsConn.onclose = (e: CloseEvent) => {
this.onclose(e);
};
2024-06-12 02:42:10 +02:00
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose?: boolean) {
if (this.noReconnect) {
return;
}
if (this.open) {
2024-06-12 02:42:10 +02:00
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
}
return;
}
this.reconnectTimes++;
if (this.reconnectTimes > 20) {
dlog("cannot connect, giving up");
2024-06-12 02:42:10 +02:00
return;
}
2024-07-23 21:46:29 +02:00
const timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60];
2024-06-12 02:42:10 +02:00
let timeout = 60;
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
if (Date.now() - this.lastReconnectTime < 500) {
timeout = 1;
}
if (timeout > 0) {
dlog(sprintf("sleeping %ds", timeout));
2024-06-12 02:42:10 +02:00
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout * 1000);
}
onclose(event: CloseEvent) {
2024-06-12 02:42:10 +02:00
// console.log("close", event);
if (this.onOpenTimeoutId) {
clearTimeout(this.onOpenTimeoutId);
}
2024-06-12 02:42:10 +02:00
if (event.wasClean) {
dlog("connection closed");
2024-06-12 02:42:10 +02:00
} else {
dlog("connection error/disconnected");
2024-06-12 02:42:10 +02:00
}
if (this.open || this.opening) {
this.open = false;
2024-06-12 02:42:10 +02:00
this.opening = false;
this.reconnect();
}
}
onopen(e: Event) {
dlog("connection open");
this.open = true;
2024-06-12 02:42:10 +02:00
this.opening = false;
this.onOpenTimeoutId = setTimeout(() => {
this.reconnectTimes = 0;
dlog("clear reconnect times");
}, StableConnTime);
for (let handler of reconnectHandlers) {
handler();
}
2024-06-12 02:42:10 +02:00
this.runMsgQueue();
}
runMsgQueue() {
if (!this.open) {
2024-06-12 02:42:10 +02:00
return;
}
if (this.msgQueue.length == 0) {
return;
}
2024-07-23 21:46:29 +02:00
const msg = this.msgQueue.shift();
2024-06-12 02:42:10 +02:00
this.sendMessage(msg);
setTimeout(() => {
this.runMsgQueue();
}, 100);
}
onmessage(event: MessageEvent) {
2024-06-12 02:42:10 +02:00
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
}
if (eventData == null) {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
// nothing
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
} catch (e) {
console.log("[error] messageCallback", e);
}
}
}
sendPing() {
if (!this.open) {
2024-06-12 02:42:10 +02:00
return;
}
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
sendMessage(data: WSCommandType) {
if (!this.open) {
2024-06-12 02:42:10 +02:00
return;
}
2024-07-23 21:46:29 +02:00
const msg = JSON.stringify(data);
2024-06-12 02:42:10 +02:00
const byteSize = new Blob([msg]).size;
if (byteSize > MaxWebSocketSendSize) {
console.log("ws message too large", byteSize, data.wscommand, msg.substring(0, 100));
2024-06-12 02:42:10 +02:00
return;
}
2024-09-18 08:10:09 +02:00
if (byteSize > WarnWebSocketSendSize) {
console.log("ws message large", byteSize, data.wscommand, msg.substring(0, 100));
}
2024-06-12 02:42:10 +02:00
this.wsConn.send(msg);
}
pushMessage(data: WSCommandType) {
if (!this.open) {
2024-06-12 02:42:10 +02:00
this.msgQueue.push(data);
return;
}
this.sendMessage(data);
}
}
let globalWS: WSControl;
function initGlobalWS(
baseHostPort: string,
2024-10-17 23:34:02 +02:00
tabId: string,
messageCallback: WSEventCallback,
electronOverrideOpts?: ElectronOverrideOpts
) {
2024-10-17 23:34:02 +02:00
globalWS = new WSControl(baseHostPort, tabId, messageCallback, electronOverrideOpts);
}
function sendRawRpcMessage(msg: RpcMessage) {
const wsMsg: WSRpcCommand = { wscommand: "rpc", message: msg };
sendWSCommand(wsMsg);
}
function sendWSCommand(cmd: WSCommandType) {
globalWS?.pushMessage(cmd);
}
export {
WSControl,
addWSReconnectHandler,
globalWS,
initGlobalWS,
removeWSReconnectHandler,
sendRawRpcMessage,
sendWSCommand,
type ElectronOverrideOpts,
};