Workers P2: Feed-Steuerung und Hooks (Zustand 15 s, Ereignisse 5 s)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 01:34:20 +00:00
1 parent 97fdfbfcf0
commit 0bfad0ae0b
3 files changed
+630

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createFeedController } from '../lib/workersFeed'
import { createRequestGuard } from '../lib/requestGuard'
// Spur eines Hustlers wird bei sichtbarem Tab alle 15 s neu geholt
const TRACE_MS = 15000
const UNKNOWN_ERROR = 'Unbekannter Fehler'
const isVisible = () => document.visibilityState !== 'hidden'
// { state, error, loaded, refresh }: Zustand der Hustler (alle 15 s) und Ereignisse (alle 5 s, über onEvents) von der
// Dienst-API. state ist null bis zum ersten Erfolg, loaded wird dann wahr und bleibt es. error ist der deutsche Fehlertext
// (oder null); die Abfrage läuft bei einem Fehler weiter. Im Hintergrund-Tab ruht alles, beim Zurückkehren wird sofort
// abgefragt. refresh() holt den Zustand sofort (z. B. nach dem Pausenschalter). onEvents darf bei jedem Render neu sein.
export function useWorkersFeed(api, { onEvents } = {}) {
const [state, setState] = useState(null)
const [error, setError] = useState(null)
const [loaded, setLoaded] = useState(false)
const onEventsRef = useRef(onEvents)
const controllerRef = useRef(null)
useEffect(() => { onEventsRef.current = onEvents }, [onEvents])
useEffect(() => {
setError(null)
const controller = createFeedController({
api,
setTimer: (fn, ms) => window.setTimeout(fn, ms),
clearTimer: (id) => window.clearTimeout(id),
isVisible,
onSnapshot: (snapshot) => {
setState(snapshot)
setLoaded(true)
},
onEvents: (list) => { if (onEventsRef.current) onEventsRef.current(list) },
onError: (err) => setError(err ? err.message || UNKNOWN_ERROR : null),
})
controllerRef.current = controller
const onVisibility = () => controller.visibilityChanged()
document.addEventListener('visibilitychange', onVisibility)
controller.start()
return () => {
document.removeEventListener('visibilitychange', onVisibility)
controller.stop()
controllerRef.current = null
}
}, [api])
const refresh = useCallback(() => {
if (controllerRef.current) controllerRef.current.refreshState()
}, [])
return { state, error, loaded, refresh }
}
// { trace, loading, error }: Spur eines Hustlers (api.trace). workerId null ergibt { trace: null } ohne Abruf. Beim
// Wechsel der workerId wird neu geladen (loading, bis die erste Antwort da ist), danach alle 15 s bei sichtbarem Tab und
// sofort beim Zurückkehren. Eine späte Antwort für einen früheren Hustler oder eine ältere Anfrage wird verworfen.
export function useWorkerTrace(api, workerId) {
const [result, setResult] = useState({ key: null, trace: null, error: null })
useEffect(() => {
if (!workerId) return undefined
const guard = createRequestGuard()
let timer = null
const stopTimer = () => {
if (timer === null) return
window.clearTimeout(timer)
timer = null
}
const load = async () => {
stopTimer()
const id = guard.next()
try {
const trace = await api.trace(workerId)
if (!guard.isCurrent(id)) return
setResult({ key: workerId, trace, error: null })
} catch (err) {
if (!guard.isCurrent(id)) return
// die letzte Spur desselben Hustlers bleibt stehen, der Fehler kommt dazu
setResult((prev) => ({ key: workerId, trace: prev.key === workerId ? prev.trace : null, error: err?.message || UNKNOWN_ERROR }))
}
timer = window.setTimeout(tick, TRACE_MS)
}
// Im Hintergrund kein Abruf, nur neu planen
const tick = () => {
timer = null
if (isVisible()) load()
else timer = window.setTimeout(tick, TRACE_MS)
}
const onVisibility = () => { if (isVisible()) load() }
document.addEventListener('visibilitychange', onVisibility)
load()
return () => {
guard.next()
stopTimer()
document.removeEventListener('visibilitychange', onVisibility)
}
}, [api, workerId])
const current = Boolean(workerId) && result.key === workerId
return {
trace: current ? result.trace : null,
loading: Boolean(workerId) && !current,
error: current ? result.error : null,
}
}