From 4424883496b52d693802555fab9177de4860f4fa Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 7 Aug 2026 12:01:56 +0000 Subject: [PATCH] Fix realtime notifications: event bus wasn't a real singleton in production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of "no toast, no sound, no live updates" reported in production: the ticket-event EventEmitter was only stashed on globalThis in development (to survive Next.js's dev-mode hot reload). The comment explaining that assumed production only loads the module once — true for a simple Node require() cache, but Turbopack's route-level bundling can inline this small shared module separately into each route's own output chunk instead of pointing them at one shared instance. So /api/events (the SSE endpoint) and whatever publishes an event (ticket creation, mail ingestion, agent replies, ...) were each getting their own private, disconnected EventEmitter — the SSE connection itself worked fine (heartbeats arrived reliably, confirmed via the browser's Network tab), but no actual ticket.created/message.created event ever reached a subscriber. Now globalThis is always used as the singleton home, regardless of NODE_ENV — verified locally end-to-end: streamed /api/events with curl while creating a ticket via the API, and the ticket.created + message.created events arrived on the stream immediately. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY --- src/lib/events/bus.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/events/bus.ts b/src/lib/events/bus.ts index a3e90de..0cdf108 100644 --- a/src/lib/events/bus.ts +++ b/src/lib/events/bus.ts @@ -6,16 +6,21 @@ export type TicketEvent = | { type: "ticket.updated"; ticket: TicketDTO } | { type: "message.created"; ticketId: string; customerId: string; assigneeId: string | null; message: MessageDTO }; -// Next.js dev mode reloads this module on every edit — stash the emitter on -// globalThis so hot reload doesn't spawn a second, disconnected bus. +// Stashed on globalThis rather than relying on Node's module cache to give +// us a singleton: Next.js dev mode reloads this module on every edit (hot +// reload would otherwise spawn a second, disconnected bus), and — the +// production-breaking case — Turbopack's route-level bundling can inline +// this small module separately into each route's own chunk instead of +// sharing one instance, so /api/events (the SSE subscriber) and whatever +// publishes an event (ticket creation, mail ingestion, ...) can each end up +// holding their own private EventEmitter that never sees the other's +// events. globalThis is the one thing guaranteed to be the same object +// across every chunk in the process, in both dev and production. const globalForBus = globalThis as unknown as { ticketEventBus?: EventEmitter }; const emitter = globalForBus.ticketEventBus ?? new EventEmitter(); emitter.setMaxListeners(200); // one per open SSE connection - -if (process.env.NODE_ENV !== "production") { - globalForBus.ticketEventBus = emitter; -} +globalForBus.ticketEventBus = emitter; const CHANNEL = "ticket-event";