Files
MikrotikManager/hooks/use-traffic-live.ts
T

108 lines
4.2 KiB
TypeScript
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.
"use client"
import { useEffect, useState } from "react"
import { resolveApiUrl, withAuthHeaders } from "@/shared/api/http-client"
export interface TrafficLiveSample {
rxMbps: number
txMbps: number
at: string
}
function parseSseBlock(block: string): { event: string; data: string } {
let event = "message"
const dataLines: string[] = []
for (const line of block.split(/\r?\n/)) {
if (line.startsWith("event:")) event = line.slice(6).trim()
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim())
}
return { event, data: dataLines.join("\n") }
}
export function useTrafficLive(opts: {
enabled: boolean
backendUrl: string
serverId: string
iface: string
}): { 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)
useEffect(() => {
if (!opts.enabled || !opts.serverId) return
const ac = new AbortController()
let sampleTimer: ReturnType<typeof setTimeout> | undefined
const stopTimer = () => clearTimeout(sampleTimer)
const setError = (error: string) => {
stopTimer()
if (!ac.signal.aborted) setResult(previous => ({ key, sample: previous?.key === key ? previous.sample : null, receivedAt: previous?.key === key ? previous.receivedAt : null, error }))
}
const ifaceQ =
opts.iface && opts.iface !== "__all__"
? `?iface=${encodeURIComponent(opts.iface)}`
: ""
const path = `/api/traffic/servers/${encodeURIComponent(opts.serverId)}/live${ifaceQ}`
const url = resolveApiUrl(opts.backendUrl, path)
// Backend ticks every 1.5–2s; tolerate several missed samples. Heartbeats do not reset this timer.
const awaitSample = () => {
stopTimer()
sampleTimer = setTimeout(() => setError("Нет обновлений live более 15 секунд"), 15_000)
}
awaitSample()
let buf = ""
void (async () => {
try {
const res = await fetch(url, {
headers: withAuthHeaders({ Accept: "text/event-stream" }),
signal: ac.signal,
credentials: "include",
})
if (ac.signal.aborted) return
if (!res.ok || !res.body) {
setError(`live HTTP ${res.status}`)
return
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
while (!ac.signal.aborted) {
const { done, value } = await reader.read()
if (ac.signal.aborted) return
if (done) {
setError("Поток live завершён")
break
}
buf += decoder.decode(value, { stream: true })
const parts = buf.split(/\r?\n\r?\n/)
buf = parts.pop() ?? ""
for (const raw of parts) {
if (!raw.trim()) continue
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))) {
throw new Error("Некорректные данные live-потока")
}
awaitSample()
setResult({ key, 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")
}
}
}
} catch (e) {
if (ac.signal.aborted) return
setError(e instanceof Error ? e.message : "live error")
} finally {
stopTimer()
}
})()
return () => { stopTimer(); ac.abort() }
}, [opts.enabled, opts.backendUrl, opts.serverId, opts.iface, key])
return result?.key === key ? { sample: result.sample, error: result.error, receivedAt: result.receivedAt } : { sample: null, error: null, receivedAt: null }
}