All files / web/src/lib/abacus/print ring-throttle.ts

100% Statements 38/38
100% Branches 5/5
100% Functions 2/2
100% Lines 38/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 392x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 14x 14x 14x 14x 14x 14x 10x 10x 10x 2x 2x 2x 12x 12x  
/**
 * Per-connection ring throttle (Phase 2a, #8.4).
 *
 * The service may ring in bursts (phase flurries, retries). Since a ring is
 * only a hint to re-read, collapsing a burst to one emit per ~250ms loses
 * nothing — the re-read sees the latest state either way, and the sanctioned
 * safety-net poll covers anything dropped.
 *
 * In-memory and therefore per-replica: with 3 app pods a burst can emit up to
 * 3× instead of 1×. That's an over-notification bound, not a correctness
 * issue, so it stays simple. Size is bounded by the number of connections
 * (small, per-user rows).
 */
 
export const RING_MIN_INTERVAL_MS = 250
 
const lastRingAt = new Map<string, number>()
 
/**
 * Claim the right to emit for this connection. Returns false when a ring
 * landed within the min interval — the caller should ack the ring but skip
 * the emit.
 */
export function claimRingSlot(
  connectionId: string,
  now: number = Date.now(),
  minIntervalMs: number = RING_MIN_INTERVAL_MS
): boolean {
  const last = lastRingAt.get(connectionId)
  if (last !== undefined && now - last < minIntervalMs) return false
  lastRingAt.set(connectionId, now)
  return true
}
 
/** Test hook — clears throttle state between cases. */
export function resetRingThrottle(): void {
  lastRingAt.clear()
}