import { Injectable, MessageEvent } from '@nestjs/common'; import { Observable, Observer } from 'rxjs'; import { UserList } from '../list-templates/list-template.types'; export type ListRealtimeEvent = | { type: 'list.snapshot'; data: UserList; } | { type: 'list.deleted'; data: { listId: string }; } | { type: 'heartbeat'; data: { at: string }; }; @Injectable() export class ListRealtimeService { // In-memory SSE fanout for one API process. If the API is scaled horizontally, // replace this map with a shared pub/sub backend while keeping the event shape. private readonly channels = new Map>>(); /** * Opens an owner-scoped stream. The controller authenticates the request before * calling this method, so subscribers only receive their own list events. */ eventsFor(ownerId: string): Observable { return new Observable((observer) => { this.addObserver(ownerId, observer); // Keep proxies and browsers from treating an otherwise quiet stream as idle. const heartbeatInterval = setInterval(() => { observer.next({ type: 'heartbeat', data: { at: new Date().toISOString() }, }); }, 25_000); return () => { clearInterval(heartbeatInterval); this.removeObserver(ownerId, observer); }; }); } publishSnapshot(ownerId: string, list: UserList): void { this.publish(ownerId, { type: 'list.snapshot', data: list, }); } publishDeleted(ownerId: string, listId: string): void { this.publish(ownerId, { type: 'list.deleted', data: { listId }, }); } private publish(ownerId: string, event: ListRealtimeEvent): void { const observers = this.channels.get(ownerId); if (!observers) { return; } observers.forEach((observer) => observer.next(event)); } private addObserver( ownerId: string, observer: Observer, ): void { const observers = this.channels.get(ownerId) ?? new Set(); observers.add(observer); this.channels.set(ownerId, observers); } private removeObserver( ownerId: string, observer: Observer, ): void { const observers = this.channels.get(ownerId); if (!observers) { return; } observers.delete(observer); if (observers.size === 0) { this.channels.delete(ownerId); } } }