/* workshop-stream.js — одна SSE-шина Мастерской на всю страницу. * * Компоненты не открывают свой EventSource: иначе N компонентов — N соединений, * и серверный веер умножается. Все подписываются здесь, а этот файл владеет * единственным соединением и раздаёт каждое событие подписчикам его типа (и «*»). * * SSE живёт на поде данных: `/workshop/sse` (Traefik уводит /workshop на сервис * мастерской). Страница рисуется сайтом eureka-web на общей оболочке. * * Использование: * const off = WorkshopStream.on("noise", (ev) => { ... }); * off(); // отписка */ (function () { const subs = new Map(); // Последнее событие каждого типа, повторяемое поздним подписчикам. Без этого // обработчики страницы теряют события, случившиеся до их регистрации: // компоненты апгрейдятся во время загрузки скрипта, и первый brain_ready // может прийти раньше, чем запустится inline-скрипт страницы. const last = new Map(); let es = null; function dispatch(type, ev) { last.set(type, ev); const set = subs.get(type); if (set) for (const fn of set) safe(fn, ev); const all = subs.get("*"); if (all) for (const fn of all) safe(fn, ev); } function safe(fn, ev) { try { fn(ev); } catch (err) { console.error("[workshop-stream] подписчик упал", err); } } function ensure() { if (es) return; es = new EventSource("/workshop/sse"); es.onmessage = (m) => { let ev; try { ev = JSON.parse(m.data); } catch (err) { console.error("[workshop-stream] плохое событие", err); return; } dispatch(ev.type, ev); }; es.onerror = () => {}; // EventSource переподключается сам } window.WorkshopStream = { on(type, fn) { ensure(); if (!subs.has(type)) subs.set(type, new Set()); subs.get(type).add(fn); if (last.has(type)) safe(fn, last.get(type)); return () => subs.get(type).delete(fn); }, }; })();