## Summary
Fixes#483 — navigating away from the live page while matrix/hop
animations are running throws `TypeError: Cannot read properties of null
(reading 'addLayer')`.
## Root Cause
`destroy()` sets `animLayer = null` and `pathsLayer = null`, but
in-flight `requestAnimationFrame` callbacks continue executing and
attempt to call `.addTo(animLayer)` or `.removeLayer()` on the now-null
references.
The entry guards at the top of `drawMatrixLine()` and
`drawAnimatedLine()` only protect the initial call — not the rAF
continuation loops inside `tick()`, `fadeOut()`, `animateLine()`, and
`animateFade()`.
## Fix
Added null-guards (`if (!animLayer || !pathsLayer) return`) at the top
of all four rAF callback functions in `live.js`:
1. **`tick()`** (line ~2203) — matrix animation main loop
2. **`fadeOut()`** (line ~2253) — matrix animation fade-out
3. **`animateLine()`** (line ~2302) — standard line animation main loop
4. **`animateFade()`** (line ~2337) — standard line fade-out
This pattern is already used elsewhere in the file (e.g., line 1873,
1886) for the same purpose.
## Testing
- All unit tests pass (`npm test` — 0 failures)
- Go server tests pass (`cmd/server` + `cmd/ingestor`)
- Change is defensive only (early return on null) — no behavioral change
when layers exist
---------
Co-authored-by: you <you@example.com>
## Summary
Replace all `setInterval`-based animations in `live.js` with
`requestAnimationFrame` loops and add a concurrency cap to prevent
unbounded animation accumulation under high packet throughput.
Fixes#384
## Problem
Under high throughput (≥5 packets/sec), the live map accumulated
unbounded `setInterval` timers:
- `pulseNode()`: 26ms interval per pulse ring
- `drawAnimatedLine()`: 33ms interval per hop line + 52ms nested
interval for fade-out
- Ghost hop pulse: 600ms interval per ghost marker
At 5 pkts/sec × 3 hops = **15+ concurrent intervals**, climbing without
limit. This caused UI jank, rising CPU usage, and potential memory leaks
from leaked Leaflet markers.
## Changes
### `public/live.js`
| Function | Before | After |
|----------|--------|-------|
| `pulseNode()` | `setInterval` (26ms) + `setTimeout` safety |
`requestAnimationFrame` loop, self-terminates at 2s or opacity ≤ 0 |
| `drawAnimatedLine()` | `setInterval` (33ms) for line + nested
`setInterval` (52ms) for fade | Two `requestAnimationFrame` loops (line
advance + fade-out) |
| Ghost hop pulse | `setInterval` (600ms) + `setTimeout` (3s) |
`requestAnimationFrame` loop with 3s expiry |
| `animatePath()` | No concurrency limit | Returns early when
`activeAnims >= MAX_CONCURRENT_ANIMS` (20) |
### `public/index.html`
- Cache buster version bump
### `test-live-anims.js` (new)
- 7 tests verifying:
- No `setInterval` in `pulseNode`, `drawAnimatedLine`, or `animatePath`
- `MAX_CONCURRENT_ANIMS` defined and set to 20
- Concurrency check present in `animatePath`
- No stale `setInterval` in animation hot paths
## Complexity & Scale
- **Time complexity**: O(1) per animation frame (no change in per-frame
work)
- **Concurrency**: Hard-capped at 20 simultaneous animations (previously
unbounded)
- **At 5 pkts/sec, 3 hops**: Excess animations silently dropped instead
of accumulating timers
- **rAF benefit**: Browser coalesces all animations into single paint
cycle; paused tabs stop animating automatically
## Test Results
```
=== Animation interval elimination ===
✅ pulseNode does not use setInterval
✅ drawAnimatedLine does not use setInterval
✅ ghost hop pulse does not use setInterval
=== Concurrency cap ===
✅ MAX_CONCURRENT_ANIMS is defined
✅ MAX_CONCURRENT_ANIMS is set to 20
✅ animatePath checks MAX_CONCURRENT_ANIMS before proceeding
=== Safety: no stale setInterval in animation functions ===
✅ no setInterval remains in animation hot path
7 passed, 0 failed
```
All existing tests pass (packet-filter: 62, aging: 29, frontend-helpers:
241).
## Performance Proof (Rule 0 compliance)
Benchmark: `node test-anim-perf.js` — simulates timer/animation
accumulation under realistic throughput.
### Timer count: old (setInterval) vs new (rAF + cap)
| Scenario | Old model (peak concurrent timers) | New model (peak
concurrent animations) |
|----------|-----------------------------------:|---------------------------------------:|
| 5 pkt/s × 3 hops, 30s sustained | **123** | **20** |
| 5 pkt/s × 3 hops, 5min sustained | **123** | **20** |
| 20 pkt/s × 3 hops, 10s burst | **246** | **20** |
**Before:** Each hop spawns 3 `setInterval` timers (pulse 26ms, line
33ms, fade 52ms) that live 0.6–2s each. At 5 pkt/s × 3 hops = 15
timers/sec, peak concurrent timers reach **123** (limited only by timer
lifetime, not by any cap). Under burst traffic (20 pkt/s), this climbs
to **246+**.
**After:** `MAX_CONCURRENT_ANIMS = 20` hard-caps active animations.
Excess packets are silently dropped. rAF loops replace all `setInterval`
calls, coalescing into single paint cycles. Peak concurrent animations:
**always ≤ 20**, regardless of throughput or duration.
---------
Co-authored-by: you <you@example.com>
Co-authored-by: Kpa-clawbot <kpabap+clawdbot@gmail.com>