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
3cbd5026f8
commit
4aa79c93ff
@@ -5,12 +5,19 @@ import { laufZumAbarbeiten, laufZustand } from "@/server/queries/laeufe";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* Der Zustand fuer die Aktualisierung alle zehn Sekunden (#013).
|
||||
* Der Zustand, von dem die Aktualisierung lebt (#013, Strom seit #033).
|
||||
*
|
||||
* Zwei Zusagen haengen daran: zwei Browser am selben Lauf sehen die Aenderung
|
||||
* des anderen — und die Antwort enthaelt **keine Befehle**, weil ein
|
||||
* `{{generiertes_passwort}}` bei jeder Aufloesung neu entsteht und dem Techniker
|
||||
* sonst unter den Haenden wegtauschte.
|
||||
*
|
||||
* Seit #033 kommt der Stand ueber einen Ereignisstrom statt alle zehn Sekunden.
|
||||
* Am Lesemodell aendert das nichts — **aber an seiner Vollstaendigkeit hängt
|
||||
* jetzt mehr**: das Polling verglich den ganzen Stand und sah damit auch, was
|
||||
* kein Trigger meldet. Was hier fehlt, loest nun gar keine Aktualisierung mehr
|
||||
* aus. Deshalb zaehlt der Test unten die Schluessel namentlich auf, und
|
||||
* `tests/lauf-ereignisse.test.ts` prueft, dass zu jedem eine Meldung gehoert.
|
||||
*/
|
||||
|
||||
async function aufbau() {
|
||||
@@ -137,6 +144,7 @@ describe.runIf(datenbankVerfuegbar)("Zustand eines Laufs", () => {
|
||||
expect(alsText).not.toContain("Password");
|
||||
expect(alsText).not.toContain("{{");
|
||||
expect(Object.keys(zustand ?? {}).sort()).toEqual([
|
||||
"anzahlAnhaenge",
|
||||
"anzahlKommentare",
|
||||
"anzahlPflichtOffen",
|
||||
"geaendertAm",
|
||||
@@ -163,7 +171,31 @@ describe.runIf(datenbankVerfuegbar)("Zustand eines Laufs", () => {
|
||||
expect(mitKommentar).not.toBe(vorher);
|
||||
|
||||
await testPrisma.lauf.update({ where: { id: lauf.id }, data: { titel: "PC-001 (neuer Titel)" } });
|
||||
expect(JSON.stringify(await laufZustand(lauf.id, benutzer))).not.toBe(mitKommentar);
|
||||
const mitTitel = JSON.stringify(await laufZustand(lauf.id, benutzer));
|
||||
expect(mitTitel).not.toBe(mitKommentar);
|
||||
|
||||
/*
|
||||
* Und ein Anhang (#032/#033). Derselbe Grund wie beim Kommentar: ein Foto,
|
||||
* das der Kollege gerade an einen Schritt gehaengt hat, blieb sonst
|
||||
* unsichtbar — und seit der Stand nicht mehr alle zehn Sekunden vollstaendig
|
||||
* verglichen wird, bliebe es das fuer immer.
|
||||
*/
|
||||
const ergebnis = await testPrisma.schrittergebnis.findFirstOrThrow({
|
||||
where: { laufId: lauf.id },
|
||||
select: { id: true },
|
||||
});
|
||||
await testPrisma.anhang.create({
|
||||
data: {
|
||||
schrittergebnisId: ergebnis.id,
|
||||
laufId: lauf.id,
|
||||
kundeId: lauf.kundeId,
|
||||
dateiname: "typenschild.png",
|
||||
medientyp: "image/png",
|
||||
groesse: 42,
|
||||
pruefsumme: "c".repeat(64),
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(await laufZustand(lauf.id, benutzer))).not.toBe(mitTitel);
|
||||
});
|
||||
|
||||
it("liefert den Status mit", async () => {
|
||||
|
||||
Reference in New Issue
Block a user