refactor: replace Jinja templates with static frontend pages
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Hoover — websocket.js
|
||||
*
|
||||
* WebSocket connection manager with auto-reconnect, subscribe/unsubscribe
|
||||
* per component per topic, and version-track messages.
|
||||
*
|
||||
* The _wsSubs Map stores entries keyed by renderer function so that
|
||||
* auto-refresh messages from the backend can trigger page reloads.
|
||||
*/
|
||||
|
||||
import { setSubscribeFn } from './component.js';
|
||||
|
||||
const _wsSubs = new Map();
|
||||
let _wsConn = null;
|
||||
let _wsReconnectMs = 0;
|
||||
|
||||
/**
|
||||
* Build the WebSocket URL. Supports an override via `window.__WS_URL__`
|
||||
* (useful for proxy setups). Falls back to port 9091 when the current
|
||||
* origin has no port (nginx fronting the WS on a different port).
|
||||
*/
|
||||
function _wsUrl() {
|
||||
if (window.__WS_URL__) return window.__WS_URL__;
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return proto + '//' + location.host + '/ws';
|
||||
}
|
||||
|
||||
/** Attempt a WebSocket connection. */
|
||||
function _wsConnect() {
|
||||
if (_wsConn && _wsConn.readyState <= 1) return;
|
||||
|
||||
_wsConn = new WebSocket(_wsUrl());
|
||||
|
||||
_wsConn.onopen = () => {
|
||||
_wsReconnectMs = 0;
|
||||
};
|
||||
|
||||
_wsConn.onclose = () => {
|
||||
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000);
|
||||
setTimeout(_wsConnect, _wsReconnectMs);
|
||||
};
|
||||
|
||||
_wsConn.onerror = () => {
|
||||
_wsConn.close();
|
||||
};
|
||||
|
||||
_wsConn.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = typeof ev.data === 'string' ? JSON.parse(ev.data) : ev.data;
|
||||
handleMessage(msg);
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route an incoming WS message to subscribed components.
|
||||
*
|
||||
* Expected message shapes:
|
||||
* { type: 'versions', updated: ['firewall', 'dnsmasq', …] }
|
||||
* { type: 'notify', topic: 'firewall' }
|
||||
* { type: 'status', topic: 'firewall', … }
|
||||
*
|
||||
* Components subscribed to wildcard ('*') match every topic.
|
||||
*/
|
||||
function handleMessage(msg) {
|
||||
const topics = [];
|
||||
|
||||
if (msg.type === 'versions' || msg.type === 'refresh') {
|
||||
topics.push(...(msg.updated || msg.topics || []));
|
||||
} else if (msg.type === 'notify') {
|
||||
topics.push(msg.topic);
|
||||
} else if (msg.type === 'status') {
|
||||
topics.push(msg.topic || '*');
|
||||
}
|
||||
|
||||
for (const s of _wsSubs.values()) {
|
||||
if (s.unsubscribed) continue;
|
||||
if (s.topic === '*') {
|
||||
s.loadFn(s.state);
|
||||
} else if (topics.some(t => t === s.topic || t === '*')) {
|
||||
s.loadFn(s.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe a component to WS topics.
|
||||
*
|
||||
* Called by component.js on mount. Returns an unsubscribe function
|
||||
* called by component.js on unmount.
|
||||
*
|
||||
* @param {function} componentFn – The page renderer function (used as map key)
|
||||
* @param {string} topic – Topic to listen for ('*' = all)
|
||||
* @param {function} loadFn – Function to call when topic updates
|
||||
* @param {object} state – Reactive state passed to loadFn
|
||||
* @returns {function} unsubscribe
|
||||
*/
|
||||
function subscribe(componentFn, topic, loadFn, state) {
|
||||
const entry = { componentFn, topic, loadFn, state, unsubscribed: false };
|
||||
_wsSubs.set(componentFn, entry);
|
||||
|
||||
return () => {
|
||||
entry.unsubscribed = true;
|
||||
_wsSubs.delete(componentFn);
|
||||
};
|
||||
}
|
||||
|
||||
/** Register the subscribe function with component.js and kick off connection. */
|
||||
setSubscribeFn(subscribe);
|
||||
|
||||
/** Start the WebSocket connection. */
|
||||
export function connect() {
|
||||
_wsConnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public subscribe API for direct one-off usage (e.g. from page code).
|
||||
* @param {string|string[]} topics
|
||||
* @param {function} handler
|
||||
* @returns {function} unsubscribe
|
||||
*/
|
||||
export function onMessage(topics, handler) {
|
||||
const tArray = Array.isArray(topics) ? topics : [topics];
|
||||
const fns = [];
|
||||
for (const t of tArray) {
|
||||
const entry = {
|
||||
componentFn: handler, topic: t, loadFn: handler, state: {},
|
||||
unsubscribed: false
|
||||
};
|
||||
_wsSubs.set(handler + ':' + t, entry);
|
||||
fns.push(() => { entry.unsubscribed = true; _wsSubs.delete(handler + ':' + t); });
|
||||
}
|
||||
return () => fns.forEach(f => f());
|
||||
}
|
||||
Reference in New Issue
Block a user