feat(laeufe): Live-Aktualisierung per Ereignisstrom statt Polling (#53)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s

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:
Florian Wilde
2026-10-05 09:36:10 +02:00
co-authored by Claude Opus 5
parent 3cbd5026f8
commit 4aa79c93ff
20 changed files with 1640 additions and 84 deletions
+23 -1
View File
@@ -56,7 +56,11 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
});
it("hat die Trigger, die eine veröffentlichte Version schützen", async () => {
expect(await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal`)).toEqual([
expect(
await namen(
`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname NOT LIKE '%_melden'`,
),
).toEqual([
"BausteinSchritt_unveraenderlich",
"BausteinVersion_status_einbahn",
"VorlagenAbschnitt_baustein_veroeffentlicht",
@@ -67,6 +71,24 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
]);
});
it("hat die Trigger, die eine Änderung am Lauf melden", async () => {
/*
* Ohne sie bleibt die Live-Aktualisierung still, und zwar **lautlos**: die
* Seite sieht aus wie immer, nur sieht niemand mehr die Arbeit des anderen.
* Welche Tabellen dabei sind, haengt an dem, was `laufZustand` liefert —
* fehlte `Kommentar` oder `Anhang`, waere der Strom gegenueber dem alten
* Polling ein Rueckschritt (#033).
*/
expect(
await namen(`SELECT tgname AS name FROM pg_trigger WHERE NOT tgisinternal AND tgname LIKE '%_melden'`),
).toEqual([
"Anhang_geaendert_melden",
"Kommentar_geaendert_melden",
"Lauf_geaendert_melden",
"Schrittergebnis_geaendert_melden",
]);
});
it("hat die partiellen Eindeutigkeitsindizes", async () => {
expect(
await namen(