feat: replace client polling with Server-Sent Events (SSE)
Server: - poller.js: add pollSubscribers Set with onPollComplete/offPollComplete; notify all SSE callbacks immediately after every successful poll - dashboard.js: add GET /api/dashboard/stream endpoint (text/event-stream) - requireAuth enforced via cookie (no CSRF needed — GET is a safe method) - X-Accel-Buffering: no for nginx proxy compatibility - 25s heartbeat comments to survive proxy idle timeouts - initial payload sent immediately on connect - cleanup on req.close: deregister callback, stop heartbeat, remove client - active client tracking updated: type='sse', connectedAt, no refreshRateMs Frontend: - app.js: replace setInterval/fetchUserDownloads with EventSource - startSSE() opens /api/dashboard/stream; stopSSE() closes it - first incoming message hides loading spinner - showAll toggle re-opens stream with ?showAll=true param - logout calls stopSSE() before POST /api/auth/logout - status panel: fixed 5s refresh, shows SSE clients + connect duration - statusRefreshHandle now always 5s, not tied to old refresh-rate selector - index.html: remove now-unused refresh-rate <select> element Docs: - ARCHITECTURE.md §4.3: update poller description - ARCHITECTURE.md §5: rename to SSE Stream (§5.2) + Download Matching (§5.3) - ARCHITECTURE.md §7: update active client tracking description - ARCHITECTURE.md §9: add /stream endpoint, update /status clients schema - ARCHITECTURE.md §10: update key functions table; replace Auto-Refresh section with Live Push via SSE - class-server.puml: add /stream to dashboard routes; update ClientInfo - component.puml: annotate dashboard with SSE note; update label
This commit is contained in:
@@ -16,6 +16,12 @@ const POLLING_ENABLED = POLL_INTERVAL > 0;
|
||||
let polling = false;
|
||||
let lastPollTimings = null;
|
||||
|
||||
// SSE subscribers: Set of () => void callbacks, each registered by an open /stream connection
|
||||
const pollSubscribers = new Set();
|
||||
|
||||
function onPollComplete(cb) { pollSubscribers.add(cb); }
|
||||
function offPollComplete(cb) { pollSubscribers.delete(cb); }
|
||||
|
||||
// Timed fetch helper: runs a fetch and records how long it took
|
||||
async function timed(label, fn) {
|
||||
const t0 = Date.now();
|
||||
@@ -184,6 +190,11 @@ async function pollAllServices() {
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(`[Poller] Poll complete in ${elapsed}ms`);
|
||||
|
||||
// Notify all SSE stream connections so they push fresh data immediately
|
||||
for (const cb of pollSubscribers) {
|
||||
try { cb(); } catch { /* subscriber already disconnected */ }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Poller] Poll error:`, err.message);
|
||||
} finally {
|
||||
@@ -216,4 +227,4 @@ function getLastPollTimings() {
|
||||
return lastPollTimings;
|
||||
}
|
||||
|
||||
module.exports = { startPoller, stopPoller, pollAllServices, getLastPollTimings, POLL_INTERVAL, POLLING_ENABLED };
|
||||
module.exports = { startPoller, stopPoller, pollAllServices, getLastPollTimings, onPollComplete, offPollComplete, POLL_INTERVAL, POLLING_ENABLED };
|
||||
|
||||
Reference in New Issue
Block a user