93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}
|