Skip to main content

max / makenotwork

4.3 KB · 122 lines History Blame Raw
1 import assert from "node:assert/strict";
2 import { test } from "node:test";
3
4 import {
5 MAX_RECONNECT_ATTEMPTS,
6 MAX_RECONNECT_DELAY_MS,
7 ReconnectPolicy,
8 reconnectDelayMs,
9 } from "./reconnect.logic.ts";
10
11 /** Pin the jitter to the middle of its range, giving the base delay exactly. */
12 const noJitter = () => 0.5;
13
14 test("delay doubles per attempt", () => {
15 assert.equal(reconnectDelayMs(0, noJitter), 1_000);
16 assert.equal(reconnectDelayMs(1, noJitter), 2_000);
17 assert.equal(reconnectDelayMs(2, noJitter), 4_000);
18 assert.equal(reconnectDelayMs(3, noJitter), 8_000);
19 });
20
21 test("delay is capped", () => {
22 assert.equal(reconnectDelayMs(6, noJitter), MAX_RECONNECT_DELAY_MS);
23 assert.equal(reconnectDelayMs(20, noJitter), MAX_RECONNECT_DELAY_MS);
24 // The exponent ceiling exists so a large attempt count cannot overflow into
25 // Infinity and produce a NaN delay.
26 assert.equal(reconnectDelayMs(1e6, noJitter), MAX_RECONNECT_DELAY_MS);
27 assert.ok(Number.isFinite(reconnectDelayMs(1e6, noJitter)));
28 });
29
30 test("jitter stays within twenty percent either side", () => {
31 for (const attempt of [0, 1, 2, 3, 4, 5, 6, 10]) {
32 const base = Math.min(1000 * 2 ** Math.min(attempt, 6), MAX_RECONNECT_DELAY_MS);
33 for (const r of [0, 0.25, 0.5, 0.75, 0.999]) {
34 const delay = reconnectDelayMs(attempt, () => r);
35 assert.ok(
36 delay >= base * 0.8 - 1 && delay <= base * 1.2 + 1,
37 `attempt ${attempt} with random ${r} gave ${delay}, outside 20% of ${base}`,
38 );
39 }
40 }
41 });
42
43 test("jitter never pushes the delay past the documented cap", () => {
44 // +20% on a capped base is ~72s; the result must still honor MAX_RECONNECT_DELAY_MS.
45 for (const attempt of [6, 10, 20, 1e6]) {
46 assert.equal(reconnectDelayMs(attempt, () => 0.999), MAX_RECONNECT_DELAY_MS);
47 }
48 });
49
50 test("jitter actually spreads clients out", () => {
51 // The point of jitter is that a fleet reconnecting after one deploy does not
52 // arrive in lockstep. Identical delays would defeat it.
53 const low = reconnectDelayMs(3, () => 0);
54 const high = reconnectDelayMs(3, () => 0.999);
55 assert.ok(high - low > 1_000, `spread was only ${high - low}ms`);
56 });
57
58 test("delay is never negative", () => {
59 assert.ok(reconnectDelayMs(0, () => 0) >= 0);
60 });
61
62 test("policy backs off then gives up", () => {
63 const policy = new ReconnectPolicy(noJitter);
64
65 for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) {
66 const action = policy.fail();
67 assert.equal(action.kind, "retry", `attempt ${i} should still retry`);
68 }
69
70 assert.equal(policy.fail().kind, "give-up");
71 });
72
73 test("a success resets the streak", () => {
74 // A long-lived tab reconnects fine at every deploy. It must never accumulate
75 // its way to the give-up ceiling across weeks of successful reconnects.
76 const policy = new ReconnectPolicy(noJitter);
77
78 for (let deploy = 0; deploy < 50; deploy++) {
79 const action = policy.fail();
80 assert.equal(action.kind, "retry");
81 assert.equal(action.kind === "retry" && action.delayMs, 1_000, "always the first-step delay");
82 policy.succeed();
83 }
84
85 assert.equal(policy.attempt, 0);
86 });
87
88 test("delays escalate across consecutive failures", () => {
89 const policy = new ReconnectPolicy(noJitter);
90 const delays: number[] = [];
91 for (let i = 0; i < 4; i++) {
92 const action = policy.fail();
93 if (action.kind === "retry") delays.push(action.delayMs);
94 }
95 assert.deepEqual(delays, [1_000, 2_000, 4_000, 8_000]);
96 });
97
98 test("regaining focus or network skips the accumulated wait", () => {
99 // Waiting out a 60s backoff after the laptop opens makes chat look broken for
100 // a minute for a reason that has nothing to do with the server.
101 const policy = new ReconnectPolicy(noJitter);
102 for (let i = 0; i < 5; i++) policy.fail();
103 assert.ok(policy.attempt > 0);
104
105 policy.resetForImmediateRetry();
106
107 assert.equal(policy.attempt, 0);
108 const action = policy.fail();
109 assert.equal(action.kind === "retry" && action.delayMs, 1_000);
110 });
111
112 test("giving up is sticky until something resets it", () => {
113 const policy = new ReconnectPolicy(noJitter);
114 for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) policy.fail();
115
116 assert.equal(policy.fail().kind, "give-up");
117 assert.equal(policy.fail().kind, "give-up", "still given up");
118
119 policy.resetForImmediateRetry();
120 assert.equal(policy.fail().kind, "retry", "user action can revive it");
121 });
122