diff --git a/frontend/packages/dashboard/src/common.js b/frontend/packages/dashboard/src/common.js index f18b406d..2bc430d8 100644 --- a/frontend/packages/dashboard/src/common.js +++ b/frontend/packages/dashboard/src/common.js @@ -99,71 +99,6 @@ function getSharedPort() { return _sharedPort; } -// #515: detect when the SharedWorker has been killed. Firefox aggressively -// reclaims "idle" SharedWorkers under memory pressure (or just on tab -// lifecycle quirks we don't fully understand), and there's no native -// signal to the client when that happens — `postMessage` on a dead -// port silently no-ops, and the page just stops receiving events. The -// observable symptom is "dashboard never refreshes; F5 fixes it" (a -// fresh page creates a fresh worker), which is mara's report on #515. -// -// Pipeline: -// - The worker pings every connected port every WORKER_PING_INTERVAL_MS. -// - `route` (every message arrival, including pings) bumps -// `_lastWorkerActivityAt`. -// - `startWorkerWatchdog` polls every WORKER_WATCHDOG_INTERVAL_MS. -// If the page is visible AND we have active subs AND we haven't -// heard from the worker in > WORKER_DEAD_THRESHOLD_MS, we presume -// the worker is dead, log a warning, and re-subscribe on a fresh -// port. -// -// Numbers picked so a real Firefox tab-suspend / unsuspend cycle (which -// can pause the watchdog itself) doesn't false-positive: 90s without a -// 30s ping means at least three pings missed. Visibility-gated so a -// backgrounded tab — where Firefox throttles setInterval to 1Hz min and -// our healthcheck wouldn't trigger reliably anyway — doesn't try to -// reconnect uselessly. -const WORKER_DEAD_THRESHOLD_MS = 90_000; -const WORKER_WATCHDOG_INTERVAL_MS = 15_000; -let _lastWorkerActivityAt = 0; -function noteWorkerActivity() { _lastWorkerActivityAt = Date.now(); } -let _watchdogTimer = null; -function startWorkerWatchdog() { - if (_watchdogTimer != null) return; - _watchdogTimer = setInterval(() => { - if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return; - if (!_activeSubs.size) return; - if (!_sharedPort) return; - const sinceLast = Date.now() - _lastWorkerActivityAt; - if (sinceLast < WORKER_DEAD_THRESHOLD_MS) return; - console.warn( - 'hyperhive-stream worker silent for ' + Math.round(sinceLast / 1000) - + 's, presumed dead — re-subscribing on a fresh port'); - rebindOnFreshPort(); - }, WORKER_WATCHDOG_INTERVAL_MS); -} -function rebindOnFreshPort() { - // Drop any listeners attached to the dead port (best-effort; calls - // on a dead port throw, which we swallow). The active-subs registry - // is the source of truth for what we need to re-attach. - if (_sharedPort) { - for (const sub of _activeSubs.values()) { - try { _sharedPort.removeEventListener('message', sub.route); } catch {} - } - } - _sharedPort = null; - const port = getSharedPort(); - if (!port) return; // SharedWorker unsupported / unavailable — nothing to do - for (const [url, sub] of _activeSubs) { - sub.target.readyState = 0; // CONNECTING — worker will (re-)fire 'open' - port.addEventListener('message', sub.route); - try { port.postMessage({ kind: 'subscribe', url }); } catch {} - } - // Reset the activity clock so the watchdog gives the fresh worker - // a full window to settle before re-triggering. - noteWorkerActivity(); -} - // Registry of live subscriptions on this page. Keyed by url so a // second openStream call for the same URL (would only happen on a // hypothetical multi-consumer page) attaches to the existing route @@ -187,7 +122,6 @@ let _lifecycleBound = false; function bindLifecycleOnce() { if (_lifecycleBound) return; _lifecycleBound = true; - startWorkerWatchdog(); window.addEventListener('pagehide', () => { if (!_sharedPort) return; for (const url of _activeSubs.keys()) { @@ -245,12 +179,8 @@ export function openStream(url) { }, }; const route = (e) => { - // #515: any message from the worker is proof of life — note it - // before the URL filter, since heartbeat pings carry no URL. - noteWorkerActivity(); const m = e.data; - if (!m || m.kind === 'ping') return; - if (m.url !== url) return; + if (!m || m.url !== url) return; if (m.kind === 'open') { target.readyState = 1; // OPEN if (target.onopen) { @@ -272,9 +202,6 @@ export function openStream(url) { _activeSubs.set(url, { target, route }); port.addEventListener('message', route); port.postMessage({ kind: 'subscribe', url }); - // #515: seed the activity clock so the watchdog has a baseline; it - // would otherwise compare against 0 (epoch) and trigger immediately. - noteWorkerActivity(); return target; } diff --git a/frontend/packages/dashboard/src/stream-worker.js b/frontend/packages/dashboard/src/stream-worker.js index 166cc7eb..3d7af676 100644 --- a/frontend/packages/dashboard/src/stream-worker.js +++ b/frontend/packages/dashboard/src/stream-worker.js @@ -29,18 +29,6 @@ // re-sync after a reconnect gap. // { kind: 'message', url: '...', data: '' } // { kind: 'error', url: '...' } relayed from EventSource.onerror. -// { kind: 'ping' } #515: heartbeat — fired every -// PING_INTERVAL_MS to every -// connected port. The client's -// watchdog uses these as -// proof-of-life; silence past -// ~3× the interval triggers a -// re-subscribe on a fresh port -// (recovers from Firefox killing -// the SharedWorker out from -// under us, which it does under -// memory pressure with no native -// signal to the client). // // Subscriptions are tracked per (port, url): a single port can // subscribe to multiple URLs (today only one is in use but the shape @@ -49,19 +37,6 @@ // don't keep idle streams open. const streams = new Map(); -// All currently-connected ports. Used by the heartbeat tick to fan -// pings out across every tab regardless of which URLs each port is -// subscribed to. Stays disjoint from per-stream `entry.ports` (which -// is URL-scoped); a port may be in `allPorts` without any active -// subscription (e.g. between a tab loading and its first subscribe). -const allPorts = new Set(); -const PING_INTERVAL_MS = 30_000; -setInterval(() => { - for (const port of allPorts) { - try { port.postMessage({ kind: 'ping' }); } - catch { /* port dead — left in the Set; see onconnect's closing comment */ } - } -}, PING_INTERVAL_MS); function getOrCreateStream(url) { let entry = streams.get(url); @@ -102,7 +77,6 @@ function unsubscribe(port, url) { self.onconnect = (connectEvent) => { const port = connectEvent.ports[0]; - allPorts.add(port); const subscribedUrls = new Set(); port.onmessage = (e) => { const msg = e.data;