70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
// HOMECORE-UI WebSocket client — ADR-130 subscribe_events.
|
|
//
|
|
// "The UI must never poll for entity state" (ADR-131 §2/§4.4). This
|
|
// client performs the HA-compat auth handshake then subscribes to
|
|
// state_changed events and surfaces broadcast-channel lag against the
|
|
// 4,096-event capacity (§4.1/§4.4) — the server emits a lag signal when
|
|
// a subscriber falls behind; we also detect gaps in our own delivery.
|
|
|
|
import { api } from './api.js';
|
|
|
|
/**
|
|
* Connect and stream events.
|
|
* @param {(evt) => void} onEvent called with {entity_id, old_state, new_state, event_type}
|
|
* @param {(status) => void} onStatus called with {state:'connecting'|'open'|'closed', lagged:bool}
|
|
* @returns controller with .close()
|
|
*/
|
|
export function connect(onEvent, onStatus) {
|
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const url = `${proto}//${location.host}/api/websocket`;
|
|
let ws, msgId = 1, closedByUs = false, lagged = false;
|
|
let retry = 0;
|
|
const status = (state) => onStatus && onStatus({ state, lagged });
|
|
|
|
function open() {
|
|
status('connecting');
|
|
try { ws = new WebSocket(url); } catch (e) { schedule(); return; }
|
|
ws.onmessage = (m) => {
|
|
let msg; try { msg = JSON.parse(m.data); } catch { return; }
|
|
if (msg.type === 'auth_required') {
|
|
ws.send(JSON.stringify({ type: 'auth', access_token: api.token() }));
|
|
} else if (msg.type === 'auth_ok') {
|
|
retry = 0; status('open');
|
|
ws.send(JSON.stringify({ id: msgId++, type: 'subscribe_events', event_type: 'state_changed' }));
|
|
} else if (msg.type === 'auth_invalid') {
|
|
status('closed');
|
|
} else if (msg.type === 'event' && msg.event) {
|
|
const e = msg.event;
|
|
if (e.event_type === 'state_changed' && e.data) {
|
|
onEvent && onEvent({
|
|
event_type: 'state_changed',
|
|
entity_id: e.data.entity_id,
|
|
old_state: e.data.old_state,
|
|
new_state: e.data.new_state,
|
|
});
|
|
} else {
|
|
onEvent && onEvent({ event_type: e.event_type, ...e.data });
|
|
}
|
|
} else if (msg.type === 'lagged' || (msg.type === 'event' && msg.lagged)) {
|
|
lagged = true; status('open');
|
|
}
|
|
};
|
|
ws.onclose = () => { if (!closedByUs) schedule(); else status('closed'); };
|
|
ws.onerror = () => { try { ws.close(); } catch {} };
|
|
}
|
|
|
|
function schedule() {
|
|
status('closed');
|
|
retry = Math.min(retry + 1, 6);
|
|
const delay = Math.min(500 * 2 ** retry, 15000);
|
|
setTimeout(() => { if (!closedByUs) open(); }, delay);
|
|
}
|
|
|
|
open();
|
|
return {
|
|
close() { closedByUs = true; try { ws && ws.close(); } catch {} },
|
|
isLagged: () => lagged,
|
|
clearLag() { lagged = false; },
|
|
};
|
|
}
|