Improve event stream resilience

This commit is contained in:
Bohdan Triapitsyn
2026-04-24 12:30:11 +03:00
parent f0c3d5e326
commit 9d9af7e263
8 changed files with 240 additions and 2 deletions
+38
View File
@@ -12,10 +12,20 @@ type Listener = (event: OpenChamberEvent) => void;
let eventSource: EventSource | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempt = 0;
const listeners = new Set<Listener>();
const MAX_RECONNECT_DELAY_MS = 30_000;
const HEARTBEAT_TIMEOUT_MS = 45_000;
const clearHeartbeatTimer = () => {
if (!heartbeatTimer) {
return;
}
clearTimeout(heartbeatTimer);
heartbeatTimer = null;
};
const scheduleReconnect = () => {
if (reconnectTimer || listeners.size === 0) {
@@ -30,12 +40,24 @@ const scheduleReconnect = () => {
};
const cleanupSource = () => {
clearHeartbeatTimer();
if (eventSource) {
eventSource.close();
}
eventSource = null;
};
const resetHeartbeatTimer = () => {
clearHeartbeatTimer();
if (listeners.size === 0) {
return;
}
heartbeatTimer = setTimeout(() => {
cleanupSource();
scheduleReconnect();
}, HEARTBEAT_TIMEOUT_MS);
};
const parseEnvelope = (raw: string): { type: string; properties: unknown } | null => {
if (!raw || raw.trim().length === 0) {
return null;
@@ -60,6 +82,10 @@ const dispatchFromEnvelope = (envelope: { type: string; properties: unknown }) =
return;
}
if (envelope.type === 'openchamber:heartbeat') {
return;
}
if (envelope.type !== 'openchamber:scheduled-task-ran') {
return;
}
@@ -93,10 +119,22 @@ const connect = () => {
if (typeof window === 'undefined' || listeners.size === 0) {
return;
}
if (typeof EventSource !== 'function') {
return;
}
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
return;
}
cleanupSource();
const source = new EventSource('/api/openchamber/events');
source.onopen = () => {
resetHeartbeatTimer();
};
source.onmessage = (event) => {
resetHeartbeatTimer();
const envelope = parseEnvelope(event.data);
if (!envelope) {
return;