Fix realtime notifications: event bus wasn't a real singleton in production
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
This commit is contained in:
1 parent
681f89000f
commit
4424883496
1 file changed
+11
-6
+11
-6
@@ -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";
|
||||
|
||||
|
||||
Reference in new issue
Block a user