Reconnect SSE connection if we miss heartbeats for 30 seconds
Some checks failed
/ golint (push) Failing after 10s

Issue: #11
This commit is contained in:
Eli Ribble 2026-05-21 15:43:56 +00:00
parent 03d40774cb
commit 614f4d274e
No known key found for this signature in database
2 changed files with 52 additions and 4 deletions

View file

@ -1,3 +1,5 @@
import { log } from "@/log";
// Define types for the SSE data structure // Define types for the SSE data structure
export interface SSEMessageBase { export interface SSEMessageBase {
type: string; type: string;
@ -35,8 +37,10 @@ declare global {
} }
*/ */
const HEARTBEAT_TIMEOUT_MILLISECONDS = 30000;
export const SSEManager: SSEManagerType = (function (): SSEManagerType { export const SSEManager: SSEManagerType = (function (): SSEManagerType {
let connectionPromise: Promise<EventSource> | null = null; let connectionPromise: Promise<EventSource> | null = null;
let heartbeatTimeout: number | null = null;
let isConnected: boolean = false; let isConnected: boolean = false;
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
let serverUrl: string = ""; let serverUrl: string = "";
@ -55,17 +59,18 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
eventSource.onopen = function (): void { eventSource.onopen = function (): void {
isConnected = true; isConnected = true;
heartbeatTimeoutSet();
eventSource!.addEventListener("message", (message: MessageEvent) => { eventSource!.addEventListener("message", (message: MessageEvent) => {
const data: SSEMessageBase = JSON.parse(message.data); const data: SSEMessageBase = JSON.parse(message.data);
handleMessage(data); handleMessage(data);
}); });
console.log("SSE connected"); log.info("SSE connected");
resolve(eventSource!); resolve(eventSource!);
}; };
eventSource.onerror = function (err: Event): void { eventSource.onerror = function (err: Event): void {
console.error("SSE error:", err); log.error("SSE error:", err);
isConnected = false; isConnected = false;
// Close old connection // Close old connection
@ -75,7 +80,7 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
// Reconnect after delay // Reconnect after delay
setTimeout(() => { setTimeout(() => {
console.log("SSE reconnecting"); log.info("SSE reconnecting");
connectionPromise = null; connectionPromise = null;
connect(url); connect(url);
}, 5000); }, 5000);
@ -93,14 +98,16 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
if (eventSource) { if (eventSource) {
eventSource.close(); eventSource.close();
eventSource = null; eventSource = null;
heartbeatTimeoutClear();
isConnected = false; isConnected = false;
connectionPromise = null; connectionPromise = null;
console.log("SSE disconnected"); log.info("SSE disconnected");
} }
} }
function handleMessage(msg: SSEMessageBase) { function handleMessage(msg: SSEMessageBase) {
if (msg.type == "heartbeat") { if (msg.type == "heartbeat") {
heartbeatTimeoutReset();
return; return;
} else if (msg.type == "status") { } else if (msg.type == "status") {
subscribersStatus.forEach((handler: SSEHandlerStatus, _: string) => { subscribersStatus.forEach((handler: SSEHandlerStatus, _: string) => {
@ -112,6 +119,24 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
}); });
} }
} }
function heartbeatTimeoutClear() {
if (heartbeatTimeout) {
clearTimeout(heartbeatTimeout);
}
heartbeatTimeout = 0;
}
function heartbeatTimeoutReset() {
heartbeatTimeoutClear();
heartbeatTimeoutSet();
}
function heartbeatTimeoutSet() {
if (heartbeatTimeout) {
throw new Error("can't set heartbeat timeout - already set");
}
heartbeatTimeout = setTimeout(function () {
reconnect(0);
}, HEARTBEAT_TIMEOUT_MILLISECONDS);
}
function ready(callback: (eventSource: EventSource) => void): void { function ready(callback: (eventSource: EventSource) => void): void {
if (connectionPromise) { if (connectionPromise) {
@ -128,6 +153,7 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType {
} }
function reconnect(delay: number) { function reconnect(delay: number) {
log.info("Reconnecting SSEManager to", serverUrl);
disconnect(); disconnect();
setTimeout(() => { setTimeout(() => {
connect(serverUrl); connect(serverUrl);

22
ts/log.ts Normal file
View file

@ -0,0 +1,22 @@
// log.ts
const pageLoadTime = performance.now();
function getTimestamp(): string {
const elapsed = performance.now() - pageLoadTime;
const hours = Math.floor(elapsed / 3600000);
const minutes = Math.floor((elapsed % 3600000) / 60000);
const seconds = Math.floor((elapsed % 60000) / 1000);
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
export const log = {
info(...args: any[]): void {
console.log(`[${getTimestamp()}]`, ...args);
},
error(...args: any[]): void {
console.error(`[${getTimestamp()}]`, ...args);
},
};