feat(notifications): replace polling with WebSocket push

Eliminates the 5-second setInterval polling loop for notifications in
EditorLayout. A persistent /ws/notifications WebSocket connection is
opened on mount; the backend pushes each alert the instant dispatchAlert
fires, with 5s auto-reconnect on close.

Key changes:
- NotificationService: injectable broadcaster callback (setBroadcaster)
- DatabaseService.addNotificationHistory: returns full NotificationHistory
  record (with id/is_read) instead of void
- index.ts: notificationSubscribers Set + /ws/notifications upgrade handler
  (JWT-verified, placed before remote proxy path)
- EditorLayout: polling removed, WS connect/reconnect replaces it
This commit is contained in:
SaelixCode
2026-03-20 20:47:51 -04:00
parent 23a22598ab
commit a5ac3e4981
5 changed files with 91 additions and 8 deletions
+40 -3
View File
@@ -162,11 +162,48 @@ export default function EditorLayout() {
}
};
// Notification polling - independent of active node, runs once on mount
// Notification WS push — load history once on mount, then receive live updates
useEffect(() => {
fetchNotifications();
const notificationInterval = setInterval(fetchNotifications, 5000);
return () => clearInterval(notificationInterval);
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsBase = `${wsProtocol}//${window.location.host}`;
let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let isMounted = true;
const connect = () => {
ws = new WebSocket(`${wsBase}/ws/notifications`);
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'notification' && msg.payload) {
setNotifications(prev => [msg.payload, ...prev]);
}
} catch (e) {
console.error('[WS notifications] parse error', e);
}
};
ws.onclose = () => {
if (isMounted) {
reconnectTimer = setTimeout(connect, 5000);
}
};
ws.onerror = () => {
ws?.close();
};
};
connect();
return () => {
isMounted = false;
if (reconnectTimer) clearTimeout(reconnectTimer);
ws?.close();
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Re-fetch stacks whenever the active node changes (or becomes available on mount).