fix(traffic): сохранять историю и показывать достоверную свежесть
This commit is contained in:
1 parent
ebe62ae2ef
commit
1275dd7b9b
48 files changed
+1899
-187
No files matched your search
+4
-2
@@ -1,5 +1,6 @@
|
||||
/** Формат скорости: Кбит/с при < 1 Мбит/с, иначе Мбит/с / Гбит/с. */
|
||||
export function fmtRate(v: number): string {
|
||||
export function fmtRate(v: number | null | undefined): string {
|
||||
if (v == null) return "—"
|
||||
if (!Number.isFinite(v) || v <= 0) return "0 Кбит/с"
|
||||
if (v >= 1000) return `${(v / 1000).toFixed(2)} Гбит/с`
|
||||
if (v < 1) return `${Math.round(v * 1000)} Кбит/с`
|
||||
@@ -8,7 +9,8 @@ export function fmtRate(v: number): string {
|
||||
return `${Math.round(v)} Мбит/с`
|
||||
}
|
||||
|
||||
export function fmtGB(v: number): string {
|
||||
export function fmtGB(v: number | null | undefined): string {
|
||||
if (v == null) return "—"
|
||||
if (v >= 1000) return `${(v / 1000).toFixed(2)} ТБ`
|
||||
return `${v.toFixed(1)} ГБ`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { projectTrafficObservations } from "./traffic-observations.js"
|
||||
const at=Date.parse("2026-10-12T12:00:00Z"),sampledAt=new Date(at).toISOString(),expiresAt=new Date(at+90_000).toISOString()
|
||||
const row={rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal:0,txTotal:0,rxSeries:[0,0],txSeries:[0,0]}
|
||||
const obs={rxNow:true,txNow:true,rxSeries:[false,true],txSeries:[false,true],sampledAt,currentState:"current",expiresAt}
|
||||
assert.equal(projectTrafficObservations(row,at).rxNow,null)
|
||||
const known=projectTrafficObservations({...row,observations:obs},at);assert.equal(known.rxNow,0);assert.deepEqual(known.rxSeries,[null,0])
|
||||
const stale=projectTrafficObservations({...row,observations:obs},at+90_001);assert.equal(stale.rxNow,null);assert.equal(stale.observations?.currentState,"stale");assert.deepEqual(stale.rxSeries,[null,0])
|
||||
assert.equal(projectTrafficObservations({...row,observations:{...obs,currentState:"error"}},at).rxNow,null)
|
||||
for(const change of [{rxNow:-1},{observations:{...obs,sampledAt:"bad"}},{observations:{...obs,sampledAt:null}},{observations:{...obs,rxSeries:[]}}])assert.throws(()=>projectTrafficObservations({...row,...change},at))
|
||||
console.log("traffic observations nullable/zero/gaps/expiry guards tests ok")
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod"
|
||||
const timestamp = z.string().refine(v => Number.isFinite(Date.parse(v))).nullable()
|
||||
const rate = z.number().finite().nonnegative().nullable()
|
||||
const meta = z.object({ rxNow: z.boolean(), txNow: z.boolean(), rxSeries: z.array(z.boolean()), txSeries: z.array(z.boolean()),
|
||||
sampledAt: timestamp, expiresAt: timestamp.optional(), lastAttemptAt: timestamp.optional(), lastSucceededAt: timestamp.optional(), error: z.string().nullable().optional(),
|
||||
currentState: z.enum(["current", "unknown", "pending", "error", "stale", "disabled"]).optional() })
|
||||
export type TrafficObservationMeta = z.infer<typeof meta>
|
||||
const metrics = z.object({ rxNow: rate, txNow: rate, rxPeak: rate, txPeak: rate, rxTotal: rate, txTotal: rate,
|
||||
rxSeries: z.array(rate), txSeries: z.array(rate), observations: meta.optional() }).refine(r =>
|
||||
(!r.observations || r.observations.rxSeries.length === r.rxSeries.length && r.observations.txSeries.length === r.txSeries.length &&
|
||||
(!(r.observations.rxNow || r.observations.txNow || r.observations.rxSeries.some(Boolean) || r.observations.txSeries.some(Boolean)) || r.observations.sampledAt !== null)))
|
||||
/** Receipt of numeric legacy DTO values cannot establish an observed measurement. */
|
||||
export function projectTrafficObservations(raw: unknown, now = Date.now()) {
|
||||
const parsed = metrics.safeParse(raw)
|
||||
if (!parsed.success) throw new Error("Некорректные измерения трафика")
|
||||
const r = parsed.data, o = r.observations && { ...r.observations }
|
||||
if (o && o.expiresAt && Date.parse(o.expiresAt) < now && o.currentState === "current") o.currentState = "stale"
|
||||
const current = !o?.currentState || o.currentState === "current"
|
||||
const rxSeries = r.rxSeries.map((v, i) => o?.rxSeries[i] ? v : null)
|
||||
const txSeries = r.txSeries.map((v, i) => o?.txSeries[i] ? v : null)
|
||||
return { observations: o, rxNow: current && o?.rxNow ? r.rxNow : null, txNow: current && o?.txNow ? r.txNow : null,
|
||||
rxSeries, txSeries, rxPeak: rxSeries.some(v => v !== null) ? r.rxPeak : null,
|
||||
txPeak: txSeries.some(v => v !== null) ? r.txPeak : null,
|
||||
rxTotal: rxSeries.some(v => v !== null) ? r.rxTotal : null, txTotal: txSeries.some(v => v !== null) ? r.txTotal : null }
|
||||
}
|
||||
Reference in new issue
Block a user