fix(traffic): проверять live-срез и прекращать опрос выключенных устройств

This commit is contained in:
Denozordec committed 2026-10-12 21:35:55 +07:00
1 parent 1275dd7b9b
commit 2358ecef60
29 files changed
+1620 -114

No files matched your search

+23 -11
View File
@@ -7,6 +7,7 @@ export interface TrafficLiveSample {
rxMbps: number
txMbps: number
at: string
timestampKind?: "backend-receipt"
}
function parseSseBlock(block: string): { event: string; data: string } {
@@ -24,18 +25,26 @@ export function useTrafficLive(opts: {
backendUrl: string
serverId: string
iface: string
reloadKey?: number
}): { sample: TrafficLiveSample | null; error: string | null; receivedAt: number | null } {
const key = JSON.stringify([opts.enabled, opts.backendUrl, opts.serverId, opts.iface])
const [result, setResult] = useState<{ key: string; sample: TrafficLiveSample | null; receivedAt: number | null; error: string | null } | null>(null)
const scope = JSON.stringify([opts.backendUrl, opts.serverId, opts.iface])
const key = JSON.stringify([opts.enabled, scope, opts.reloadKey ?? 0])
const [result, setResult] = useState<{ key: string; scope: string; sample: TrafficLiveSample | null; receivedAt: number | null; error: string | null } | null>(null)
useEffect(() => {
if (!opts.enabled || !opts.serverId) return
const ac = new AbortController()
let lastStreamError: string | null = null
// A new connection owns the status; the same device's previous sample remains historical.
// eslint-disable-next-line react-hooks/set-state-in-effect
setResult(previous => ({ key, scope, sample: previous?.scope === scope ? previous.sample : null,
receivedAt: previous?.scope === scope ? previous.receivedAt : null, error: "Ожидание нового live-измерения" }))
let sampleTimer: ReturnType<typeof setTimeout> | undefined
const stopTimer = () => clearTimeout(sampleTimer)
const setError = (error: string) => {
lastStreamError = error
stopTimer()
if (!ac.signal.aborted) setResult(previous => ({ key, sample: previous?.key === key ? previous.sample : null, receivedAt: previous?.key === key ? previous.receivedAt : null, error }))
if (!ac.signal.aborted) setResult(previous => ({ key, scope, sample: previous?.scope === scope ? previous.sample : null, receivedAt: previous?.scope === scope ? previous.receivedAt : null, error }))
}
const ifaceQ =
opts.iface && opts.iface !== "__all__"
@@ -61,7 +70,8 @@ export function useTrafficLive(opts: {
})
if (ac.signal.aborted) return
if (!res.ok || !res.body) {
setError(`live HTTP ${res.status}`)
const body = await res.json().catch(() => null) as { error?: unknown } | null
setError(`${typeof body?.error === "string" ? body.error : "Live-поток недоступен"} (HTTP ${res.status})`)
return
}
const reader = res.body.getReader()
@@ -70,7 +80,7 @@ export function useTrafficLive(opts: {
const { done, value } = await reader.read()
if (ac.signal.aborted) return
if (done) {
setError("Поток live завершён")
setError(lastStreamError ?? "Поток live завершён")
break
}
buf += decoder.decode(value, { stream: true })
@@ -81,14 +91,15 @@ export function useTrafficLive(opts: {
const ev = parseSseBlock(raw)
if (ev.event === "sample" && ev.data) {
const parsed = JSON.parse(ev.data) as TrafficLiveSample
if (!parsed || !Number.isFinite(parsed.rxMbps) || !Number.isFinite(parsed.txMbps) || parsed.rxMbps < 0 || parsed.txMbps < 0 || typeof parsed.at !== "string" || !Number.isFinite(Date.parse(parsed.at))) {
if (!parsed || !Number.isFinite(parsed.rxMbps) || !Number.isFinite(parsed.txMbps) || parsed.rxMbps < 0 || parsed.txMbps < 0 || typeof parsed.at !== "string" || !Number.isFinite(Date.parse(parsed.at)) || parsed.timestampKind !== undefined && parsed.timestampKind !== "backend-receipt") {
throw new Error("Некорректные данные live-потока")
}
awaitSample()
setResult({ key, sample: parsed, receivedAt: Date.now(), error: null })
lastStreamError = null
setResult({ key, scope, sample: parsed, receivedAt: Date.now(), error: null })
} else if (ev.event === "error" && ev.data) {
const parsed = JSON.parse(ev.data) as { error?: string }
setError(parsed.error ?? "live error")
const parsed = JSON.parse(ev.data) as { error?: unknown } | null
setError(typeof parsed?.error === "string" ? parsed.error : "Некорректный ответ об ошибке live-потока")
}
}
}
@@ -97,11 +108,12 @@ export function useTrafficLive(opts: {
setError(e instanceof Error ? e.message : "live error")
} finally {
stopTimer()
ac.abort()
}
})()
return () => { stopTimer(); ac.abort() }
}, [opts.enabled, opts.backendUrl, opts.serverId, opts.iface, key])
}, [opts.enabled, opts.backendUrl, opts.serverId, opts.iface, key, scope])
return result?.key === key ? { sample: result.sample, error: result.error, receivedAt: result.receivedAt } : { sample: null, error: null, receivedAt: null }
return opts.enabled && result?.scope === scope ? { sample: result.sample, error: result.key === key ? result.error : "Ожидание нового live-измерения", receivedAt: result.receivedAt } : { sample: null, error: null, receivedAt: null }
}