From 414cd6bf0a1c4b295191109600e3dd9d138eb24b Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 29 Mar 2023 17:51:42 -0700 Subject: [PATCH] webshare websocket implementation --- src/webshare-elems.tsx | 2 +- src/webshare-model.ts | 28 ++++-- src/webshare-ws.ts | 190 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/webshare-ws.ts diff --git a/src/webshare-elems.tsx b/src/webshare-elems.tsx index 7962a9a1..af01b946 100644 --- a/src/webshare-elems.tsx +++ b/src/webshare-elems.tsx @@ -18,7 +18,7 @@ type OArr = mobx.IObservableArray; type OMap = mobx.ObservableMap; // TODO selection -// TODO remotevars +// TODO websocket function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string { if (isBlank(ownerName) && isBlank(name)) { diff --git a/src/webshare-model.ts b/src/webshare-model.ts index 9d81973f..77af2f87 100644 --- a/src/webshare-model.ts +++ b/src/webshare-model.ts @@ -6,6 +6,7 @@ import * as T from "./types"; import {TermWrap} from "./term"; import * as lineutil from "./lineutil"; import {windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure"; +import {WebShareWSControl} from "./webshare-ws"; type OV = mobx.IObservableValue; type OArr = mobx.IObservableArray; @@ -20,6 +21,10 @@ function getBaseUrl() { return "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"; } +function getBaseWSUrl() { + return "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev"; +} + class WebShareModelClass { viewKey : string; screenId : string; @@ -29,13 +34,14 @@ class WebShareModelClass { renderers : Record = {}; // lineid => RendererModel contentHeightCache : Record = {}; // lineid => height selectedLine : OV = mobx.observable.box(0, {name: "selectedLine"}); + wsControl : WebShareWSControl; constructor() { let urlParams = new URLSearchParams(window.location.search); this.viewKey = urlParams.get("viewkey"); this.screenId = urlParams.get("screenid"); setTimeout(() => this.loadFullScreenData(), 10); - + this.wsControl = new WebShareWSControl(getBaseWSUrl(), this.screenId, this.viewKey, this.wsMessageCallback.bind(this)); } setErrMessage(msg : string) : void { @@ -52,6 +58,21 @@ class WebShareModelClass { return 12; } + wsMessageCallback(msg : any) { + console.log("ws message", msg); + } + + setWebFullScreen(screen : T.WebFullScreen) { + mobx.action(() => { + this.screen.set(screen); + if (screen.lines != null && screen.lines.length > 0) { + this.selectedLine.set(screen.lines[screen.lines.length-1].linenum); + } + this.wsControl.reconnect(true); + })(); + + } + loadTerminalRenderer(elem : Element, line : T.WebLine, cmd : T.WebCmd, width : number) : void { let lineId = cmd.lineid; let termWrap = this.getTermWrap(lineId); @@ -181,10 +202,7 @@ class WebShareModelClass { fetch(url, {method: "GET", mode: "cors", cache: "no-cache"}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { mobx.action(() => { let screen : T.WebFullScreen = data; - this.screen.set(screen); - if (screen.lines != null && screen.lines.length > 0) { - this.selectedLine.set(screen.lines[screen.lines.length-1].linenum); - } + this.setWebFullScreen(screen); })(); }).catch((err) => { this.errMessage.set("Cannot get screen: " + err.message); diff --git a/src/webshare-ws.ts b/src/webshare-ws.ts new file mode 100644 index 00000000..b97c382d --- /dev/null +++ b/src/webshare-ws.ts @@ -0,0 +1,190 @@ +import * as mobx from "mobx"; +import {sprintf} from "sprintf-js"; +import {boundMethod} from "autobind-decorator"; +import {WatchScreenPacketType} from "./types"; +import dayjs from "dayjs"; + +class WebShareWSControl { + wsConn : any; + open : mobx.IObservableValue; + opening : boolean = false; + reconnectTimes : number = 0; + msgQueue : any[] = []; + messageCallback : (any) => void = null; + screenId : string = null; + viewKey : string = null; + wsUrl : string; + closed : boolean; + + constructor(wsUrl : string, screenId : string, viewKey : string, messageCallback : (any) => void) { + this.wsUrl = wsUrl; + this.messageCallback = messageCallback; + this.screenId = screenId; + this.viewKey = viewKey; + this.open = mobx.observable.box(false, {name: "WSOpen"}); + this.closed = true; + setInterval(this.sendPing, 15000); + } + + close() : void { + this.closed = true; + if (this.wsConn != null) { + this.wsConn.close(); + } + } + + log(str : string) { + console.log("[wscontrol]", str); + } + + @mobx.action + setOpen(val : boolean) { + mobx.action(() => { + this.open.set(val); + })(); + } + + connectNow(desc : string) { + this.closed = false; + if (this.open.get()) { + return; + } + this.log(sprintf("try reconnect (%s)", desc)); + this.opening = true; + this.wsConn = new WebSocket(this.wsUrl); + this.wsConn.onopen = this.onopen; + this.wsConn.onmessage = this.onmessage; + this.wsConn.onclose = this.onclose; + // turns out onerror is not necessary (onclose always follows onerror) + // this.wsConn.onerror = this.onerror; + } + + reconnect(forceClose? : boolean) { + this.closed = false; + if (this.open.get()) { + if (forceClose) { + this.wsConn.close(); // this will force a reconnect + } + return; + } + this.reconnectTimes++; + if (this.reconnectTimes > 20) { + this.log("cannot connect, giving up"); + return; + } + let timeoutArr = [0, 5, 5, 15, 30, 60, 300, 3600]; + let timeout = 60; + if (this.reconnectTimes < timeoutArr.length) { + timeout = timeoutArr[this.reconnectTimes]; + } + if (timeout > 0) { + this.log(sprintf("sleeping %ds", timeout)); + } + setTimeout(() => { + this.connectNow(String(this.reconnectTimes)); + }, timeout*1000); + } + + @boundMethod + onclose(event : any) { + // console.log("close", event); + if (event.wasClean) { + this.log("connection closed"); + } + else { + this.log("connection error/disconnected"); + } + if (this.open.get() || this.opening) { + this.setOpen(false); + this.opening = false; + if (!this.closed) { + this.reconnect(); + } + } + } + + @boundMethod + onopen() { + this.log("connection open"); + this.setOpen(true); + this.opening = false; + this.runMsgQueue(); + this.sendWebShareInit(); + // reconnectTimes is reset in onmessage:hello + } + + runMsgQueue() { + if (!this.open.get()) { + return; + } + if (this.msgQueue.length == 0) { + return; + } + let msg = this.msgQueue.shift(); + this.sendMessage(msg); + setTimeout(() => { + this.runMsgQueue(); + }, 100); + } + + @boundMethod + onmessage(event : any) { + 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 (eventData.type == "hello") { + this.reconnectTimes = 0; + return; + } + if (this.messageCallback) { + try { + this.messageCallback(eventData); + } + catch (e) { + this.log("[error] messageCallback", e); + } + } + } + + @boundMethod + sendPing() { + if (!this.open.get()) { + return; + } + this.wsConn.send(JSON.stringify({type: "ping", stime: Date.now()})); + } + + sendMessage(data : any) { + if (!this.open.get()) { + return; + } + this.wsConn.send(JSON.stringify(data)); + } + + pushMessage(data : any) { + if (!this.open.get()) { + this.msgQueue.push(data); + return; + } + this.sendMessage(data); + } + + sendWebShareInit() { + let pk : WatchScreenPacketType = {"type": "webshare", screenid: this.screenId, viewkey: this.viewKey}; + this.pushMessage(pk); + } +} + +export {WebShareWSControl};