workshop: страница в общей оболочке сайта + компоненты

Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js,
общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/
workshop.css), компоненты читают данные пода по SSE /workshop/sse.
Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/.
build.js собирает страницу (active: workshop).
This commit is contained in:
eureka 2026-10-04 01:08:02 +03:00
parent e2fe11f723
commit cc3d6c5ca0
15 changed files with 1627 additions and 2 deletions

View file

@ -0,0 +1,64 @@
/* 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);
},
};
})();