gitdataai/src/ws/reconnect.ts

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,
};
}
}