frontend/dashboard: heartbeat + watchdog so a dead SharedWorker self-heals (#515)
Firefox kills "idle" SharedWorkers under memory pressure with no native signal to the client. The page's port silently becomes a no-op and events stop flowing — observable symptom: mara's "dashboard never refreshes; F5 fixes it" (because F5 creates a fresh page that creates a fresh worker). The worker now pings every connected port every 30s. The client tracks last-activity-from-worker on every message arrival (incl. pings, since those carry no URL — bumped before the URL filter in the route handler). A visibility-gated watchdog polls every 15s; if the page is visible AND has active subs AND hasn't heard from the worker in >90s, it presumes the worker dead, logs a console warning, and re-subscribes on a fresh port. Three pings missed before we act, so a normal tab-throttle blip doesn't false-positive. The fresh-port re-subscribe re-uses the bfcache-restore code path (same shape: drop stale listeners, getSharedPort → new SharedWorker, re-attach each route + repost subscribe). Recovery is per-tab — when one tab's watchdog fires and brings up a new worker, other tabs that share the named worker pick it up on their own watchdog cycle. Falls back gracefully on environments without SharedWorker (the existing direct-EventSource path is untouched) and is invisible on the healthy path — pings are 30s apart, no UI surface.
This commit is contained in:
parent
c786b9ec37
commit
b9df538940
2 changed files with 100 additions and 1 deletions
|
|
@ -29,6 +29,18 @@
|
|||
// re-sync after a reconnect gap.
|
||||
// { kind: 'message', url: '...', data: '<raw SSE data string>' }
|
||||
// { 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
|
||||
|
|
@ -37,6 +49,19 @@
|
|||
// 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 — cleanup happens lazily on next subscribe */ }
|
||||
}
|
||||
}, PING_INTERVAL_MS);
|
||||
|
||||
function getOrCreateStream(url) {
|
||||
let entry = streams.get(url);
|
||||
|
|
@ -77,6 +102,7 @@ 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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue