/** * Reusable bounded-backoff reconnect / retry controller. * * Generic helper consumed by {@link WebRTCClient}'s ICE-restart loop and * available to any other genesis subsystem that needs a deterministic, * clock-injectable backoff sequence. Plain logic, no WebRTC coupling — * which keeps it trivial to unit test and reuse. */ /** * Policy describing the backoff sequence and exhaustion bound. */ export interface ReconnectPolicy { /** Backoff delays in milliseconds. The last entry is clamped on overflow. */ readonly backoffMs: readonly number[]; /** Maximum number of attempts before {@link ReconnectController.next} returns null. */ readonly maxAttempts: number; } /** * One advancing step in the backoff sequence. */ export interface ReconnectStep { /** 1-indexed attempt number. */ readonly attempt: number; /** Delay before this attempt should fire, in milliseconds. */ readonly delayMs: number; } /** * Stateful controller that walks a {@link ReconnectPolicy}'s backoff * sequence. Calling {@link ReconnectController.next} advances internal * state; calling {@link ReconnectController.reset} returns to attempt 0. */ export class ReconnectController { private readonly policy: ReconnectPolicy; private readonly clock: { now: () => number }; private currentAttempts = 0; private lastEventAt = 0; public constructor( policy: ReconnectPolicy, clock: { now: () => number } = { now: () => Date.now() } ) { if (policy.backoffMs.length === 0) { throw new RangeError( "ReconnectController: backoffMs must contain at least one step" ); } if (policy.maxAttempts < 1) { throw new RangeError( "ReconnectController: maxAttempts must be >= 1" ); } this.policy = policy; this.clock = clock; } /** * Returns the delay for the next attempt; advances internal state. * Returns `null` once `maxAttempts` is reached. */ public next(): ReconnectStep | null { if (this.currentAttempts >= this.policy.maxAttempts) { return null; } const idx = Math.min( this.currentAttempts, this.policy.backoffMs.length - 1 ); const delayMs = this.policy.backoffMs[idx] as number; this.currentAttempts += 1; this.lastEventAt = this.clock.now(); return { attempt: this.currentAttempts, delayMs }; } /** Reset to attempt 0. */ public reset(): void { this.currentAttempts = 0; this.lastEventAt = 0; } /** Attempts consumed so far. */ public get attempts(): number { return this.currentAttempts; } /** Timestamp of the last `next()` call, sourced from the injected clock. */ public get lastAttemptAt(): number { return this.lastEventAt; } }