This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm that you accept these cookies being set.

Visu localbus.js reconnect behaviour, reuse object registry, reconnect without delay
#1
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.
Reply


Forum Jump: