![]() |
|
Visu localbus.js reconnect behaviour, reuse object registry, reconnect without delay - Printable Version +- LogicMachine Forum (https://forum.logicmachine.net) +-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1) +--- Forum: Visu (https://forum.logicmachine.net/forumdisplay.php?fid=24) +--- Thread: Visu localbus.js reconnect behaviour, reuse object registry, reconnect without delay (/showthread.php?tid=6543) |
Visu localbus.js reconnect behaviour, reuse object registry, reconnect without delay - savaskorkmaz - 05.09.2026 Setup: LM firmware 2025, Visu, apps/js/localbus.js (v=20251031), Android kiosk panels (WebView) plus desktop Chrome for comparison. PROBLEM On some corporate networks a middlebox (IPS / application control / proxy / NAC policy) resets established TCP sessions a few seconds after they open. Visu's live-data WebSocket is hit by this; the classic HTTP-polling visualization is not. With the current client logic the visible effect and the server load are much larger than necessary: - on every WebSocket close, localbus.js waits 1 s, - then re-downloads the FULL object registry via POST /apps/localbus.lp (about 285 KB in our project, 2832 objects), - and only then reopens the WebSocket. So each reset costs the user ~3 s of "reconnecting" spinner with frozen values, and costs the LM a full registry regeneration. Field symptom that started this: WebSocket killed 1-1.5 s after open, floor plan stays rendered, spinner flashes every ~3 s, values freeze. A laptop on the same LAN (not subject to the policy) worked fine. WHAT WE MEASURED Lab: a transparent TCP relay between client and LM that kills every session after N seconds. 1) Current client, Android panel, kill after 5 s WS lifetime 5.0 s, visible gap 2.9-3.0 s per cycle, ~37 % of the time disconnected, server work per cycle: registry POST 285 KB + WS handshake. 2) Current client, Android panel, kill after 2 s The registry POST never completes, page stays blank with a permanent spinner. 3) Patched client (see below), desktop Chrome, kill after 5 s WS lifetime 5.06 s, visible gap 22-37 ms, < 1 % disconnected, server work: WS handshake only (~4 KB). 4) Patched client, desktop Chrome, kill after 1.5 s (the field case) WS lifetime 1.55 s, gap avg 25 ms / max 29 ms, 1.5 % disconnected, live values keep flowing. 5) Control, kill only after 15 s idle No disconnects at all: the client "ping" every 10 s keeps the socket alive, so idle timeouts >= 11 s are not the cause. Also verified: the server ACCEPTS a new WebSocket in the same session without a preceding registry POST (opened in 21 ms, live messages received). OBSERVED CLIENT LOGIC (localbus.js) onclose -> setstate(false) -> onerror() -> setTimeout(init, 1000) -> init(): POST /apps/localbus.lp (full registry) -> register() -> start(): new WebSocket. No backoff. The registry is re-fetched on every reconnect although objectstore already holds it. REQUESTED CHANGE (small) 1. On WebSocket close, if objectstore is already populated, reopen the WebSocket directly (start()), without the 1 s delay and without re-downloading the registry. 2. Keep the full init() path only when the registry is empty or the previous session was rejected (lived < ~300 ms), so an expired session still recovers. 3. Add a simple cap/backoff (e.g. max 60 fast reconnects per minute, then 1 s delay) to protect the server under persistent resets. 4. Optional: expose reconnect counters (count, last lifetime, last gap) on the localbus object for diagnostics. WHY IT MATTERS FOR THE SERVER TOO Under a 1.5 s reset policy with 20 panels the current logic makes the LM regenerate about 1.4 MB/s of registry JSON continuously; the patched logic reduces that to WebSocket handshakes (about 60 KB/s). On healthy networks the change has zero effect. REFERENCE IMPLEMENTATION (tested as Visu Custom JS, wraps start/onerror only) (function () { var CFG = { FAST_LIMIT_PER_MIN: 60, SLOW_WAIT_MS: 1000, REJECT_LIFE_MS: 300, WAIT_FOR_LOCALBUS_MS: 30000 }; if (window.__sonaLb) return; window.__sonaLb = { version: 'V1', active: false, fast: 0, slow: 0, full: 0, lastLifeMs: null, lastGapMs: null, sessions: 0 }; function install() { var lb = window.localbus; if (!lb || typeof lb.start !== 'function' || typeof lb.onerror !== 'function' || typeof lb.init !== 'function') { console.warn('[sona-lb] unexpected localbus structure, patch not applied'); return; } var S = window.__sonaLb, hist = [], startT = 0, closeT = 0; var origStart = lb.start, origOnError = lb.onerror; lb.start = function () { startT = Date.now(); S.sessions++; if (closeT) { S.lastGapMs = startT - closeT; closeT = 0; } return origStart.apply(this, arguments); }; lb.onerror = function () { var now = Date.now(); clearTimeout(this.inittimer); this.inittimer = null; closeT = now; S.lastLifeMs = startT ? now - startT : null; var haveRegistry = this.objectstore && Object.keys(this.objectstore).length > 0; var rejected = S.lastLifeMs !== null && S.lastLifeMs < CFG.REJECT_LIFE_MS; if (!haveRegistry || rejected) { S.full++; return origOnError.call(this); } // default path: 1 s + POST + WS hist = hist.filter(function (t) { return now - t < 60000; }); var self = this; if (hist.length >= CFG.FAST_LIMIT_PER_MIN) { // cap: wait, but no POST S.slow++; this.inittimer = setTimeout(function () { self.inittimer = null; self.start(); }, CFG.SLOW_WAIT_MS); return; } hist.push(now); S.fast++; this.start(); // fast path: new WebSocket now }; S.active = true; console.info('[sona-lb] fast reconnect active (' + S.version + ')'); } if (window.localbus) { install(); return; } var t0 = Date.now(); var timer = setInterval(function () { if (window.localbus) { clearInterval(timer); install(); } else if (Date.now() - t0 > CFG.WAIT_FOR_LOCALBUS_MS) { clearInterval(timer); console.warn('[sona-lb] localbus not found'); } }, 200); })(); We can share the raw measurement logs and the relay tool used to reproduce this. |