返回 CodeWhale
connection-state.ts
根目录 / web / lib / connection-state.ts
1 /**
2 * connection-state.ts — the typed connection model behind the signed-in
3 * shell's offline/reconnect banner (`components/connection-banner.tsx`).
4 *
5 * Pure: a reducer over browser and probe events, plus the retry schedule.
6 * The component owns timers and `fetch`; this module owns the meaning of
7 * each state so it can be unit-tested without a DOM and so no surface can
8 * invent a fourth kind of "online".
9 *
10 * online — the browser reports a network and the last probe (if any)
11 * succeeded. No banner.
12 * offline — `navigator.onLine` is false. Banner; probes wait for the
13 * browser's `online` event before retrying.
14 * reconnecting — the browser reports a network but the server has not
15 * answered a probe yet. Banner with the attempt count;
16 * probes retry on a capped exponential backoff.
17 * degraded — a probe failed while the browser still reports a network.
18 * Banner; the next probe is scheduled.
19 */
20
21 export type ConnectionStatus = "online" | "offline" | "reconnecting" | "degraded";
22
23 export interface ConnectionState {
24 status: ConnectionStatus;
25 /** Failed or pending probes since the last success. 0 while online. */
26 attempt: number;
27 /** Epoch ms of the last completed probe, success or failure. */
28 lastCheckedAt: number | null;
29 /** True for one render cycle after a recovery, so the banner can say so. */
30 restored: boolean;
31 }
32
33 export type ConnectionEvent =
34 | { type: "browser-online" }
35 | { type: "browser-offline" }
36 | { type: "probe-start" }
37 | { type: "probe-ok"; at: number }
38 | { type: "probe-failed"; at: number }
39 | { type: "restored-seen" };
40
41 export const INITIAL_CONNECTION_STATE: ConnectionState = {
42 status: "online",
43 attempt: 0,
44 lastCheckedAt: null,
45 restored: false,
46 };
47
48 /** Base delay and ceiling for the reconnect schedule, in milliseconds. */
49 export const RETRY_BASE_MS = 2_000;
50 export const RETRY_MAX_MS = 30_000;
51
52 /**
53 * Delay before the next probe for a given attempt count: 2s, 4s, 8s, 16s,
54 * then 30s forever. Attempt 0 (first failure) retries after the base delay.
55 */
56 export function backoffDelayMs(attempt: number): number {
57 const exponent = Math.max(0, Math.min(attempt, 10));
58 return Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** exponent);
59 }
60
61 export function nextConnectionState(
62 state: ConnectionState,
63 event: ConnectionEvent,
64 ): ConnectionState {
65 switch (event.type) {
66 case "browser-offline":
67 return { ...state, status: "offline", restored: false };
68
69 case "browser-online":
70 // The browser thinks it has a network; the server has not confirmed.
71 if (state.status === "online") return state;
72 return { ...state, status: "reconnecting", restored: false };
73
74 case "probe-start":
75 if (state.status === "offline") return state;
76 return {
77 ...state,
78 status: state.status === "online" ? "online" : "reconnecting",
79 };
80
81 case "probe-ok": {
82 // A success that lands after the browser went `offline` belongs to a
83 // stale probe: the browser emits no second event, so honoring it would
84 // hide the banner with no network to back it. Record the check, keep
85 // the banner.
86 if (state.status === "offline") {
87 return { ...state, lastCheckedAt: event.at };
88 }
89 const wasDown = state.status !== "online";
90 return {
91 status: "online",
92 attempt: 0,
93 lastCheckedAt: event.at,
94 restored: wasDown,
95 };
96 }
97
98 case "probe-failed":
99 if (state.status === "offline") {
100 return { ...state, lastCheckedAt: event.at };
101 }
102 return {
103 status: "degraded",
104 attempt: state.attempt + 1,
105 lastCheckedAt: event.at,
106 restored: false,
107 };
108
109 case "restored-seen":
110 return state.restored ? { ...state, restored: false } : state;
111 }
112 }
113
114 /** Whether a probe should be scheduled from this state. */
115 export function shouldProbe(state: ConnectionState): boolean {
116 return state.status === "reconnecting" || state.status === "degraded";
117 }
118
118 lines TYPESCRIPT