Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Retrying a read that gets no answer never becomes a loop (after 71274eeb): a refresh of the page already showing kept trying every 5 s for as long as it took, which is right while the browser is offline but not on a network that is up and never answers (an extension or proxy blocking g1t's .data requests, a security policy, broken DNS): the page sat on old data under "Can't reach g1t" forever and never showed what was wrong, and a tab out of view kept asking too. Now a refresh counts only the time it waited while the browser said it was online, and gives up after 2 minutes of that (REFRESH_PATIENCE_MS); offline still waits as long as it lasts. Nothing is retried from a tab out of view: a wait that is over holds until the tab is looked at. While the error page tries on its own (lib/reconnect.ts, 3 s, 6 s, 12 s, then every 30 s), reads are tried once each instead of retrying underneath it, so a broken network costs one request every 30 s on a visible error page and none on a hidden one. A real error (an answer that is an error) was already never retried and stops the error page's trying. keepTrying in lib/resilient-fetch.ts makes the call. Checked: apps/web tsc -b; npm test (985; new: offline waited out however long, up-and-silent gives up for a refresh and a click, one try while the error page tries).

G1t-Session:
ses_01m4zzpc96ect8d41mffgr1a45
syntaqxcommitted Parent71274eeBrowse files
3 files+88−180/3 viewed
+8−1
11 import { useEffect, useRef } from "react";
22 import { useRevalidator } from "react-router";
33
4+import { setErroring } from "./resilient-fetch.ts";
5+
46 /** The wait before the error page's try `attempt` (from 1): 3 s, 6 s, 12 s, then every 30 s. */
57 export function reconnectDelay(attempt: number): number {
68 return Math.min(3000 * 2 ** (attempt - 1), 30_000);
1012 * While `on` (the page fell to its error page because the network was
1113 * gone), loads the page again: a little later each time, at once when the
1214 * browser says it is back online or the tab comes back into view. Once a
13− * load arrives, the router drops the error and the page is back.
15+ * load arrives, the router drops the error and the page is back. Each try
16+ * is one request (lib/resilient-fetch.ts stands back meanwhile), so a
17+ * network that never answers costs a request every 30 s, not a loop; one
18+ * that answers with a real error shows it, and the trying stops.
1419 */
1520 export function useReconnect(on: boolean) {
1621 const revalidator = useRevalidator();
1823 latest.current = revalidator;
1924 useEffect(() => {
2025 if (!on) return;
26+ setErroring(true);
2127 let attempt = 0;
2228 let timer: ReturnType<typeof setTimeout> | undefined;
2329 const schedule = () => {
3844 window.addEventListener("online", now);
3945 document.addEventListener("visibilitychange", now);
4046 return () => {
47+ setErroring(false);
4148 clearTimeout(timer);
4249 window.removeEventListener("online", now);
4350 document.removeEventListener("visibilitychange", now);
+15−1
11 import assert from "node:assert/strict";
22 import test from "node:test";
33
4−import { isNetworkFailure, refreshesCurrent, retriable, retryDelay } from "./resilient-fetch.ts";
4+import { PATIENCE_MS, REFRESH_PATIENCE_MS, isNetworkFailure, keepTrying, refreshesCurrent, retriable, retryDelay } from "./resilient-fetch.ts";
55 import { reconnectDelay } from "./reconnect.ts";
66
77 const ORIGIN = "https://g1t.sh";
4141 assert.deepEqual([1, 2, 3, 4, 5, 9].map(retryDelay), [250, 500, 1000, 2000, 4000, 5000]);
4242 assert.deepEqual([1, 2, 3, 4, 8].map(reconnectDelay), [3000, 6000, 12000, 24000, 30000]);
4343 });
44+
45+test("a refresh waits out the browser being offline, however long", () => {
46+ assert.equal(keepTrying({ refresh: true, elapsedMs: 60 * 60_000, onlineMs: 0, erroring: false }, 5000), true);
47+});
48+
49+test("a network that is up and never answers is no blip: a refresh gives up, and so does a click", () => {
50+ assert.equal(keepTrying({ refresh: true, elapsedMs: 0, onlineMs: REFRESH_PATIENCE_MS - 5000, erroring: false }, 5000), true);
51+ assert.equal(keepTrying({ refresh: true, elapsedMs: 0, onlineMs: REFRESH_PATIENCE_MS - 4000, erroring: false }, 5000), false);
52+ assert.equal(keepTrying({ refresh: false, elapsedMs: PATIENCE_MS - 4000, onlineMs: 0, erroring: false }, 5000), false);
53+});
54+
55+test("while the error page tries on its own, a read is tried once", () => {
56+ assert.equal(keepTrying({ refresh: true, elapsedMs: 0, onlineMs: 0, erroring: true }, 250), false);
57+});
+65−16
88 * later each time, sooner once the browser says it is back online. While
99 * one is waiting, `useReconnecting` says so (components/connection-toast.tsx).
1010 *
11− * A refresh of the page already showing keeps trying for as long as it
12− * takes: the page stays as it is meanwhile, and a click elsewhere cancels
13− * it. A read for another page (a click) tries for `PATIENCE_MS`, then
14− * reaches the error page, which keeps trying on its own (lib/reconnect.ts).
11+ * A refresh of the page already showing keeps the page as it is while it
12+ * tries, and a click elsewhere cancels it. It waits out the browser being
13+ * offline for as long as that lasts, but not a network that is up and
14+ * still never answers (an extension or proxy blocking g1t, broken DNS):
15+ * that is no blip, so after `REFRESH_PATIENCE_MS` of it the error is shown.
16+ * A read for another page (a click) tries for `PATIENCE_MS`. Either way the
17+ * error page then keeps trying on its own, on its own slower schedule
18+ * (lib/reconnect.ts): while it shows, reads are not retried here as well.
19+ * Nothing is retried from a tab out of view; it waits to be looked at.
1520 * A read the router gave up on itself (its signal aborted) stops at once.
1621 * Writes are never sent again: one that failed may have arrived, and
1722 * sending it twice could do it twice.
2126 /** How long a read keeps trying; under lib/stuck.ts's `STUCK_NAVIGATION_MS`, so a click still lands on an error page that recovers rather than a whole-page load that cannot. */
2227 export const PATIENCE_MS = 45_000;
2328
29+/** How long a refresh keeps the page while the browser says it is online and nothing answers. */
30+export const REFRESH_PATIENCE_MS = 120_000;
31+
2432 /** The wait before try `attempt` (1 is the first retry): 0.25 s, 0.5 s, 1 s, 2 s, then every 5 s. */
2533 export function retryDelay(attempt: number): number {
2634 return Math.min(250 * 2 ** (attempt - 1), 5000);
6674 return query(url.search) === query(current.search);
6775 }
6876
77+/** What a read has been through, to decide whether to try it again after `wait`. */
78+export type Tried = {
79+ /** A refresh of the page already showing, not a navigation. */
80+ refresh: boolean;
81+ /** Since the read started. */
82+ elapsedMs: number;
83+ /** Waited between tries while the browser said it was online. */
84+ onlineMs: number;
85+ /** Whether the error page is showing, trying on its own. */
86+ erroring: boolean;
87+};
88+
89+/** Whether to try a read again after `wait`: never twice over, never forever on a network that is up. */
90+export function keepTrying(tried: Tried, wait: number): boolean {
91+ if (tried.erroring) return false;
92+ if (tried.refresh) return tried.onlineMs + wait <= REFRESH_PATIENCE_MS;
93+ return tried.elapsedMs + wait <= PATIENCE_MS;
94+}
95+
96+/** Set by the error page while it tries on its own (lib/reconnect.ts). */
97+let erroring = false;
98+export function setErroring(on: boolean) {
99+ erroring = on;
100+}
101+
69102 // ── Whether a read is waiting on the network ─────────────────────────────
70103
71104 let waiting = 0;
87120 );
88121 }
89122
90−/** Waits `ms`, or less once the browser is back online; rejects when `signal` aborts. */
123+/**
124+ * Waits `ms`, or less once the browser is back online, and then for as long
125+ * as the tab is out of view; rejects when `signal` aborts.
126+ */
91127 function pause(ms: number, signal: AbortSignal | null | undefined): Promise<void> {
92128 return new Promise((resolve, reject) => {
93129 if (signal?.aborted) return reject(signal.reason);
94− const done = () => {
130+ const cleanup = () => {
95131 clearTimeout(timer);
96− window.removeEventListener("online", done);
132+ window.removeEventListener("online", due);
133+ document.removeEventListener("visibilitychange", shown);
97134 signal?.removeEventListener("abort", stop);
135+ };
136+ let elapsed = false;
137+ function due() {
138+ elapsed = true;
139+ if (document.visibilityState !== "visible") return;
140+ cleanup();
98141 resolve();
99− };
100− const stop = () => {
101− clearTimeout(timer);
102− window.removeEventListener("online", done);
142+ }
143+ // Only once the wait is over: coming into view does not cut it short.
144+ function shown() {
145+ if (elapsed) due();
146+ }
147+ function stop() {
148+ cleanup();
103149 reject(signal?.reason);
104− };
105− const timer = setTimeout(done, ms);
106− window.addEventListener("online", done, { once: true });
150+ }
151+ const timer = setTimeout(due, ms);
152+ window.addEventListener("online", due);
153+ document.addEventListener("visibilitychange", shown);
107154 signal?.addEventListener("abort", stop, { once: true });
108155 });
109156 }
126173 const method = init?.method ?? request?.method ?? "GET";
127174 if (!retriable(url, method, window.location.origin)) return original(input, init);
128175 const signal = init?.signal ?? request?.signal;
129− const patient = refreshesCurrent(url, window.location);
176+ const refresh = refreshesCurrent(url, window.location);
130177 const started = Date.now();
178+ let onlineMs = 0;
131179 let counted = false;
132180 try {
133181 for (let attempt = 1; ; attempt++) {
137185 } catch (error) {
138186 const wait = retryDelay(attempt);
139187 if (!isNetworkFailure(error) || signal?.aborted) throw error;
140− if (!patient && Date.now() - started + wait > PATIENCE_MS) throw error;
188+ if (!keepTrying({ refresh, elapsedMs: Date.now() - started, onlineMs, erroring }, wait)) throw error;
189+ if (navigator.onLine) onlineMs += wait;
141190 // The first miss may be nothing; from the second, say so.
142191 if (attempt === 2 && !counted) {
143192 counted = true;

Comments

Sign in to comment.