From fdc14a502160e42ab9264fb8d0e254d9d8f81951 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sun, 1 Mar 2026 09:40:35 -0800 Subject: [PATCH] fix race condition --- src/server/api/apps.ts | 3 ++- src/server/api/events.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index 9c136c5..9a83fed 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -24,13 +24,14 @@ function convert(app: BackendApp): SharedApp { // SSE: full app state snapshots for the dashboard UI (every state change) // For discrete lifecycle events consumed by app processes, see /api/events/stream router.sse('/stream', (send) => { + let queue = Promise.resolve() const broadcast = () => { const apps: SharedApp[] = allApps().map(({ name, state, icon, error, port, started, logs, tool, tunnelEnabled, tunnelUrl }) => ({ name, state, icon, error, port, started, logs, tool, tunnelEnabled, tunnelUrl, })) - send(apps) + queue = queue.then(() => send(apps)) } broadcast() diff --git a/src/server/api/events.ts b/src/server/api/events.ts index f949e24..90f2fa6 100644 --- a/src/server/api/events.ts +++ b/src/server/api/events.ts @@ -7,8 +7,12 @@ const router = Hype.router() // Unlike /api/apps/stream (full state snapshots for the dashboard), this sends // individual events so apps can react to specific lifecycle changes. router.sse('/stream', (send) => { - const unsub = onEvent(event => send(event)) - const heartbeat = setInterval(() => send('', 'ping'), 60_000) + let queue = Promise.resolve() + const safeSend = (...args: Parameters) => { + queue = queue.then(() => send(...args)) + } + const unsub = onEvent(event => safeSend(event)) + const heartbeat = setInterval(() => safeSend('', 'ping'), 60_000) return () => { clearInterval(heartbeat) unsub()