feat(laeufe): Live-Aktualisierung per Ereignisstrom statt Polling (#53)
Die Abarbeitungsansicht fragte alle zehn Sekunden nach dem Zustand. Jetzt melden vier Datenbank-Trigger `NOTIFY lauf_geaendert, '<laufId>'`, eine LISTEN-Verbindung je Prozess hoert zu, und der Route Handler schiebt weiter. Gemessen: der Haken des Kollegen ist nach 485 ms beim Zuschauer (am Handy 487 ms), vorher bis zu zehn Sekunden. Zwanzig offene Laschen kosten nichts mehr statt zwanzig Abfragen je zehn Sekunden. Das Ereignis kommt aus der Datenbank und nicht aus dem Dienst, aus demselben Grund, aus dem auch die Regeln dort stehen: ein Trigger sieht auch den Seed, eine Migration und die Reparatur per psql. Und NOTIFY erreicht jede Instanz der Anwendung, ohne einen zweiten Dienst. Welche Tabellen melden, haengt an dem, was `laufZustand` liefert — nicht daran, was das Issue vorgeschlagen hat. Mit Triggern nur auf Schrittergebnis und Lauf waere der Strom ein **Rueckschritt** gewesen: das Polling verglich den ganzen Stand und sah deshalb auch Kommentare, ein Strom sieht nur, was gemeldet wird. Der Kommentar des Kollegen waere nie angekommen, und die Seite haette ausgesehen wie immer. Es melden jetzt vier Tabellen, und `laufZustand` traegt `anzahlAnhaenge` — ein Ereignis ohne Entsprechung im Zustand loest nichts aus. Das Polling bleibt als Rueckfall an **derselben** Adresse, unterschieden nur am Accept-Kopf: eine Rechtepruefung, ein Lesemodell, eine Nutzlast. Ein zweiter Endpunkt waere eine zweite Stelle, an der jemand rechtErforderlich vergessen kann. Bei erreichter Obergrenze antwortet der Handler gewoehnlich mit JSON; EventSource meldet darauf einen Fehler und versucht es nicht wieder, also faellt der Client von sich aus zurueck — ohne Sonderfall. Nebenbei zwei Fehler in docs/betrieb/reverse-proxy.md behoben, beide aus #52: `client_max_body_size 8m` bei `ANHANG_MAX_MB=10` haette ein 10-MB-Foto mit 413 abgewiesen, bevor die eigene Meldung zum Zuge kommt. Closes #53 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3ccf3aa2d9
commit
1be73b266c
+57
-15
@@ -1,10 +1,10 @@
|
||||
# #033 M20 – Live-Aktualisierung per SSE statt Polling
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Im Tracker:** [#53](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/53)
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** phase-2, backend, frontend
|
||||
**Geschätzt:** 6 h **Tatsächlich:** –
|
||||
**Geschätzt:** 6 h **Tatsächlich:** 1 h 40 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -17,36 +17,78 @@ Die Vorleistung steht: das Polling liegt in genau einem Route Handler
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
- [ ] **`LISTEN`/`NOTIFY` in Postgres als Signal** — Empfehlung. Ein Trigger auf `Schrittergebnis`
|
||||
- [x] **`LISTEN`/`NOTIFY` in Postgres als Signal** — Empfehlung. Ein Trigger auf `Schrittergebnis`
|
||||
und `Lauf` schickt `NOTIFY lauf_geaendert, '<laufId>'`; der Route Handler hält eine Verbindung
|
||||
und schiebt weiter. Damit funktioniert es auch mit mehreren Instanzen der Anwendung, ohne
|
||||
einen zweiten Dienst. Die Alternative — der Server fragt selbst alle zwei Sekunden — wäre
|
||||
dasselbe Polling, nur an anderer Stelle.
|
||||
- [ ] **Polling bleibt als Rückfall**, nicht als zweite Wahrheit: derselbe Handler antwortet weiter
|
||||
- [x] **Polling bleibt als Rückfall**, nicht als zweite Wahrheit: derselbe Handler antwortet weiter
|
||||
auf eine gewöhnliche Anfrage. Wer kein `EventSource` hat oder hinter einem Proxy sitzt, der
|
||||
Ströme puffert, bekommt den alten Weg. Ein Rückfall, der nie geprüft wird, ist keiner —
|
||||
deshalb ein Test je Weg.
|
||||
|
||||
## Aufgaben
|
||||
|
||||
- [ ] Trigger und `NOTIFY` in einer handgeschriebenen Migration, mit dem üblichen Vermerk (Prisma
|
||||
- [x] Trigger und `NOTIFY` in einer handgeschriebenen Migration, mit dem üblichen Vermerk (Prisma
|
||||
kennt Trigger nicht und will sie löschen)
|
||||
- [ ] Route Handler liefert `text/event-stream`, schickt alle 25 s einen Kommentar als Herzschlag
|
||||
- [x] Route Handler liefert `text/event-stream`, schickt alle 25 s einen Kommentar als Herzschlag
|
||||
(sonst schließen Proxys die Verbindung) und beendet sauber bei `abort`
|
||||
- [ ] **Der Handler prüft Recht und Mandant selbst** — ein offener Strom ist ein Leseweg wie jeder
|
||||
- [x] **Der Handler prüft Recht und Mandant selbst** — ein offener Strom ist ein Leseweg wie jeder
|
||||
andere (CLAUDE.md: Route Handler deckt die Hülle nicht ab)
|
||||
- [ ] Client: `EventSource` mit Wiederverbindung; bei versteckter Lasche pausieren wie heute; bei
|
||||
- [x] Client: `EventSource` mit Wiederverbindung; bei versteckter Lasche pausieren wie heute; bei
|
||||
`error` auf Polling zurückfallen und das **einmal** melden, nicht bei jedem Versuch
|
||||
- [ ] `docs/betrieb/reverse-proxy.md`: `proxy_buffering off` für nginx, `flush_interval -1` für
|
||||
- [x] `docs/betrieb/reverse-proxy.md`: `proxy_buffering off` für nginx, `flush_interval -1` für
|
||||
Caddy — ohne das kommt kein Ereignis an, und die Seite sieht aus, als sei sie hängen geblieben
|
||||
- [ ] Obergrenze für gleichzeitige Ströme je Instanz, damit ein Browser mit vierzig Tabs die
|
||||
- [x] Obergrenze für gleichzeitige Ströme je Instanz, damit ein Browser mit vierzig Tabs die
|
||||
Verbindungen nicht aufbraucht
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Test: zwei Sitzungen am selben Lauf — hakt die eine ab, sieht die andere es in unter einer
|
||||
- [x] Test: zwei Sitzungen am selben Lauf — hakt die eine ab, sieht die andere es in unter einer
|
||||
Sekunde, ohne neu zu laden
|
||||
- [ ] Test: ohne Recht wird der Strom abgewiesen, bevor das erste Ereignis fließt
|
||||
- [ ] Test: ein Lauf eines fremden Kunden liefert keinen Strom
|
||||
- [ ] Test: der Rückfall auf Polling funktioniert — gefahren mit abgeschaltetem `EventSource`
|
||||
- [ ] Der Herzschlag kommt an: eine Verbindung überlebt 60 s ohne Änderung
|
||||
- [x] Test: ohne Recht wird der Strom abgewiesen, bevor das erste Ereignis fließt
|
||||
- [x] Test: ein Lauf eines fremden Kunden liefert keinen Strom
|
||||
- [x] Test: der Rückfall auf Polling funktioniert — gefahren mit abgeschaltetem `EventSource`
|
||||
- [x] Der Herzschlag kommt an: eine Verbindung überlebt 60 s ohne Änderung
|
||||
|
||||
## Beim Umsetzen dazugekommen
|
||||
|
||||
- **Es wäre ein Rückschritt geworden.** Das Issue nannte Trigger auf `Schrittergebnis` und `Lauf`.
|
||||
`laufZustand` trägt aber auch die Zahl der **Kommentare** — das Polling verglich alle zehn
|
||||
Sekunden den ganzen Stand und sah sie damit, ein Strom sieht nur, was gemeldet wird. Mit den zwei
|
||||
Triggern aus dem Issue wäre der Kommentar des Kollegen **nie** angekommen, und niemandem wäre
|
||||
etwas aufgefallen: die Seite sieht aus wie immer. Jetzt melden vier Tabellen, und
|
||||
`tests/datenbankregeln.test.ts` zählt sie namentlich auf. Gegenprobe gefahren: ohne die Trigger auf
|
||||
`Kommentar` und `Anhang` wird `tests/lauf-ereignisse.test.ts` rot.
|
||||
- **`laufZustand` hat `anzahlAnhaenge` bekommen.** Anhänge gibt es seit #032, im Zustand standen sie
|
||||
nicht. Ein Ereignis ohne Entsprechung im Zustand löst nichts aus, weil der Client vergleicht,
|
||||
bevor er neu lädt — ein angehängtes Foto wäre also auch mit Trigger unsichtbar geblieben.
|
||||
- **Ein Handler für beide Wege, unterschieden am `Accept`-Kopf.** So steht „Polling bleibt als
|
||||
Rückfall, nicht als zweite Wahrheit“ nicht nur im Issue: es ist dieselbe Rechteprüfung, dasselbe
|
||||
Lesemodell, dieselbe Nutzlast. Ein zweiter Endpunkt wäre eine zweite Stelle, an der jemand
|
||||
`rechtErforderlich` vergessen kann.
|
||||
- **Die Obergrenze braucht keinen Sonderfall.** Ist sie erreicht, antwortet der Handler gewöhnlich
|
||||
mit JSON. `EventSource` verlangt `text/event-stream`, meldet einen Fehler und versucht es **nicht
|
||||
wieder** — der Client fällt damit von sich aus auf Polling zurück. Kein eigener Fehlercode, kein
|
||||
zweiter Pfad.
|
||||
- **Eine LISTEN-Verbindung je Prozess, nicht je Strom** — und ausdrücklich **nicht** aus dem
|
||||
Prisma-Pool: ein Pool gibt Verbindungen zurück und verteilt sie neu, ein `LISTEN` darauf gilt
|
||||
danach für eine fremde Anfrage oder für niemanden.
|
||||
- **Nach einem Verbindungsabbruch werden alle Zuhörer geweckt.** `NOTIFY` hat keinen
|
||||
Zwischenspeicher; was während des Abbruchs passierte, ist verloren. Lieber ein Abruf zu viel als
|
||||
ein stiller Stand, der falsch ist.
|
||||
- **Bei versteckter Lasche wird der Strom geschlossen**, nicht bloß ignoriert. Eine offene
|
||||
Verbindung je Hintergrundlasche ist genau das, was die Obergrenze verhindern soll.
|
||||
- **Zwei Fehler in `docs/betrieb/reverse-proxy.md` gefunden, beide aus #032:**
|
||||
`client_max_body_size` stand auf **8m**, während `ANHANG_MAX_MB` 10 erlaubt — ein 10-MB-Foto wäre
|
||||
an nginx mit „413“ gescheitert, und die freundliche Meldung der Anwendung wäre nie zum Zuge
|
||||
gekommen. Jetzt 12m, mit dem Hinweis, dass beide Werte zusammengehören. Dazu beschrieb der
|
||||
Kommentar am `proxy_read_timeout` noch das alte Polling.
|
||||
- **Der Herzschlag ist echt über 70 Sekunden gefahren**, nicht nur im Test mit verkürztem Intervall:
|
||||
`readyState` blieb `OPEN`, ein einziges Zustands-Ereignis, kein Fehler, kein Neuaufbau.
|
||||
- **Gemessen statt behauptet:** der Haken war nach **485 ms** beim Zuschauer (Desktop) und nach
|
||||
**487 ms** am Handy. Mit abgeschaltetem `EventSource` dauert derselbe Ablauf rund 13 Sekunden —
|
||||
das ist zugleich der Nachweis, dass der Strom wirklich benutzt wird und nicht heimlich gepollt
|
||||
wird.
|
||||
- **Der Testname versprach `tests/e2e/live.spec.ts`**, also gibt es die Datei. Zwei Browser-Kontexte,
|
||||
und für den Rückfall wird dem Zuschauer `EventSource` vor dem Laden weggenommen.
|
||||
|
||||
Reference in New Issue
Block a user