// Pure AIMD (additive-increase / multiplicative-decrease) math for the // audio-check probe cadence. Kept free of IO/logging so the curve is unit // testable in isolation; runAudioCheckedYtdlp owns the surrounding state, // logging, and the actual setTimeout. // // The live interval starts at the configured value (the ceiling). A malformed // checkpoint multiplies it down toward `floorMs` (fast tightening); a run of // clean checkpoints steps it back up toward the ceiling (slow relaxation). This // mirrors TCP congestion control, inverted for our purpose: probe harder the // moment a source misbehaves, ease off only once it has proven stable again. export type CadenceParams = { // Configured interval — the ceiling and starting point; recovery never // exceeds it. ceilingMs: number; // Lower bound the multiplicative decrease clamps to. floorMs: number; // Multiplicative-decrease factor, expected in (0, 1]. A factor >= 1 (or an // interval already at the floor) makes backoff a no-op. factor: number; // Additive-increase step applied on recovery. recoverStepMs: number; // Consecutive clean checkpoints required before each recovery step. recoverAfterClean: number; }; // Multiplicative decrease on a malformed checkpoint. Returns the new interval, // clamped to the floor; returns `currentMs` unchanged when there's no room to // shrink (already at/below the floor, or a degenerate factor). export function backoffInterval(currentMs: number, p: CadenceParams): number { if (!(p.factor < 1) || currentMs <= p.floorMs) return currentMs; return Math.max(p.floorMs, Math.round(currentMs * p.factor)); } // Additive increase driven by a clean checkpoint. `cleanStreakBefore` is the // streak prior to this checkpoint; the checkpoint itself counts as one more. // Once the streak reaches `recoverAfterClean` the interval steps up by // `recoverStepMs` (capped at the ceiling) and the streak resets; otherwise the // interval is unchanged and the incremented streak carries forward. export function recoverInterval( currentMs: number, cleanStreakBefore: number, p: CadenceParams, ): { intervalMs: number; cleanStreak: number } { const streak = cleanStreakBefore + 1; if (streak < p.recoverAfterClean || currentMs >= p.ceilingMs) { return { intervalMs: currentMs, cleanStreak: streak }; } return { intervalMs: Math.min(p.ceilingMs, currentMs + p.recoverStepMs), cleanStreak: 0, }; } // Human-readable interval for log lines. Sub-second (test) intervals print as // milliseconds so they stay legible; whole-second production intervals print as // seconds. export function formatInterval(ms: number): string { return ms >= 1000 ? `${Math.round(ms / 1000)}s` : `${ms}ms`; }