fix(gps): остановлен 429-шторм — throttle SSE gps_asset_moved (3 сек/asset) и клиентских инвалидаций (60 сек), polling позиций 60 сек, статусов 5 мин

Каждая точка ingest (~1/сек) broadcast'илась SSE всем клиентам орг., клиент
на каждое событие делал invalidateQueries без throttle → 2+ GET/сек → исчерпание
per-user бакета apiLimiter (1500/15мин) → все API устройства 429 (формы не грузились).
Модель теперь как у нормальных трекеров: точки пишутся в БД, карта читает по интервалу.
+4 unit-теста троттла (server/gps/sse-throttle.ts)
This commit is contained in:
2026-09-17 21:59:34 +03:00
parent e4f9a1992a
commit c1d5b1cbee
4 changed files with 133 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { apiRequest, queryClient } from '@/lib/queryClient';
import { useEvents } from '@/hooks/useEvents';
@@ -25,29 +25,64 @@ export function useGpsConfig() {
export function useGpsAssets() {
return useQuery<{ success: boolean; assets: GpsAsset[] }>({
queryKey: ['/api/gps/assets'],
refetchInterval: 30_000, // автообновление статусов онлайн/«Нет GPS»
refetchInterval: 300_000, // автообновление статусов онлайн/«Нет GPS» — не критично к минутной задержке
});
}
/**
* Текущие позиции активных объектов.
* Обновляются по SSE (gps_asset_moved / gps_asset_offline) + fallback polling 30 сек.
* Обновляются по SSE (gps_asset_moved / gps_asset_offline, серверный throttle 3 сек)
* + fallback polling 60 сек.
*/
export function useGpsPositions(enabled = true) {
return useQuery<{ success: boolean; positions: GpsPosition[] }>({
queryKey: ['/api/gps/assets/positions'],
refetchInterval: 30_000,
refetchInterval: 60_000,
enabled,
});
}
/** Подписка на живые GPS-события: инвалидирует позиции и список объектов */
/** Минимальный интервал между инвалидациями по SSE — иначе поток событий исчерпывает apiLimiter (429) */
const GPS_LIVE_INVALIDATE_INTERVAL_MS = 60_000;
/** Подписка на живые GPS-события: инвалидирует позиции и список объектов (trailing throttle 60 сек) */
export function useGpsLiveUpdates(enabled = true) {
const invalidate = useCallback(() => {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingRef = useRef(false);
const invalidateNow = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ['/api/gps/assets/positions'] });
queryClient.invalidateQueries({ queryKey: ['/api/gps/assets'] });
}, []);
const invalidate = useCallback(() => {
// Окно ещё не открыто — инвалидируем сразу и открываем окно
if (timerRef.current === null) {
invalidateNow();
timerRef.current = setTimeout(() => {
timerRef.current = null;
// Trailing: события, накопившиеся за окно, дают одну инвалидацию по выходу
if (pendingRef.current) {
pendingRef.current = false;
invalidateNow();
}
}, GPS_LIVE_INVALIDATE_INTERVAL_MS);
} else {
pendingRef.current = true;
}
}, [invalidateNow]);
useEffect(
() => () => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
pendingRef.current = false;
},
[]
);
useEvents(
enabled
? {