Files
abdulive/apps/overlay-server/src/bus.test.ts
T
ZeasyAlexandClaude Opus 5 bc2c8f6cbe Overlay: fuenf Browser-Sources fuer OBS
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>
2026-09-18 00:13:28 +02:00

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);
});