Skip to content
markpaper

src/ws/backoff.ts

v0.3.0 · 2.1 KB

Download file
// 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;
}
All files