feat(frontend): исправлен график
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import type { AudioSample } from "../../../entities/audioSample/model/types";
|
||||
import { env } from "../../../shared/config/env";
|
||||
import { RingBuffer } from "../../../shared/lib/ringBuffer";
|
||||
import type { WsStatus } from "./types";
|
||||
import { LiveWsClient } from "../lib/liveWsClient";
|
||||
import { parseAndValidateMessage } from "../lib/parseAndValidate";
|
||||
@@ -12,8 +11,6 @@ type LiveStreamState = {
|
||||
|
||||
latest: AudioSample | null;
|
||||
|
||||
// snapshot for UI
|
||||
// storage is ring buffer inside module
|
||||
history60s: AudioSample[];
|
||||
peakHoldDb3s: number | null;
|
||||
|
||||
@@ -22,13 +19,24 @@ type LiveStreamState = {
|
||||
loadLatest: (limit?: number) => Promise<void>;
|
||||
};
|
||||
|
||||
const HISTORY_POINTS = 600; // 60s * 10Hz
|
||||
const PEAK_WINDOW_MS = 3000;
|
||||
const WINDOW_MS = 60_000;
|
||||
const PEAK_WINDOW_MS = 3_000;
|
||||
|
||||
const historyRing = new RingBuffer<AudioSample>(HISTORY_POINTS);
|
||||
let peakWindow: AudioSample[] = []; // time-based window
|
||||
// safety cap для worst-case 60Hz: ~3600 точек за 60 секунд
|
||||
// (нужен на случай странных таймстампов/скачков времени)
|
||||
const MAX_POINTS_60S = 60 * 60;
|
||||
|
||||
let historyWindow: AudioSample[] = [];
|
||||
let peakWindow: AudioSample[] = [];
|
||||
let client: LiveWsClient | null = null;
|
||||
|
||||
function trimByTime(arr: AudioSample[], cutoffMs: number): AudioSample[] {
|
||||
// shift в цикле ок для ~3600 элементов; если захочешь — можно оптимизировать индексом
|
||||
while (arr.length && arr[0]!.timeMs < cutoffMs) arr.shift();
|
||||
if (arr.length > MAX_POINTS_60S) arr = arr.slice(-MAX_POINTS_60S);
|
||||
return arr;
|
||||
}
|
||||
|
||||
function computePeakDb(windowSamples: AudioSample[]): number | null {
|
||||
if (windowSamples.length === 0) return null;
|
||||
let max = -Infinity;
|
||||
@@ -36,7 +44,7 @@ function computePeakDb(windowSamples: AudioSample[]): number | null {
|
||||
return Number.isFinite(max) ? max : null;
|
||||
}
|
||||
|
||||
export const useLiveStreamStore = create<LiveStreamState>()((set, get) => ({
|
||||
export const useLiveStreamStore = create<LiveStreamState>()((set) => ({
|
||||
status: "disconnected",
|
||||
lastMessageAt: null,
|
||||
|
||||
@@ -55,22 +63,19 @@ export const useLiveStreamStore = create<LiveStreamState>()((set, get) => ({
|
||||
|
||||
const sample = parsed.sample;
|
||||
|
||||
// 60s ring
|
||||
historyRing.push(sample);
|
||||
// 60s time window (по времени, а не по "600 точек")
|
||||
historyWindow.push(sample);
|
||||
historyWindow = trimByTime(historyWindow, sample.timeMs - WINDOW_MS);
|
||||
|
||||
// 3s peak window
|
||||
const now = Date.now();
|
||||
// 3s peak-hold window (тоже по времени сэмпла)
|
||||
peakWindow.push(sample);
|
||||
const cutoff = now - PEAK_WINDOW_MS;
|
||||
while (peakWindow.length && peakWindow[0]!.timeMs < cutoff) {
|
||||
peakWindow.shift();
|
||||
}
|
||||
peakWindow = trimByTime(peakWindow, sample.timeMs - PEAK_WINDOW_MS);
|
||||
|
||||
set({
|
||||
latest: sample,
|
||||
history60s: historyRing.toArray(),
|
||||
history60s: historyWindow.slice(), // копия для UI
|
||||
peakHoldDb3s: computePeakDb(peakWindow),
|
||||
lastMessageAt: now,
|
||||
lastMessageAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -82,13 +87,11 @@ export const useLiveStreamStore = create<LiveStreamState>()((set, get) => ({
|
||||
client?.close();
|
||||
client = null;
|
||||
peakWindow = [];
|
||||
// historyRing intentionally kept:
|
||||
// UI/график не прыгает при дисконнекте
|
||||
// historyWindow оставляем: график не должен "прыгать" при reconnect
|
||||
set({ status: "disconnected" });
|
||||
},
|
||||
|
||||
loadLatest: async (limit = 100) => {
|
||||
// optional warm-up for chart after refresh
|
||||
const url = `${env.apiUrl.replace(/\/$/, "")}/api/v1/audio/latest?limit=${limit}`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
@@ -99,12 +102,19 @@ export const useLiveStreamStore = create<LiveStreamState>()((set, get) => ({
|
||||
rms_db: number;
|
||||
freq_hz: number;
|
||||
}>;
|
||||
|
||||
for (const item of raw) {
|
||||
const parsed = parseAndValidateMessage(JSON.stringify(item));
|
||||
if (!parsed.ok) continue;
|
||||
historyRing.push(parsed.sample);
|
||||
historyWindow.push(parsed.sample);
|
||||
}
|
||||
set({ history60s: historyRing.toArray() });
|
||||
|
||||
// после warm-up тоже режем по времени "последней точки"
|
||||
const last = historyWindow.at(-1);
|
||||
if (last)
|
||||
historyWindow = trimByTime(historyWindow, last.timeMs - WINDOW_MS);
|
||||
|
||||
set({ history60s: historyWindow.slice() });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user