Files
Laufzettel/prisma/migrations/20261005090000_lauf_ereignisse/migration.sql
T
Florian WildeandClaude Opus 5 1be73b266c
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
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>
2026-10-05 22:50:32 +02:00

85 lines
3.5 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ---------------------------------------------------------------------------
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
-- (Archivsuche), die partiellen eindeutigen Indizes und alle Trigger: es kennt
-- sie nicht, weil sie sich im Schema nicht ausdruecken lassen. Diese
-- Loeschbefehle sind bewusst gestrichen.
--
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
-- ---------------------------------------------------------------------------
-- Live-Aktualisierung per SSE statt Polling (#033).
--
-- Die Datenbank sagt selbst, wenn sich an einem Lauf etwas geaendert hat:
-- `NOTIFY lauf_geaendert, '<laufId>'`. Der Route Handler haelt eine
-- LISTEN-Verbindung und schiebt weiter.
--
-- **Warum in der Datenbank und nicht im Dienst.** Ein Ereignis, das der Service
-- verschickt, entsteht nur auf dem Weg durch den Service. Die Regeln dieses
-- Projekts stehen aber absichtlich auch in der Datenbank, weil jemand an der
-- Service-Schicht vorbei schreiben kann — der Seed, eine Migration, eine
-- Reparatur per psql. Ein Trigger sieht all das.
--
-- **Warum es mit mehreren Instanzen funktioniert.** NOTIFY geht an alle
-- Verbindungen, die LISTEN auf dem Kanal haben, also an jede Instanz der
-- Anwendung. Ein zweiter Dienst (Redis, ein Nachrichtenbus) waere dafuer nicht
-- noetig.
--
-- **Zugestellt wird erst beim COMMIT.** Das ist hier genau richtig: ein
-- Abonnent, der eine noch nicht festgeschriebene Aenderung meldet, wuerde die
-- Seite mit Daten neu bauen lassen, die es gleich wieder nicht gibt.
CREATE OR REPLACE FUNCTION laufzettel_lauf_geaendert() RETURNS trigger AS $$
DECLARE
betroffener TEXT;
BEGIN
/*
* Bei DELETE gibt es kein NEW. Der Kanalname ist fest, die Nutzlast nicht –
* deshalb pg_notify() und nicht NOTIFY, das keine Variablen nimmt.
*/
IF TG_OP = 'DELETE' THEN
betroffener := OLD."laufId";
ELSE
betroffener := NEW."laufId";
END IF;
IF betroffener IS NOT NULL THEN
PERFORM pg_notify('lauf_geaendert', betroffener);
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Derselbe Rumpf fuer die Laufzeile selbst: dort heisst der Schluessel "id".
CREATE OR REPLACE FUNCTION laufzettel_lauf_geaendert_selbst() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('lauf_geaendert', NEW."id");
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
/*
* **Welche Tabellen melden, haengt daran, was `laufZustand` liefert** — nicht
* daran, was sich "irgendwie" aendert. Das Lesemodell traegt Status,
* Schrittzustaende, die Zahl der Kommentare und (seit #033) die Zahl der
* Anhaenge. Wuerde eine davon nicht melden, waere der Strom gegenueber dem
* Polling, das alle zehn Sekunden vollstaendig vergleicht, ein **Rueckschritt**:
* ein neuer Kommentar kaeme nie an.
*/
CREATE TRIGGER "Lauf_geaendert_melden"
AFTER UPDATE ON "Lauf"
FOR EACH ROW EXECUTE FUNCTION laufzettel_lauf_geaendert_selbst();
CREATE TRIGGER "Schrittergebnis_geaendert_melden"
AFTER INSERT OR UPDATE OR DELETE ON "Schrittergebnis"
FOR EACH ROW EXECUTE FUNCTION laufzettel_lauf_geaendert();
CREATE TRIGGER "Kommentar_geaendert_melden"
AFTER INSERT OR UPDATE OR DELETE ON "Kommentar"
FOR EACH ROW EXECUTE FUNCTION laufzettel_lauf_geaendert();
CREATE TRIGGER "Anhang_geaendert_melden"
AFTER INSERT OR DELETE ON "Anhang"
FOR EACH ROW EXECUTE FUNCTION laufzettel_lauf_geaendert();