fix(dashboard): reduce SSE keepalive churn

This commit is contained in:
gsxdsm
2026-06-23 10:42:36 -07:00
parent fe536b2af8
commit fade815c85
6 changed files with 47 additions and 9 deletions

View File

@@ -2,4 +2,4 @@
"@runfusion/fusion": patch
---
Dispose completed spawned child agent sessions so execution memory is released promptly after `fn_spawn_agent` children finish, keep artifact registry listing metadata-only so large inline artifacts are not loaded during agent execution, and bound structured tool-result log previews before serialization.
Dispose completed spawned child agent sessions so execution memory is released promptly after `fn_spawn_agent` children finish, keep artifact registry listing metadata-only so large inline artifacts are not loaded during agent execution, bound structured tool-result log previews before serialization, and reduce dashboard SSE keepalive churn.

View File

@@ -182,6 +182,36 @@ describe("sse-bus", () => {
expect(MockEventSource.instances.length).toBe(countBeforeTimers);
});
it("does not storm keepalive control requests for active local event streams", () => {
vi.useFakeTimers();
const originalFetch = window.fetch;
const fetchMock = vi.fn(() => Promise.resolve(new Response(null, { status: 204 })));
Object.defineProperty(window, "fetch", {
configurable: true,
writable: true,
value: fetchMock,
});
try {
const unsub = subscribeSse("/api/events", {});
expect(fetchMock).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(29_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
unsub();
} finally {
Object.defineProperty(window, "fetch", {
configurable: true,
writable: true,
value: originalFetch,
});
vi.useRealTimers();
}
});
it("reopens subscribed channel on pageshow even when event.persisted is false", () => {
subscribeSse("/api/events?projectId=p1", {});
expect(MockEventSource.instances).toHaveLength(1);

View File

@@ -16,8 +16,12 @@ type OpenListener = () => void;
const HEARTBEAT_TIMEOUT_MS = 45_000;
const RECONNECT_DELAY_MS = 3_000;
const CLIENT_KEEPALIVE_INTERVAL_MS = 2_000;
const CLIENT_KEEPALIVE_TIMEOUT_MS = 1_500;
/*
* FNXC:DashboardSSE 2026-06-23-15:08:
* Dashboard SSE keepalive exists only to let the server reap abandoned browser streams. It must not create a visible storm of regular HTTP connections when the engine is off, so keep the liveness probe infrequent and let the server stale window absorb brief tab/network stalls.
*/
const CLIENT_KEEPALIVE_INTERVAL_MS = 30_000;
const CLIENT_KEEPALIVE_TIMEOUT_MS = 5_000;
const VISIBILITY_REOPEN_DEDUPE_MS = 1_000;
const CLIENT_ID_STORAGE_KEY = "fusion:sse-client-id";

View File

@@ -338,7 +338,7 @@ describe("createSSE client cleanup", () => {
expect(getActiveSSEConnections()).toBe(baseline + 1);
vi.advanceTimersByTime(4_999);
vi.advanceTimersByTime(74_999);
expect(connection.res.end).not.toHaveBeenCalled();
expect(getActiveSSEConnections()).toBe(baseline + 1);
@@ -353,14 +353,14 @@ describe("createSSE client cleanup", () => {
const baseline = getActiveSSEConnections();
const connection = openSseConnection("client-five");
vi.advanceTimersByTime(4_000);
vi.advanceTimersByTime(30_000);
expect(markSSEClientAlive("client-five")).toBe(1);
vi.advanceTimersByTime(4_000);
vi.advanceTimersByTime(74_999);
expect(connection.res.end).not.toHaveBeenCalled();
expect(getActiveSSEConnections()).toBe(baseline + 1);
vi.advanceTimersByTime(1_000);
vi.advanceTimersByTime(1);
expect(connection.res.end).toHaveBeenCalledTimes(1);
expect(getActiveSSEConnections()).toBe(baseline);
});

View File

@@ -963,7 +963,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
// attach to the same EventEmitter instance that the engine writes to,
// rather than a separate store created by getOrCreateProjectStore.
let scopedStore: TaskStore;
let agentStore;
let agentStore: AgentStore | undefined;
let messageStore: MessageStore | undefined;
let automationStore: AutomationStore | undefined;
let scopedChatStore = chatStore;

View File

@@ -19,7 +19,11 @@ let highWaterMark = 0;
let nextConnectionId = 1;
const SSE_CLIENT_ID_MAX_LENGTH = 128;
const SSE_CLIENT_STALE_MS = 5_000;
/*
* FNXC:DashboardSSE 2026-06-23-15:08:
* Client-side keepalive probes are intentionally infrequent to avoid a dashboard-only HTTP connection storm. Keep the server stale timer comfortably above that cadence so healthy streams are not reaped between probes while abandoned streams still self-clean.
*/
const SSE_CLIENT_STALE_MS = 75_000;
// If a client's outbound buffer exceeds this, treat the connection as stuck
// and close it. Without this, res.write() silently queues into res.outputData
// for a paused/backgrounded client, and every store event for every entity