102 lines
2.6 KiB
TypeScript
102 lines
2.6 KiB
TypeScript
/**
|
|
* Reconnection manager — exponential backoff with jitter,
|
|
* matching Rust reconnect patterns (1s base, 30s cap, 2^n multiplier).
|
|
*/
|
|
import {
|
|
RECONNECT_BASE_DELAY_MS,
|
|
RECONNECT_MAX_DELAY_MS,
|
|
RECONNECT_MAX_ATTEMPTS,
|
|
} from './constants';
|
|
|
|
export interface ReconnectState {
|
|
attempt: number;
|
|
nextDelay: number;
|
|
isReconnecting: boolean;
|
|
lastAttemptAt: number | null;
|
|
}
|
|
|
|
export class ReconnectManager {
|
|
private attempt = 0;
|
|
private isReconnecting = false;
|
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
private onReconnect: (() => void) | null = null;
|
|
|
|
/** Register the callback that fires when reconnection should be attempted. */
|
|
setCallback(onReconnect: () => void): void {
|
|
this.onReconnect = onReconnect;
|
|
}
|
|
|
|
/** Start reconnection cycle — exponential backoff with jitter. */
|
|
start(): void {
|
|
if (this.isReconnecting) return;
|
|
this.isReconnecting = true;
|
|
this.attempt = 0;
|
|
this.scheduleNext();
|
|
}
|
|
|
|
/** Stop reconnection cycle. */
|
|
stop(): void {
|
|
this.isReconnecting = false;
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
}
|
|
|
|
/** Notify that reconnection succeeded — resets state. */
|
|
succeed(): void {
|
|
this.attempt = 0;
|
|
this.isReconnecting = false;
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
}
|
|
|
|
/** Reset attempt counter and start a fresh reconnect cycle (used after visibility/network recovery). */
|
|
resetAndStart(): void {
|
|
this.attempt = 0;
|
|
this.isReconnecting = false;
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
this.start();
|
|
}
|
|
|
|
/** Notify that reconnection attempt failed — schedules next. */
|
|
fail(): void {
|
|
this.attempt++;
|
|
if (this.attempt >= RECONNECT_MAX_ATTEMPTS) {
|
|
this.stop();
|
|
return;
|
|
}
|
|
this.scheduleNext();
|
|
}
|
|
|
|
/** Calculate delay: base * 2^attempt, capped at max, with jitter. */
|
|
private getDelay(): number {
|
|
const exponential = RECONNECT_BASE_DELAY_MS * Math.pow(2, this.attempt);
|
|
const capped = Math.min(exponential, RECONNECT_MAX_DELAY_MS);
|
|
// Full jitter: random between 0 and capped (matches Rust LCG PRNG pattern)
|
|
const jitter = Math.random() * capped;
|
|
return jitter;
|
|
}
|
|
|
|
private scheduleNext(): void {
|
|
const delay = this.getDelay();
|
|
this.timer = setTimeout(() => {
|
|
this.onReconnect?.();
|
|
}, delay);
|
|
}
|
|
|
|
/** Get current reconnection state. */
|
|
getState(): ReconnectState {
|
|
return {
|
|
attempt: this.attempt,
|
|
nextDelay: this.isReconnecting ? this.getDelay() : 0,
|
|
isReconnecting: this.isReconnecting,
|
|
lastAttemptAt: null,
|
|
};
|
|
}
|
|
} |