diff --git a/src/simplerenderer.tsx b/src/simplerenderer.tsx index 05dfed284..552d8ac07 100644 --- a/src/simplerenderer.tsx +++ b/src/simplerenderer.tsx @@ -36,7 +36,7 @@ class SimpleBlobRendererModel { this.savedHeight = params.savedHeight; this.ptyDataSource = params.ptyDataSource; if (this.isDone.get()) { - this.reload(0); + setTimeout(() => this.reload(0), 10); } } @@ -74,6 +74,10 @@ class SimpleBlobRendererModel { this.loading.set(true); })(); let rtnp = this.ptyDataSource(this.context); + if (rtnp == null) { + console.log("no promise returned from ptyDataSource (simplerenderer)", this.context); + return; + } rtnp.then((ptydata) => { setTimeout(() => { this.ptyData = ptydata; diff --git a/src/term.ts b/src/term.ts index a0c541d1d..7636173da 100644 --- a/src/term.ts +++ b/src/term.ts @@ -109,7 +109,7 @@ class TermWrap { if (opts.customKeyHandler != null) { this.terminal.attachCustomKeyEventHandler((e) => opts.customKeyHandler(e, this)); } - this.reload(0); + setTimeout(() => this.reload(0), 10); } getUsedRows() : number { @@ -281,6 +281,10 @@ class TermWrap { this.reloading = true; this.terminal.reset(); let rtnp = this.ptyDataSource(this.termContext); + if (rtnp == null) { + console.log("no promise returned from ptyDataSource (termwrap)", this.termContext); + return; + } rtnp.then((ptydata) => { setTimeout(() => { this._reloadThenHandler(ptydata); diff --git a/src/types.ts b/src/types.ts index fd65c3bdc..9a035ae7a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -535,9 +535,11 @@ type WebCmd = { }; type WebFullScreen = { + screenid : string, screen : WebScreen, lines : WebLine[], cmds : WebCmd[], + cmdptymap : Record, vts : number, } @@ -548,16 +550,6 @@ type PtyDataUpdate = { data : string, }; -type WebScreenUpdate = { - type : string, - screenid : string, - screen : WebScreen, - lines : WebLine[], - cmds : WebCmd[], - ptydata : PtyDataUpdate[], - removedlines : string[], -}; - type WebShareWSMessage = { type : string, screenid : string, @@ -589,4 +581,4 @@ type RendererContainerType = { type LineHeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void; -export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs, WebFullScreen, WebScreen, WebLine, WebCmd, RemoteTermContext, TermContextUnion, WebRemote, WebScreenUpdate, PtyDataUpdate, WebShareWSMessage, LineHeightChangeCallbackType, LineFactoryProps, LineInterface, RendererContainerType}; +export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, FeStateType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs, WebFullScreen, WebScreen, WebLine, WebCmd, RemoteTermContext, TermContextUnion, WebRemote, PtyDataUpdate, WebShareWSMessage, LineHeightChangeCallbackType, LineFactoryProps, LineInterface, RendererContainerType}; diff --git a/src/webshare-elems.tsx b/src/webshare-elems.tsx index c38152de7..d6a5b56e8 100644 --- a/src/webshare-elems.tsx +++ b/src/webshare-elems.tsx @@ -24,7 +24,6 @@ type OMap = mobx.ObservableMap; let foo = LinesView; // TODO reshare -// TODO debounce some of the updates function makeFullRemoteRef(ownerName : string, remoteRef : string, name : string) : string { if (isBlank(ownerName) && isBlank(name)) { @@ -624,7 +623,7 @@ class WebScreenView extends React.Component<{}, {}> { } render() { - let fullScreen = WebShareModel.screen.get(); + let fullScreen = WebShareModel.fullScreen.get(); if (fullScreen == null || fullScreen.lines.length == 0) { return this.renderEmpty(); } @@ -643,7 +642,7 @@ class WebShareMain extends React.Component<{}, {}> { } render() { - let screen = WebShareModel.screen.get(); + let screen = WebShareModel.fullScreen.get(); let errMessage = WebShareModel.errMessage.get(); let shareName = ""; if (screen != null) { diff --git a/src/webshare-model.ts b/src/webshare-model.ts index 51f9261a4..247e40f19 100644 --- a/src/webshare-model.ts +++ b/src/webshare-model.ts @@ -14,6 +14,10 @@ type OArr = mobx.IObservableArray; type OMap = mobx.ObservableMap; type CV = mobx.IComputedValue; +type PtyListener = { + receiveData(ptyPos : number, data : Uint8Array, reason? : string); +}; + function isBlank(s : string) { return (s == null || s == ""); } @@ -30,7 +34,7 @@ class WebShareModelClass { viewKey : string; screenId : string; errMessage : OV = mobx.observable.box(null, {name: "errMessage"}); - screen : OV = mobx.observable.box(null, {name: "webScreen"}); + fullScreen : OV = mobx.observable.box(null, {name: "webScreen"}); terminals : Record = {}; // lineid => TermWrap renderers : Record = {}; // lineid => RendererModel contentHeightCache : Record = {}; // lineid => height @@ -39,12 +43,17 @@ class WebShareModelClass { selectedLine : OV = mobx.observable.box(0, {name: "selectedLine"}); syncSelectedLine : OV = mobx.observable.box(true, {name: "syncSelectedLine"}); lastScreenSize : T.WindowSize = null; + activePtyFetch : Record = {}; // lineid -> active + localPtyOffsetMap : Record = {}; + remotePtyOffsetMap : Record = {}; + activeUpdateFetch : boolean = false; + remoteScreenVts : number = 0; constructor() { let urlParams = new URLSearchParams(window.location.search); this.viewKey = urlParams.get("viewkey"); this.screenId = urlParams.get("screenid"); - setTimeout(() => this.loadFullScreenData(), 10); + setTimeout(() => this.loadFullScreenData(false), 10); this.wsControl = new WebShareWSControl(getBaseWSUrl(), this.screenId, this.viewKey, this.wsMessageCallback.bind(this)); document.addEventListener("keydown", this.docKeyDownHandler.bind(this)); } @@ -59,7 +68,7 @@ class WebShareModelClass { mobx.action(() => { this.syncSelectedLine.set(val); if (val) { - let fullScreen = this.screen.get(); + let fullScreen = this.fullScreen.get(); if (fullScreen != null) { this.selectedLine.set(fullScreen.screen.selectedline); } @@ -103,7 +112,7 @@ class WebShareModelClass { } getServerSelectedLine() : number { - let fullScreen = this.screen.get(); + let fullScreen = this.fullScreen.get(); if (fullScreen != null) { return fullScreen.screen.selectedline; } @@ -116,7 +125,7 @@ class WebShareModelClass { } updateSelectedLineIndex(delta : number) : void { - let fullScreen = this.screen.get(); + let fullScreen = this.fullScreen.get(); if (fullScreen == null) { return; } @@ -209,14 +218,20 @@ class WebShareModelClass { fullScreen.cmds.push(newCmd); } - mergeUpdate(msg : T.WebScreenUpdate) { + mergeUpdate(msg : T.WebFullScreen) { if (msg.screenid != this.screenId) { - console.log("bad WebScreenUpdate, wrong screenid", msg.screenid); + console.log("bad WebFullScreen update, wrong screenid", msg.screenid); return; } - console.log("merge", msg); + console.log("merge screen-update", "vts=" + msg.vts); + // console.log("merge", "vts=" + msg.vts, msg); mobx.action(() => { - let fullScreen = this.screen.get(); + let fullScreen = this.fullScreen.get(); + if (fullScreen.vts >= msg.vts) { + console.log("stale merge", "cur-vts=" + fullScreen.vts, "merge-vts=" + msg.vts); + return; + } + fullScreen.vts = msg.vts; if (msg.screen) { fullScreen.screen = msg.screen; if (this.syncSelectedLine.get()) { @@ -225,6 +240,10 @@ class WebShareModelClass { } if (msg.lines != null && msg.lines.length > 0) { for (let line of msg.lines) { + if (line.archived) { + this.removeLine(fullScreen, line.lineid); + continue; + } this.mergeLine(fullScreen, line); } } @@ -233,33 +252,118 @@ class WebShareModelClass { this.mergeCmd(fullScreen, cmd); } } - if (msg.ptydata != null && msg.ptydata.length > 0) { - for (let data of msg.ptydata) { - let termWrap = this.getTermWrap(data.lineid); - if (termWrap == null) { - continue; - } - let dataArr = base64ToArray(data.data); - termWrap.receiveData(data.ptypos, dataArr, "ws:ptydata"); - } + this.handleCmdPtyMap(msg.cmdptymap); + })(); + } + + handleCmdPtyMap(ptyMap : Record) { + if (ptyMap == null) { + return; + } + for (let lineId in ptyMap) { + let newOffset = ptyMap[lineId]; + this.remotePtyOffsetMap[lineId] = newOffset; + let localOffset = this.localPtyOffsetMap[lineId]; + if (localOffset != null && localOffset < newOffset) { + this.runPtyFetch(lineId); } - if (msg.removedlines != null && msg.removedlines.length > 0) { - for (let lineid of msg.removedlines) { - this.removeLine(fullScreen, lineid); + } + } + + runPtyFetch(lineId : string) { + let prtn = this.checkFetchPtyData(lineId, false); + let ptyListener = this.getPtyListener(lineId); + if (ptyListener != null) { + prtn.then((ptydata) => { + ptyListener.receiveData(ptydata.pos, ptydata.data, "model-fetch"); + if (ptydata.data.length > 0) { + setTimeout(() => this.checkFetchPtyData(lineId, false), 100); } + }); + } + } + + getPtyListener(lineId : string) { + let termWrap = this.getTermWrap(lineId); + if (termWrap != null) { + return termWrap; + } + let renderer = this.getRenderer(lineId); + if (renderer != null) { + return renderer; + } + return null; + } + + receivePtyData(lineId : string, ptyPos : number, data : Uint8Array, reason? : string) : void { + let termWrap = this.getTermWrap(lineId); + if (termWrap != null) { + termWrap.receiveData(ptyPos, data, reason); + } + let renderer = this.getRenderer(lineId); + if (renderer != null) { + renderer.receiveData(ptyPos, data, reason); + } + } + + checkFetchPtyData(lineId : string, reload : boolean) : Promise { + let lineNum = this.getLineNumFromId(lineId); + if (this.activePtyFetch[lineId]) { + // console.log("check-fetch", lineNum, "already running"); + return; + } + if (reload) { + this.localPtyOffsetMap[lineId] = 0; + } + let ptyOffset = this.localPtyOffsetMap[lineId]; + if (ptyOffset == null) { + // console.log("check-fetch", lineNum, "no local offset"); + return; + } + let remotePtyOffset = this.remotePtyOffsetMap[lineId]; + if (ptyOffset >= remotePtyOffset) { + // up to date + return Promise.resolve({pos: ptyOffset, data: new Uint8Array(0)}); + } + this.activePtyFetch[lineId] = true; + let viewKey = WebShareModel.viewKey; + // console.log("fetch pty", lineNum, "pos=" + ptyOffset); + let usp = new URLSearchParams({screenid: this.screenId, viewkey: viewKey, lineid: lineId, pos: String(ptyOffset)}); + let url = new URL(getBaseUrl() + "/webshare/ptydata?" + usp.toString()); + return fetch(url, {method: "GET", mode: "cors", cache: "no-cache"}).then((resp) => { + if (!resp.ok) { + throw new Error(sprintf("Bad fetch response for /webshare/ptydata: %d %s", resp.status, resp.statusText)); } - })(); + let ptyOffsetStr = resp.headers.get("X-PtyDataOffset"); + if (ptyOffsetStr != null && !isNaN(parseInt(ptyOffsetStr))) { + ptyOffset = parseInt(ptyOffsetStr); + } + return resp.arrayBuffer(); + }).then((buf) => { + let dataArr = new Uint8Array(buf); + let newOffset = ptyOffset + dataArr.length; + console.log("fetch pty success", lineNum, "len=" + dataArr.length, "pos => " + newOffset); + this.localPtyOffsetMap[lineId] = newOffset; + return {pos: ptyOffset, data: dataArr}; + }).finally(() => { + this.activePtyFetch[lineId] = false; + }); } wsMessageCallback(msg : any) { if (msg.type == "webscreen:update") { - this.mergeUpdate(msg); + console.log("[ws] update vts", msg.vts); + if (msg.vts > this.remoteScreenVts) { + this.remoteScreenVts = msg.vts; + setTimeout(() => this.checkUpdateScreenData(), 10); + } return; } - console.log("ws message", msg); + console.log("[ws] unhandled message", msg); } setWebFullScreen(screen : T.WebFullScreen) { + console.log("got initial screen", "vts=" + screen.vts); mobx.action(() => { if (screen.lines == null) { screen.lines = []; @@ -267,7 +371,9 @@ class WebShareModelClass { if (screen.cmds == null) { screen.cmds = []; } - this.screen.set(screen); + this.handleCmdPtyMap(screen.cmdptymap); + screen.cmdptymap = null; + this.fullScreen.set(screen); this.wsControl.reconnect(true); if (this.syncSelectedLine.get()) { this.selectedLine.set(screen.screen.selectedline); @@ -303,6 +409,10 @@ class WebShareModelClass { onUpdateContentHeight: (termContext : T.RendererContext, height : number) => { this.setContentHeight(termContext, height); }, }); this.terminals[lineId] = termWrap; + if (this.localPtyOffsetMap[lineId] == null) { + this.localPtyOffsetMap[lineId] = 0; + } + this.localPtyOffsetMap[lineId] = 0; if (this.getSelectedLine() == line.linenum) { termWrap.giveFocus(); } @@ -358,6 +468,7 @@ class WebShareModelClass { term.dispose(); delete this.terminals[lineId]; } + delete this.localPtyOffsetMap[lineId]; } getUsedRows(context : T.RendererContext, line : T.WebLine, cmd : T.WebCmd, width : number) : number { @@ -391,11 +502,26 @@ class WebShareModelClass { return this.renderers[lineId]; } - registerRenderer(cmdId : string, renderer : T.RendererModel) { - this.renderers[cmdId] = renderer; + registerRenderer(lineId : string, renderer : T.RendererModel) { + this.renderers[lineId] = renderer; + if (this.localPtyOffsetMap[lineId] == null) { + this.localPtyOffsetMap[lineId] = 0; + } } - loadFullScreenData() : void { + checkUpdateScreenData() : void { + let fullScreen = this.fullScreen.get(); + if (fullScreen == null) { + return; + } + // console.log("check-update", "vts=" + fullScreen.vts, "remote-vts=" + this.remoteScreenVts); + if (fullScreen.vts >= this.remoteScreenVts) { + return; + } + this.loadFullScreenData(true); + } + + loadFullScreenData(update : boolean) : void { if (isBlank(this.screenId)) { this.setErrMessage("No ScreenId Specified, Cannot Load."); return; @@ -404,20 +530,53 @@ class WebShareModelClass { this.setErrMessage("No ViewKey Specified, Cannot Load."); return; } - let usp = new URLSearchParams({screenid: this.screenId, viewkey: this.viewKey}); + if (this.activeUpdateFetch) { + // console.log("there is already an active update fetch"); + return; + } + // console.log("running screen-data update"); + this.activeUpdateFetch = true; + let urlParams : Record = {screenid: this.screenId, viewkey: this.viewKey}; + if (update) { + let fullScreen = this.fullScreen.get(); + if (fullScreen != null) { + urlParams.vts = String(fullScreen.vts); + } + } + let usp = new URLSearchParams(urlParams); let url = new URL(getBaseUrl() + "/webshare/screen?" + usp.toString()); fetch(url, {method: "GET", mode: "cors", cache: "no-cache"}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { - mobx.action(() => { - let screen : T.WebFullScreen = data; + let screen : T.WebFullScreen = data; + if (update) { + this.mergeUpdate(screen); + } + else { this.setWebFullScreen(screen); - })(); + } + setTimeout(() => this.checkUpdateScreenData(), 300); }).catch((err) => { this.errMessage.set("Cannot get screen: " + err.message); + }).finally(() => { + this.activeUpdateFetch = false; }); } + getLineNumFromId(lineId : string) : number { + let fullScreen = this.fullScreen.get(); + if (fullScreen == null) { + return -1; + } + for (let i=0; i { - if (!resp.ok) { - throw new Error(sprintf("Bad fetch response for /webshare/ptydata: %d %s", resp.status, resp.statusText)); - } - let ptyOffsetStr = resp.headers.get("X-PtyDataOffset"); - if (ptyOffsetStr != null && !isNaN(parseInt(ptyOffsetStr))) { - ptyOffset = parseInt(ptyOffsetStr); - } - return resp.arrayBuffer(); - }).then((buf) => { - return {pos: ptyOffset, data: new Uint8Array(buf)}; - }); + return WebShareModel.checkFetchPtyData(termContext.lineId, true); } let WebShareModel : WebShareModelClass = null; diff --git a/src/webshare-ws.ts b/src/webshare-ws.ts index 030723f60..af7508ebe 100644 --- a/src/webshare-ws.ts +++ b/src/webshare-ws.ts @@ -23,7 +23,7 @@ class WebShareWSControl { this.viewKey = viewKey; this.open = mobx.observable.box(false, {name: "WSOpen"}); this.closed = true; - setInterval(this.sendPing, 15000); + setInterval(this.sendPing, 20000); } close() : void {