2 Commits

Author SHA1 Message Date
Joey Krug
9b19b945d7 fix: address queue drain review feedback 2026-03-20 19:08:34 -04:00
Joey Krug
41b6372ec0 fix: drain inbound debounce buffer and followup queues before SIGUSR1 reload
When config.patch triggers a SIGUSR1 restart, two in-memory message
buffers were silently wiped:
1. Per-channel inbound debounce buffers (closure-local Map + setTimeout)
2. Followup queues (global Map of pending session messages)

This caused inbound messages received during the debounce window to be
permanently lost on config-triggered gateway restarts.

Fix:
- Add a global registry of inbound debouncers so they can be flushed
  collectively during restart. Each createInboundDebouncer() call now
  auto-registers in a shared Symbol.for() map, with a new flushAll()
  method that immediately processes all buffered items.
- Add flushAllInboundDebouncers() which iterates the global registry
  and forces all debounce timers to fire immediately.
- Add waitForFollowupQueueDrain() which polls the FOLLOWUP_QUEUES map
  until all queues finish processing (or timeout).
- Hook both into the SIGUSR1 restart flow in run-loop.ts: before
  markGatewayDraining(), flush all debouncers first (pushing buffered
  messages into the followup queues), then wait up to 5s for the
  followup drain loops to process them.

The ordering is critical: flush debouncers → wait for followup drain →
then mark draining. This ensures messages that were mid-debounce get
delivered to sessions before the gateway reinitializes.

Tests:
- flushAllInboundDebouncers: flushes multiple registered debouncers,
  returns count, deregisters after flush
- createInboundDebouncer.flushAll: flushes all keys in a single debouncer
- waitForFollowupQueueDrain: immediate return when empty, waits for
  drain, returns not-drained on timeout, counts draining queues
- run-loop: SIGUSR1 calls flush before markGatewayDraining, skips
  followup wait when no debouncers had buffered messages, logs warning
  on followup drain timeout
2026-03-20 19:08:34 -04:00