export interface SSEEvent { type: string; data: unknown; } export interface SSEClientOptions { path: string; onEvent: (event: SSEEvent) => void; onReconnect?: () => void; } // Wrapper над EventSource с экспоненциальным backoff и догоном через REST. export class SSEClient { private es: EventSource | null = null; private stopped = false; private attempt = 0; private readonly knownEvents = [ 'photo.created', 'photo.updated', 'photo.deleted', 'comment.created', 'like.added', 'like.removed', 'member.joined', 'member.left', 'member.updated', 'album.updated', 'album.deleted', 'notification' ]; constructor(private opts: SSEClientOptions) {} connect(token: string) { this.stopped = false; this.open(token); } private open(token: string) { if (this.stopped) return; const url = `${this.opts.path}?token=${encodeURIComponent(token)}`; this.es = new EventSource(url); this.es.onopen = () => { if (this.attempt > 0) { this.opts.onReconnect?.(); } this.attempt = 0; }; for (const name of this.knownEvents) { this.es.addEventListener(name, (ev) => { let data: unknown = null; try { data = JSON.parse((ev as MessageEvent).data); } catch { data = (ev as MessageEvent).data; } this.opts.onEvent({ type: name, data }); }); } this.es.onerror = () => { this.es?.close(); this.es = null; if (this.stopped) return; const delay = Math.min(1000 * 2 ** this.attempt, 30000); this.attempt += 1; setTimeout(() => this.open(token), delay); }; } close() { this.stopped = true; this.es?.close(); this.es = null; } }