dashboard: SharedWorker for SSE multiplexing (closes #448)
mara on #448: "firefox disconnects bc of too many tabs. needs bg service worker". picked SharedWorker over full Service Worker: smaller change, addresses the actual problem (shared connection across tabs), no offline-cache scope creep. architecture per-tab `new EventSource('/dashboard/stream')` replaced with a SharedWorker-backed facade. one SharedWorker instance per origin holds ONE upstream EventSource and fans every server-sent event out to every connected tab via MessagePort. N hyperhive tabs now share ONE backend connection, immune to Firefox's per-tab SSE throttling under many-open-tabs pressure. wire protocol (port.postMessage): tab → worker { kind: 'subscribe', url: '/dashboard/stream' } { kind: 'unsubscribe', url: '/dashboard/stream' } worker → tab { kind: 'open', url } { kind: 'message', url, data: '<raw SSE data>' } { kind: 'error', url } subscription tracking is per (port, url). a late subscriber that joins after the upstream is already OPEN gets a synthetic 'open' event so its onStreamOpen handler still runs (triggers the snapshot re-sync that recovers events lost during the join gap). unsubscribing the last port for a URL closes the upstream EventSource so we don't leak idle streams. files - frontend/packages/dashboard/src/stream-worker.js: new — the worker. multi-URL multiplexing via Map<url, {es, ports}>. - frontend/packages/dashboard/src/common.js: new exported helper openStream(url) — returns an EventSource-shaped facade backed by the SharedWorker. graceful fallback to direct EventSource when SharedWorker is unavailable. - frontend/packages/dashboard/src/app.js: replaces the inline new EventSource('/dashboard/stream') with openStream. - frontend/packages/dashboard/src/flow.js: passes streamFactory: openStream to termCreate so the broker terminal's SSE goes through the worker too. - frontend/packages/shared/src/terminal.js: accepts an optional streamFactory(url) option. default unchanged — non-dashboard consumers (per-agent UI) keep using direct EventSource. - frontend/packages/dashboard/build.mjs: new esbuild entry for stream-worker.js → dist/static/stream-worker.js (separate bundle because SharedWorker scripts run in a different global scope and can't be inlined into app.js). scope kept tight - per-agent UI's /events/stream stays on direct EventSource. the agent UI's tab count per agent is typically 1; SharedWorker helps when you have N tabs hitting the SAME stream and the per-agent stream URLs differ. if mara wants the agent UI to share its workers too it's a separate small PR. - no offline-cache, no push notifications — those need full Service Worker; explicit non-goal here per the design Q. validation - npm run build --workspace=@hive/dashboard clean. - stream-worker.js bundle: 1.8 kb. - app.js: 154 kb → 158 kb. flow.js: 29.9 kb → 32 kb. - browser smoke test isn't possible from inside iris's container; the EventSource-shaped facade preserves the exact onmessage / onopen / onerror surface the existing IIFE consumers use.
This commit is contained in:
parent
8dc3432570
commit
4504f9ede3
6 changed files with 246 additions and 3 deletions
|
|
@ -18,6 +18,7 @@ import {
|
|||
fmtAgeSecs,
|
||||
Panel, NOTIF,
|
||||
makePathLink, appendText, appendLinkified,
|
||||
openStream,
|
||||
} from './common.js';
|
||||
|
||||
// mdNode (in common.js) reads `window.marked` for the markdown side
|
||||
|
|
@ -2069,7 +2070,14 @@ window.marked = marked;
|
|||
rebuild_queue_changed: applyRebuildQueueChanged,
|
||||
};
|
||||
(function bindDashboardStream() {
|
||||
const es = new EventSource('/dashboard/stream');
|
||||
// #448: route the EventSource through a SharedWorker so all open
|
||||
// hyperhive tabs share ONE backend SSE connection. Survives
|
||||
// Firefox's per-tab connection throttling under many-open-tabs
|
||||
// pressure (the actual mara symptom). `openStream` returns an
|
||||
// EventSource-shaped facade so the rest of this IIFE is unchanged;
|
||||
// graceful fallback to direct `new EventSource` when SharedWorker
|
||||
// isn't supported.
|
||||
const es = openStream('/dashboard/stream');
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try { ev = JSON.parse(e.data); } catch { return; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue