Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein Ding, das mitten im Stream kaputtgehen kann. Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md: Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource` verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge: der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar), und ein Zustands-Schnappschuss beim Verbinden. Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur ueber EventSub. GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten. gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif PRIVMSG #abdulive_ :[Idk GIF by Respective] Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn. Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]" im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094 Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse eben nicht im Text steht. Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind: - Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der Ausblend-Animation aus dem DOM verschwinden, und lief in eine Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag eingefroren. - Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe gueltig aus und wuerde den falschen Textteil ersetzen. Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht, sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus. build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket 404 statt Overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
// Der Bus entscheidet, was eine frisch verbundene Browser-Source sieht.
|
|
// Danach richtet sich, ob das Overlay nach einem Neustart vollstaendig ist
|
|
// oder leer im Bild steht - und ob eine geloeschte Nachricht geloescht bleibt.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createBus, type ChatEvent, type OverlayEvent } from './bus.ts';
|
|
|
|
function nachricht(over: Partial<ChatEvent> = {}): ChatEvent {
|
|
return {
|
|
kind: 'chat', id: 'm1', userId: 'u1', login: 'kevin', name: 'Kevin',
|
|
text: 'moin', color: null, emotes: [], gifs: [], first: false, mod: false,
|
|
sub: false, broadcaster: false, bits: 0,
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function schnappschuss(bus: ReturnType<typeof createBus>) {
|
|
const s = bus.snapshot();
|
|
assert.equal(s.kind, 'state');
|
|
return s as Extract<OverlayEvent, { kind: 'state' }>;
|
|
}
|
|
|
|
test('ein neuer Zuhoerer bekommt den bisherigen Chat', () => {
|
|
const bus = createBus();
|
|
bus.publish(nachricht({ id: 'a' }));
|
|
bus.publish(nachricht({ id: 'b' }));
|
|
|
|
const s = schnappschuss(bus);
|
|
assert.deepEqual(s.chat.map((m) => m.id), ['a', 'b']);
|
|
});
|
|
|
|
test('der Rueckblick waechst nicht unbegrenzt', () => {
|
|
const bus = createBus();
|
|
for (let i = 0; i < 100; i++) bus.publish(nachricht({ id: `m${i}` }));
|
|
|
|
const s = schnappschuss(bus);
|
|
assert.equal(s.chat.length, 30);
|
|
assert.equal(s.chat[s.chat.length - 1]?.id, 'm99', 'die neuesten bleiben');
|
|
});
|
|
|
|
test('der zuletzt gemeldete Stream-Zustand steht im Schnappschuss', () => {
|
|
const bus = createBus();
|
|
bus.publish({ kind: 'stream', live: true, title: 'Titel', category: 'Elden Ring',
|
|
viewers: 12, startedAt: '2026-09-17T18:00:00Z' });
|
|
bus.publish({ kind: 'stream', live: true, title: 'Titel', category: 'Elden Ring',
|
|
viewers: 19, startedAt: '2026-09-17T18:00:00Z' });
|
|
|
|
assert.equal(schnappschuss(bus).stream.viewers, 19);
|
|
});
|
|
|
|
test('eine geloeschte Nachricht verschwindet auch aus dem Rueckblick', () => {
|
|
// Sonst holt der naechste Neustart der Browser-Source sie zurueck ins Bild -
|
|
// und zwar genau die eine, die ein Mod entfernt hat.
|
|
const bus = createBus();
|
|
bus.publish(nachricht({ id: 'a' }));
|
|
bus.publish(nachricht({ id: 'b' }));
|
|
bus.publish({ kind: 'clear', messageId: 'a' });
|
|
|
|
assert.deepEqual(schnappschuss(bus).chat.map((m) => m.id), ['b']);
|
|
});
|
|
|
|
test('ein gesperrter Chatter verschwindet mit allen Nachrichten', () => {
|
|
const bus = createBus();
|
|
bus.publish(nachricht({ id: 'a', userId: 'u1' }));
|
|
bus.publish(nachricht({ id: 'b', userId: 'u2' }));
|
|
bus.publish(nachricht({ id: 'c', userId: 'u1' }));
|
|
bus.publish({ kind: 'clear', userId: 'u1' });
|
|
|
|
assert.deepEqual(schnappschuss(bus).chat.map((m) => m.id), ['b']);
|
|
});
|
|
|
|
test('ein geleerter Chat leert auch den Rueckblick', () => {
|
|
const bus = createBus();
|
|
bus.publish(nachricht({ id: 'a' }));
|
|
bus.publish({ kind: 'clear' });
|
|
assert.deepEqual(schnappschuss(bus).chat, []);
|
|
});
|
|
|
|
test('alte Einblendungen werden nicht endlos aufbewahrt', () => {
|
|
const bus = createBus();
|
|
for (let i = 0; i < 20; i++) {
|
|
bus.publish({ kind: 'alert', type: 'sub', id: `a${i}`, name: 'x' });
|
|
}
|
|
assert.equal(schnappschuss(bus).alerts.length, 5);
|
|
});
|
|
|
|
test('ein Zuhoerer, der wirft, reisst die anderen nicht mit', () => {
|
|
// Eine Browser-Source, deren Verbindung gerade zusammengebrochen ist, darf
|
|
// den Chat nicht fuer alle anderen anhalten.
|
|
const bus = createBus();
|
|
const gesehen: string[] = [];
|
|
bus.subscribe(() => { throw new Error('Verbindung weg'); });
|
|
bus.subscribe((ev) => { gesehen.push(ev.kind); });
|
|
|
|
bus.publish(nachricht());
|
|
assert.deepEqual(gesehen, ['chat']);
|
|
});
|
|
|
|
test('abgemeldete Zuhoerer bekommen nichts mehr', () => {
|
|
const bus = createBus();
|
|
let n = 0;
|
|
const ab = bus.subscribe(() => { n++; });
|
|
bus.publish(nachricht());
|
|
ab();
|
|
bus.publish(nachricht());
|
|
|
|
assert.equal(n, 1);
|
|
assert.equal(bus.clients(), 0);
|
|
});
|