Files
local-mcp/static/js/events.js
2026-03-27 03:58:57 +08:00

120 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}