From be621228fb319ec997ec8dd1d145b47c5a83f525 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 14 Aug 2026 15:20:41 -0400 Subject: [PATCH 1/2] fix(remoteSession): re-assert kept-alive markers before applying states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vtkObjectManager::UpdateObjectsFromStates only deserializes fetched states under roots carrying "vtk-object-manager-kept-alive", but any local serialization — e.g. `get` on a live object, which re-serializes its whole reference graph — rebuilds stored states WITHOUT the marker (https://gitlab.kitware.com/vtk/vtk/-/work_items/20099). One `get` on an object referencing a render window (an interactor style, a camera) permanently detaches that window's subtree from server updates: new states fetch and register but never apply, and only a page reload recovers. Track the ids the server flagged in their fetched payloads and re-stamp them via a partial registerState (which merges) right before applying states. Ids are dropped when the server ignores them, and re-stamping is restricted to ids in the current update's dependency report so one render window's update never touches another's roots. --- src/remoteSession.js | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/remoteSession.js b/src/remoteSession.js index 1b5c47a..de45dd2 100644 --- a/src/remoteSession.js +++ b/src/remoteSession.js @@ -28,6 +28,15 @@ export class RemoteSession { this._pendingUpdates = new Map(); // vtkId -> queued-but-not-started run (coalescing) this.currentMTime = 1; this.stateMTimes = {}; + // ids whose fetched state carried "vtk-object-manager-kept-alive". The + // deserializer only applies fetched states to subtrees rooted in a state + // with that marker, but any local serialization (e.g. `get` on a live + // object) rebuilds the stored state WITHOUT it (see + // https://gitlab.kitware.com/vtk/vtk/-/work_items/20099). Remember which + // ids the server flagged so updateAsync can re-stamp them before applying + // states; otherwise one `get` on an object referencing a render window + // silently detaches that window from all future server updates. + this.keptAliveStateIds = new Set(); this.hashesMTime = {}; this.pendingArrays = {}; this.networkFetchState = null; @@ -216,6 +225,9 @@ export class RemoteSession { const state = serverState ? JSON.parse(serverState) : null; if (state) { this.stateMTimes[state.Id] = state.MTime; + if (state["vtk-object-manager-kept-alive"]) { + this.keptAliveStateIds.add(Number(state.Id)); + } } else { delete this.stateMTimes[vtkId]; } @@ -261,6 +273,9 @@ export class RemoteSession { const state = states[i]; if (state) { this.stateMTimes[state.Id] = state.MTime; + if (state["vtk-object-manager-kept-alive"]) { + this.keptAliveStateIds.add(Number(state.Id)); + } results.push(state); } else { delete this.stateMTimes[vtkId]; @@ -382,9 +397,10 @@ export class RemoteSession { serverStatus.cameras.forEach((v) => this.cameraIds.add(Number(v))); // Remove state that should be ignored - serverStatus.ignore_ids.forEach((vtkId) => - this.#native.unRegisterState(vtkId), - ); + serverStatus.ignore_ids.forEach((vtkId) => { + this.#native.unRegisterState(vtkId); + this.keptAliveStateIds.delete(Number(vtkId)); + }); // Ensure completion of all network calls await Promise.all(pendingWork.hashes); @@ -400,6 +416,22 @@ export class RemoteSession { } } + // Re-assert the ownership markers before applying states: registerState + // merges partial states into stored ones, so this restores root status + // that a local serialization stripped since the last update (see the + // keptAliveStateIds comment in the constructor). Restrict to ids in this + // update's dependency report — other render windows' roots are not + // listed here and must not be touched from this call. + const reportedIds = new Set(serverStatus.ids.map(([id]) => Number(id))); + this.keptAliveStateIds.forEach((id) => { + if (reportedIds.has(id)) { + this.#native.registerState({ + Id: id, + "vtk-object-manager-kept-alive": true, + }); + } + }); + // Bump local mtime and process states to reflect server state try { this.#native.updateObjectsFromStates(); From bab21e127d4893bc781aff5ae8f53c80bafabf3e Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 14 Aug 2026 16:50:50 -0400 Subject: [PATCH 2/2] fix(remoteSession): correct out-of-scope id in fetchBatchAsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The null-state branch deleted this.stateMTimes[vtkId], but vtkId is fetchStateAsync's parameter and was never in scope here — a null state in a batch response threw a ReferenceError and killed the whole update. The response is positionally aligned with the requested ids, so drop the ledger entry for stateIds[i] instead. --- src/remoteSession.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remoteSession.js b/src/remoteSession.js index de45dd2..06b5467 100644 --- a/src/remoteSession.js +++ b/src/remoteSession.js @@ -278,7 +278,7 @@ export class RemoteSession { } results.push(state); } else { - delete this.stateMTimes[vtkId]; + delete this.stateMTimes[stateIds[i]]; } this.incrementProgress("state"); }