Skip to content

Commit

Live sockets back off until a connection holds: one the server accepts and drops at once (a Worker being replaced) no longer reconnects every second from every open tab

syntaqxcommitted Parent039fc76Browse files
5 files+38−30/5 viewed
+5−1
33 import type { ChatLiveEvent } from "@g1t/contracts";
44
55 import { backoff } from "../../lib/chat";
6+import { heldOpen } from "../../lib/notify-store";
67
78 /** How long a conversation's socket stays open after leaving it, while the next one opens. */
89 const HANDOFF_MS = 1500;
4445 return;
4546 }
4647 socket.current = ws;
48+ let openedAt: number | null = null;
4749 ws.onopen = () => {
4850 const again = opened;
4951 opened = true;
50− attempt = 0;
52+ openedAt = Date.now();
5153 setState("open");
5254 if (again) handlers.current.onReconnect();
5355 };
6163 ws.onclose = () => {
6264 if (socket.current === ws) socket.current = null;
6365 if (closed) return;
66+ // Only a connection that held starts the backoff over.
67+ if (heldOpen(openedAt)) attempt = 0;
6468 setState(opened ? "reconnecting" : "connecting");
6569 schedule();
6670 };
+8−1
1717 import * as syncProtocol from "y-protocols/sync";
1818 import * as Y from "yjs";
1919
20+import { heldOpen } from "../../lib/notify-store";
21+
2022 const MESSAGE_SYNC = 0;
2123 const MESSAGE_AWARENESS = 1;
2224 const MESSAGE_QUERY_AWARENESS = 3;
2931 status: LiveStatus = "connecting";
3032 private socket: WebSocket | null = null;
3133 private attempts = 0;
34+ /** When the current connection opened; the backoff starts over only once one holds. */
35+ private openedAt: number | null = null;
3236 private timer: ReturnType<typeof setTimeout> | null = null;
3337 private keepalive: ReturnType<typeof setInterval> | null = null;
3438 private stopped = false;
7478 socket.binaryType = "arraybuffer";
7579 this.socket = socket;
7680 socket.onopen = () => {
77− this.attempts = 0;
81+ this.openedAt = Date.now();
7882 // Our state vector: the room answers with what we lack, and asks for what it lacks.
7983 const encoder = encoding.createEncoder();
8084 encoding.writeVarUint(encoder, MESSAGE_SYNC);
121125 return;
122126 }
123127 this.setStatus("offline");
128+ // Only a connection that held starts the backoff over.
129+ if (heldOpen(this.openedAt)) this.attempts = 0;
130+ this.openedAt = null;
124131 const delay = Math.min(30_000, 500 * 2 ** this.attempts) + Math.random() * 500;
125132 this.attempts++;
126133 this.timer = setTimeout(() => this.connect(), delay);
+5−1
3030 dismissToast,
3131 markReadLocally,
3232 offerPush,
33+ heldOpen,
3334 reconnectDelay,
3435 titleWith,
3536 waitingCards,
409410 return;
410411 }
411412 socket = ws;
413+ let openedAt: number | null = null;
412414 ws.onopen = () => {
413− attempt = 0;
415+ openedAt = Date.now();
414416 set({ connected: true });
415417 sendState();
416418 };
424426 };
425427 ws.onclose = () => {
426428 if (socket === ws) socket = null;
429+ // Only a connection that held starts the backoff over.
430+ if (heldOpen(openedAt)) attempt = 0;
427431 set({ connected: false });
428432 schedule();
429433 };
+7−0
44 import type { ChatSidebarEntry, FeedCounts, FeedNotification } from "@g1t/contracts";
55
66 import {
7+ heldOpen,
78 MAX_TOASTS,
89 RECENT_KEPT,
910 WAITING_MS,
216217 // Acting on the newest puts the card away; the older one about it does not come back.
217218 assert.deepEqual(waitingCards(recent, new Set(["approval:ses_1:2000000"]), now, "acme"), []);
218219 });
220+
221+test("the backoff starts over only after a connection held for a while", () => {
222+ assert.equal(heldOpen(null, 50_000), false);
223+ assert.equal(heldOpen(45_000, 50_000), false, "dropped within seconds: keep backing off");
224+ assert.equal(heldOpen(40_000, 50_000), true);
225+});
+13−0
259259 }
260260
261261 /** Full jitter: a wait anywhere up to the doubled step, from a second up to half a minute. */
262+/**
263+ * How long a connection must stay open before its backoff starts over. A
264+ * socket the server accepts and drops at once (a Worker being replaced, a
265+ * room that can't start) would otherwise reconnect every second from every
266+ * open tab, and a busy network runs into the rate limit.
267+ */
268+export const STABLE_MS = 10_000;
269+
270+/** Whether a connection that opened at `openedAt` held long enough to start the backoff over. */
271+export function heldOpen(openedAt: number | null, now: number = Date.now()): boolean {
272+ return openedAt !== null && now - openedAt >= STABLE_MS;
273+}
274+
262275 export function reconnectDelay(attempt: number, random: () => number = Math.random): number {
263276 const ceiling = Math.min(30_000, 1_000 * 2 ** Math.max(0, Math.min(attempt, 10)));
264277 return Math.round(500 + random() * (ceiling - 500));