/** * In-memory pub/sub bus for in-app notifications. * * Producers (e.g. ticket action-notification route) call publishNotification(); * SSE connections in server/api/notifications/stream.get.ts subscribe and forward * the payload to the matching client(s), where plugins/03.notifications-stream.client.ts * turns each event into an in-app toast via useToast(). * * KNOWN LIMITATION — PM2 cluster mode (2 instances per ecosystem.config.js): * This bus is per-process. A publish on instance A is not seen by SSE clients * connected to instance B. With 2 instances and roughly even load-balancing, * about half of events will be missed by the in-app toast layer for any given * recipient. The OS push path (sw-notifications.js) is unaffected because the * web-push server speaks directly to the device endpoint and doesn't go via * this bus. If cross-instance toast delivery becomes important, add a SQLite- * or Redis-backed backplane: publishNotification inserts a row, each instance * polls for unseen rows and re-emits locally. * * The instance is stashed on globalThis so that Nitro's HMR in dev does not * blow away connected listeners on every file save. */ import { EventEmitter } from 'node:events' export interface NotificationPayload { title: string body: string url?: string icon?: string type?: string ticketId?: number | string docNo?: string timestamp?: number [k: string]: any } export interface NotificationEvent { recipientRoleIds: number[] payload: NotificationPayload } const BUS_KEY = Symbol.for('logship.notificationBus') const g = globalThis as any if (!g[BUS_KEY]) { const bus = new EventEmitter() // One listener per connected SSE client. Default cap is 10; raise so we // don't get unbounded-listener warnings as more users sign in. bus.setMaxListeners(0) g[BUS_KEY] = bus console.log('[NotifBus] initialised') } export const notificationBus: EventEmitter = g[BUS_KEY] export const publishNotification = (evt: NotificationEvent) => { if (!evt.payload.timestamp) evt.payload.timestamp = Date.now() notificationBus.emit('notification', evt) console.log( `[NotifBus] published — type=${evt.payload.type ?? 'generic'} recipients=${evt.recipientRoleIds.length}` ) }