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:
@@ -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
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user