Files
AfiS-Aktensystem/web/components/realtime/useStream.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

76 lines
2.5 KiB
TypeScript

'use client'
import { useEffect, useRef } from 'react'
/**
* Eine Verbindung zum Ereignisstrom, so lange die Ansicht offen ist.
*
* `EventSource` baut nach einem Abriss von selbst wieder auf und schickt dabei
* die zuletzt gesehene Kennung mit, sodass der Server nachliefern kann. Das
* Aufräumen bei Tabwechsel übernimmt der Browser: eine Verbindung ohne
* Verkehr kostet nichts.
*
* Die Rückrufe stehen in einem Ref, damit ein neu erzeugter Handler nicht die
* ganze Verbindung neu aufbaut - sonst reisst der Strom bei jedem Tastendruck
* im Eingabefeld ab.
*/
export type StreamHandlers = Record<string, (data: unknown) => void>
export function useStream(topics: string[], handlers: StreamHandlers): void {
const aktuell = useRef(handlers)
aktuell.current = handlers
// Die Themen als Zeichenkette, damit ein neues Array mit gleichem Inhalt
// keine neue Verbindung auslöst.
const schluessel = topics.filter(Boolean).sort().join(',')
useEffect(() => {
if (!schluessel) return
const quelle = new EventSource(`/api/stream?topics=${encodeURIComponent(schluessel)}`)
const angemeldet: [string, EventListener][] = []
for (const name of Object.keys(aktuell.current)) {
const listener: EventListener = (event) => {
const nachricht = event as MessageEvent<string>
try {
aktuell.current[name]?.(JSON.parse(nachricht.data))
} catch {
// Kaputte Nutzlast ignorieren; der nächste Seitenaufbau hat den Stand.
}
}
quelle.addEventListener(name, listener)
angemeldet.push([name, listener])
}
// Der Server meldet einen Entzug ausdrücklich, statt die Verbindung nur
// fallen zu lassen: `EventSource` baut sonst sofort wieder auf, und man
// arbeitet auf einer Seite weiter, für die man keine Rechte mehr hat.
// Neu laden führt in die Anmeldung - dorthin, wo man jetzt hingehört.
const entzogen: EventListener = () => {
quelle.close()
window.location.reload()
}
quelle.addEventListener('revoked', entzogen)
angemeldet.push(['revoked', entzogen])
return () => {
for (const [name, listener] of angemeldet) quelle.removeEventListener(name, listener)
quelle.close()
}
// Absichtlich nur an den Themen: die Rückrufe hängen im Ref.
}, [schluessel])
}
export type ScanEvent = {
id: string
scanStatus: 'PENDING' | 'CLEAN' | 'BLOCKED' | 'FAILED'
scanMessage: string | null
mimeType: string
sizeBytes: number
blurhash: string | null
width: number | null
height: number | null
hasThumb: boolean
}