Commit Graph
4 Commits
Author SHA1 Message Date
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl bae9f8edb0 fix(webui): connection-diff-loop compares ONLY level — strip detail timestamps
Adversarial review caught: HttpPostAdapter.describeStatus() embeds
HH:MM:SS in 'detail' (e.g. "上次推送 14:53:01"). Every webhook delivery
rotates that string every second under any active push — and the diff
loop's raw JSON.stringify compared everything, so it published on every
500ms tick under any non-idle webhook. Net effect: SSE pushed a full
connections frame at 2 Hz, exactly the polling the loop was meant to
replace, just on the push channel.

Add an optional pickComparable projector. server.ts caller passes one
that returns { uin, nickname, adapters[{ name, kind, status }] } — the
LEVEL only, not the live-rendered detail string. The SSE handler still
ships the FULL snapshot (including detail) to the WebUI; only the diff
key is pruned.

2 new tests pin the invariant: 10 detail-only ticks → ZERO publishes; a
real status-level transition publishes exactly once. Plus a throwing-
projector isolation test. 7/7 diff-loop tests green.
2026-06-24 22:55:51 +08:00
DrSmoothl f9ce49ac7e fix(webui): single-flight per resource in bindStateStream — ordering safety
Adversarial review caught: uneven snapshot latency can deliver a STALE
final frame. Concrete repro added as a test:
  - publish('processes') @ T=0 → flush A starts a slow snapshot (200ms)
  - publish('processes') @ T=60 → flush B fires at T=110 with a fast (10ms)
    snapshot → send B at T=120
  - flush A's slow snapshot resolves at T=250 → send A with snapshot data
    captured BEFORE B → client's LAST frame is stale.

Fix: at most one in-flight flush per resource. While a flush is pending,
new publishes set a  flag instead of starting an overlapping
flush; on completion the flag re-arms the debounce timer for exactly one
follow-up flush. Sustained publish streams therefore produce at most one
frame per (debounceMs + flushLatency).

Test 'serialises overlapping flushes' fails on the pre-fix code (last
frame = 'snap-T0' stale), passes on the fix (last frame = 'snap-T60'
fresh). Test 'coalesces N publishes during in-flight' verifies the
pending flag is set-like (5 publishes → 1 follow-up, not 5).

9/9 state-stream tests green; 432/432 core suite.
2026-06-24 22:44:09 +08:00
DrSmoothl a7522deb82 feat(webui): /api/state/stream SSE — push processes/qq-list/connections
Phase 3 of switching the three dashboard resources from REST polling to
SSE push.

- New pure helper packages/core/src/webui/state-stream.ts owns the
  debounce/snapshot/fan-out logic, decoupled from Hono so it's unit-testable
  with fake timers (7 tests cover initial-snapshot, single-publish coalesce,
  per-resource independent debounce, dispose-cancels-pending, snapshot
  rejection isolation, send() throw isolation).

- /api/state/stream handler in server.ts mirrors the /api/debug/stream
  pattern: ReadableStream + createFramePusher (drop-under-backpressure with
  the same {kind:'dropped'} marker), 15s heartbeats, abort-signal teardown.
  On connect: one frame per resource. After: a fresh-snapshot frame per
  StateBus invalidation, 50ms per-resource debounce.

- The endpoint 503s when stateBus is absent (build without state-wiring),
  so clients can still fall back to the REST endpoints; the existing
  GET /api/processes etc remain in place for initial fetch + reconcile.

44 test files, 426/426 core suite green.
2026-06-24 21:44:28 +08:00