src/ws/backoff.ts
v0.3.0 · 2.1 KB
// Reconnect backoff policy.
/** Close code 1008 "policy violation": in practice, too many WS connections from the IP. */
export const WS_CLOSE_POLICY_VIOLATION = 1008;
export interface ReconnectPolicy {
/** First delay. Default 1 000 ms. */
baseMs: number;
/** Multiplier after every attempt. Default 2. */
factor: number;
/** Delay cap. Default 30 000 ms. */
maxMs: number;
/**
* Minimum delay after close code 1008. Default 60 000 ms. Lingering connections (for example from
* previous dev-server restarts) hold their slot on the HL side ~60 s; short retries only add more
* lingering connections.
*/
policyViolationPauseMs: number;
/**
* The attempt counter resets only after the socket stayed open this long with ALL subscriptions
* acknowledged and no server error. Default 8 000 ms; values up to 30 s are also reasonable.
*/
stableMs: number;
/** Attempts before giving up. Default Infinity: a long-running bot must never stop reconnecting. */
maxAttempts: number;
}
export const DEFAULT_RECONNECT_POLICY: Readonly<ReconnectPolicy> = Object.freeze({
baseMs: 1_000,
factor: 2,
maxMs: 30_000,
policyViolationPauseMs: 60_000,
stableMs: 8_000,
maxAttempts: Number.POSITIVE_INFINITY,
});
/**
* Delay before reconnect attempt number `attempt` (0-based): 1 s -> x2 -> 30 s cap, and never less
* than 60 s after close code 1008.
*
* Why the counter must not reset on `open`: HL sometimes accepts a socket and immediately closes it
* with 1008, and an ack of one subscription can arrive together with `channel:error` for another;
* resetting on `open` or on the first ack gives an endless reconnect every second.
*/
export function reconnectDelay(
attempt: number,
closeCode: number | undefined,
policy: Readonly<ReconnectPolicy> = DEFAULT_RECONNECT_POLICY,
): number {
const n = Math.max(0, Math.floor(attempt));
const exp = policy.baseMs * Math.pow(policy.factor, n);
let delay = Number.isFinite(exp) ? Math.min(exp, policy.maxMs) : policy.maxMs;
if (closeCode === WS_CLOSE_POLICY_VIOLATION) delay = Math.max(delay, policy.policyViolationPauseMs);
return delay;
}