Initial Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user