init
This commit is contained in:
119
static/js/events.js
Normal file
119
static/js/events.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* static/js/events.js
|
||||
* Server-Sent Events client – connects to /api/events and dispatches
|
||||
* updates into the central state store.
|
||||
* Uses full item payloads embedded in events to avoid extra re-fetch round-trips.
|
||||
*/
|
||||
|
||||
import { state } from './state.js';
|
||||
import { api } from './api.js';
|
||||
|
||||
let _es = null;
|
||||
let _reconnectTimer = null;
|
||||
let _reconnecting = false;
|
||||
const RECONNECT_DELAY_MS = 3000;
|
||||
|
||||
export function connectSSE() {
|
||||
if (_es) return;
|
||||
_connect();
|
||||
}
|
||||
|
||||
function _connect() {
|
||||
_es = new EventSource('/api/events');
|
||||
|
||||
_es.onopen = () => {
|
||||
console.debug('[SSE] connected');
|
||||
if (_reconnecting) {
|
||||
_reconnecting = false;
|
||||
state.set('sseReconnecting', false);
|
||||
// Full refresh after reconnect to catch anything we missed
|
||||
_fullRefresh();
|
||||
}
|
||||
state.set('serverOnline', true);
|
||||
};
|
||||
|
||||
_es.onmessage = (e) => {
|
||||
try {
|
||||
const event = JSON.parse(e.data);
|
||||
_handleEvent(event);
|
||||
} catch (err) {
|
||||
console.warn('[SSE] parse error', err);
|
||||
}
|
||||
};
|
||||
|
||||
_es.onerror = () => {
|
||||
console.warn('[SSE] connection lost – reconnecting in', RECONNECT_DELAY_MS, 'ms');
|
||||
state.set('serverOnline', false);
|
||||
_reconnecting = true;
|
||||
state.set('sseReconnecting', true);
|
||||
_es.close();
|
||||
_es = null;
|
||||
clearTimeout(_reconnectTimer);
|
||||
_reconnectTimer = setTimeout(_connect, RECONNECT_DELAY_MS);
|
||||
};
|
||||
}
|
||||
|
||||
async function _fullRefresh() {
|
||||
try {
|
||||
const [instructions, status, config] = await Promise.all([
|
||||
api.listInstructions('all'),
|
||||
api.status(),
|
||||
api.getConfig(),
|
||||
]);
|
||||
state.set('instructions', instructions.items);
|
||||
state.set('status', status);
|
||||
state.set('config', config);
|
||||
} catch (e) {
|
||||
console.error('[SSE] full refresh failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function _applyInstructionPatch(item) {
|
||||
const current = state.get('instructions') || [];
|
||||
const idx = current.findIndex(i => i.id === item.id);
|
||||
if (idx === -1) {
|
||||
// New item – append and re-sort by position
|
||||
const next = [...current, item].sort((a, b) => a.position - b.position);
|
||||
state.set('instructions', next);
|
||||
} else {
|
||||
// Replace in-place, preserving order
|
||||
const next = [...current];
|
||||
next[idx] = item;
|
||||
state.set('instructions', next.sort((a, b) => a.position - b.position));
|
||||
}
|
||||
}
|
||||
|
||||
function _handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case 'instruction.created':
|
||||
case 'instruction.updated':
|
||||
case 'instruction.consumed': {
|
||||
if (event.data?.item) {
|
||||
_applyInstructionPatch(event.data.item);
|
||||
} else {
|
||||
// Fallback: full refresh
|
||||
api.listInstructions('all').then(d => state.set('instructions', d.items)).catch(console.error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'instruction.deleted': {
|
||||
const id = event.data?.id;
|
||||
if (id) {
|
||||
const next = (state.get('instructions') || []).filter(i => i.id !== id);
|
||||
state.set('instructions', next);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'status.changed': {
|
||||
api.status().then(s => state.set('status', s)).catch(console.error);
|
||||
break;
|
||||
}
|
||||
case 'config.updated': {
|
||||
api.getConfig().then(c => state.set('config', c)).catch(console.error);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.debug('[SSE] unknown event type', event.type);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user