Каждая точка ingest (~1/сек) broadcast'илась SSE всем клиентам орг., клиент на каждое событие делал invalidateQueries без throttle → 2+ GET/сек → исчерпание per-user бакета apiLimiter (1500/15мин) → все API устройства 429 (формы не грузились). Модель теперь как у нормальных трекеров: точки пишутся в БД, карта читает по интервалу. +4 unit-теста троттла (server/gps/sse-throttle.ts)
28 lines
1.5 KiB
TypeScript
28 lines
1.5 KiB
TypeScript
/**
|
||
* In-memory throttle для SSE-событий gps_asset_moved.
|
||
* Трекеры присылают точки ~1 раз в секунду; без ограничения каждый открытый
|
||
* клиент организации получает broadcast каждую секунду и инвалидирует запросы
|
||
* позиций/объектов, что исчерпывает per-user бакет apiLimiter (429).
|
||
* Запись позиции в БД не затрагивается — ограничивается только broadcast.
|
||
*/
|
||
|
||
const GPS_MOVED_SSE_MIN_INTERVAL_MS = 3_000;
|
||
const MAX_TRACKED_ASSETS = 1_000;
|
||
|
||
const lastPublishedAt = new Map<number, number>();
|
||
|
||
/** true — событие публиковать; false — пропустить (с последней публикации прошло меньше интервала) */
|
||
export function shouldPublishGpsAssetMoved(assetId: number, now = Date.now()): boolean {
|
||
const last = lastPublishedAt.get(assetId);
|
||
if (last !== undefined && now - last < GPS_MOVED_SSE_MIN_INTERVAL_MS) return false;
|
||
|
||
lastPublishedAt.set(assetId, now);
|
||
// Защита от неограниченного роста карты: чистим устаревшие записи при превышении лимита
|
||
if (lastPublishedAt.size > MAX_TRACKED_ASSETS) {
|
||
for (const [id, ts] of lastPublishedAt) {
|
||
if (now - ts >= GPS_MOVED_SSE_MIN_INTERVAL_MS) lastPublishedAt.delete(id);
|
||
}
|
||
}
|
||
return true;
|
||
}
|