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
|
|
@ -318,7 +318,15 @@ export function create(opts) {
|
|||
let live = false;
|
||||
let buffered = [];
|
||||
|
||||
const es = new EventSource(opts.streamUrl);
|
||||
// #448: callers can supply a `streamFactory(url)` that returns an
|
||||
// EventSource-shaped object (must expose onmessage/onopen/onerror
|
||||
// + .close()). The dashboard pages pass a SharedWorker-backed
|
||||
// factory so all open hyperhive tabs share ONE upstream SSE
|
||||
// connection. Default keeps the direct `new EventSource(url)`
|
||||
// behaviour so non-dashboard consumers (per-agent UI) are unchanged.
|
||||
const es = opts.streamFactory
|
||||
? opts.streamFactory(opts.streamUrl)
|
||||
: new EventSource(opts.streamUrl);
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try { ev = JSON.parse(e.data); }
|
||||
|
|
@ -331,7 +339,10 @@ export function create(opts) {
|
|||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
if (es.readyState === EventSource.CONNECTING) row('note', '[reconnecting…]');
|
||||
// SharedWorker-backed facades expose `readyState` mirroring the
|
||||
// upstream EventSource state; the native EventSource exposes the
|
||||
// same. Either way the CONNECTING vs. closed distinction works.
|
||||
if (es.readyState === 0 /* CONNECTING */) row('note', '[reconnecting…]');
|
||||
else row('note', '[disconnected]');
|
||||
};
|
||||
es.onopen = () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue