Files
worldmonitor/server/_shared
Elie Habib cd7d3b7501 perf(baseline): move temporal baseline for news+fires to server-side (#1194)
* perf(baseline): move temporal baseline for news+fires to server-side

Every browser client was calling record-baseline-snapshot (POST) +
get-temporal-baseline (GET) on every data refresh from 5 call sites.
With N concurrent users this created N identical writes and ~5N reads
per cycle — causing 429 rate limiting and statistically biased baselines.

Phase 1 moves news and satellite_fires to server-side computation:
- New ListTemporalAnomalies RPC reads counts from existing Redis keys
  (news:insights:v1, wildfire:fires:v1), computes anomalies against v2
  baselines, applies Welford update (1 sample/cycle), caches 15min
- Bootstrap hydration delivers anomalies on page load (zero extra calls)
- Client refreshes via RPC every 10min (1 cached call vs 5N before)
- Remaining 3 types (military_flights, vessels, ais_gaps) stay client-side
- Owner-guarded distributed lock prevents concurrent computation
- All reads/writes use prefix-aware getCachedJson/setCachedJson

Expected ~60% reduction in baseline-related Vercel invocations.

* fix(temporal): per-invocation lock owner and immediate refresh on cold cache

P1: When bootstrap has no temporal anomaly data (cold cache, expired
snapshot, fresh deploy), fire refreshTemporalBaseline() immediately
instead of waiting up to 10 minutes for the scheduled refresh.

P2: Generate lockOwner per invocation via makeLockOwner() instead of
once at module load. Prevents warm edge isolates from cross-releasing
each other's locks when one invocation outlives the 30s TTL.

* fix(temporal): use TTL-only lock instead of TOCTOU GET-then-DEL release

The non-atomic GET→check-owner→DEL release was vulnerable to a race
where the TTL expires between GET and DEL, allowing a new lock holder
to be evicted. Simplify by relying solely on the 30s TTL for lock
expiry — the computation completes well within that window.
2026-03-07 16:15:43 +04:00
..