/** * Decorrelated-jitter backoff, shared by the transports that reconnect. * * @module utils/reconnectPolicy */ /** * The backoff curve and jitter for a reconnecting transport. * * **Why this exists.** `powerWebSocketClient` and `powerWebTransportClient` each * carried a private `_nextReconnectDelay()` that was character-for-character * identical — the same AWS "Exponential Backoff and Jitter" (2015) curve, the * same `* 3` growth, the same `Math.floor`. Two copies of one state machine is * two places for the same off-by-one, and neither had a unit test of its own: * the curve was only ever exercised through a live transport, so a regression in * it would surface as a flaky integration test rather than a red unit. * * **Scope, deliberately narrow.** This is the *curve* — the part that is purely * computational and therefore cleanly testable. It is **not** the attempt cap, * the elapsed bound, or the timer lifecycle: those live in each transport * because the two differ on purpose. `powerWebSocketClient` records * `_reconnectExhaustedBy` and does not count an exhausted attempt; * `powerWebTransportClient` counts it and records nothing. Unifying that would * be a behaviour change dressed as a refactor, and it is not what this is for. * * The audit that asked for this described "four copies". There are **two**: * `powerRTCChannel` has no reconnect logic at all — a data channel does not * reconnect, the `RTCPeerConnection` beneath it does, at a layer this class does * not own — and `powerSseAdapter` is server-side, where the browser's * `EventSource` performs the reconnection. Recorded here so the next reader does * not go looking for the other two. * * @example * const policy = new ReconnectPolicy({ baseMs: 500, maxMs: 30_000 }); * policy.next(); // a delay in [250, 750] * policy.next(); // a delay in [750, 2250] * policy.reset(); // back to the base, for a fresh connection */ export class ReconnectPolicy { /** * @param {object} options * @param {number} [options.baseMs=500] - The first delay, and the value the * cursor resets to. The curve grows from here. * @param {number} [options.maxMs=30000] - Ceiling for both the cursor and the * returned delay. * @param {() => number} [options.random=Math.random] - The jitter source. * Injectable so a test can pin the curve rather than assert on a range. */ constructor(options?: { baseMs?: number | undefined; maxMs?: number | undefined; random?: (() => number) | undefined; }); _baseMs: number; _maxMs: number; _random: () => number; /** * The cursor, in ms. `null` until the first `next()`, after which the first * delay is drawn from `[baseMs/2, baseMs]`. * nothing. * @type {number|null} */ _cursor: number | null; /** * The base delay, in ms. * @returns {number} */ get baseMs(): number; /** * The ceiling, in ms. * @returns {number} */ get maxMs(): number; /** * The current cursor, in ms, or the base before the first `next()`. * * Exposed for diagnostics and for the transports' own `stats()`. Reading it * does not advance the curve. * @returns {number} */ get cursorMs(): number; /** * The next delay, in ms, advancing the curve. * * Decorrelated jitter, per AWS "Exponential Backoff and Jitter" (2015): the * delay is drawn from `[cursor/2, cursor]` rather than from `[0, cursor]`, * which decorrelates far better than full jitter under load. That matters * because a server restart otherwise produces a synchronised reconnect * stampede from every client at once — full jitter spreads the retries but * still lets a cohort re-form, whereas decorrelated jitter does not. * * The cursor then grows by `* 3` and is clamped, so the *next* draw is from a * wider band. Growth is applied after the draw, so the first delay is drawn * from `[baseMs/2, baseMs]` (i.e. `baseMs` only when the jitter source * returns 1), not exactly `baseMs`. * * @returns {number} An integer number of ms, in `(0, maxMs]`. */ next(): number; /** * Return the cursor to the base, for a connection that succeeded. * * **Call this on a successful open, not only on construction.** A transport * that reconnects after a long outage would otherwise resume from a cursor * grown by the outage's failed attempts, so the first delay after a *good* * connection is still seconds long. Resetting on success is what makes the * curve track the current conditions rather than the worst it has seen. * @returns {void} */ reset(): void; }