| 8 | 8 | | * later each time, sooner once the browser says it is back online. While |
| 9 | 9 | | * one is waiting, `useReconnecting` says so (components/connection-toast.tsx). |
| 10 | 10 | | * |
| 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. |
| 15 | 20 | | * A read the router gave up on itself (its signal aborted) stops at once. |
| 16 | 21 | | * Writes are never sent again: one that failed may have arrived, and |
| 17 | 22 | | * sending it twice could do it twice. |
| ⋯ |
| 21 | 26 | | /** 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. */ |
| 22 | 27 | | export const PATIENCE_MS = 45_000; |
| 23 | 28 | | |
| 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 | + | |
| 24 | 32 | | /** The wait before try `attempt` (1 is the first retry): 0.25 s, 0.5 s, 1 s, 2 s, then every 5 s. */ |
| 25 | 33 | | export function retryDelay(attempt: number): number { |
| 26 | 34 | | return Math.min(250 * 2 ** (attempt - 1), 5000); |
| ⋯ |
| 66 | 74 | | return query(url.search) === query(current.search); |
| 67 | 75 | | } |
| 68 | 76 | | |
| 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 | + | |
| 69 | 102 | | // ── Whether a read is waiting on the network ───────────────────────────── |
| 70 | 103 | | |
| 71 | 104 | | let waiting = 0; |
| ⋯ |
| 87 | 120 | | ); |
| 88 | 121 | | } |
| 89 | 122 | | |
| 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 | + | */ |
| 91 | 127 | | function pause(ms: number, signal: AbortSignal | null | undefined): Promise<void> { |
| 92 | 128 | | return new Promise((resolve, reject) => { |
| 93 | 129 | | if (signal?.aborted) return reject(signal.reason); |
| 94 | | − | const done = () => { |
| 130 | + | const cleanup = () => { |
| 95 | 131 | | clearTimeout(timer); |
| 96 | | − | window.removeEventListener("online", done); |
| 132 | + | window.removeEventListener("online", due); |
| 133 | + | document.removeEventListener("visibilitychange", shown); |
| 97 | 134 | | signal?.removeEventListener("abort", stop); |
| 135 | + | }; |
| 136 | + | let elapsed = false; |
| 137 | + | function due() { |
| 138 | + | elapsed = true; |
| 139 | + | if (document.visibilityState !== "visible") return; |
| 140 | + | cleanup(); |
| 98 | 141 | | 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(); |
| 103 | 149 | | 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); |
| 107 | 154 | | signal?.addEventListener("abort", stop, { once: true }); |
| 108 | 155 | | }); |
| 109 | 156 | | } |
| ⋯ |
| 126 | 173 | | const method = init?.method ?? request?.method ?? "GET"; |
| 127 | 174 | | if (!retriable(url, method, window.location.origin)) return original(input, init); |
| 128 | 175 | | const signal = init?.signal ?? request?.signal; |
| 129 | | − | const patient = refreshesCurrent(url, window.location); |
| 176 | + | const refresh = refreshesCurrent(url, window.location); |
| 130 | 177 | | const started = Date.now(); |
| 178 | + | let onlineMs = 0; |
| 131 | 179 | | let counted = false; |
| 132 | 180 | | try { |
| 133 | 181 | | for (let attempt = 1; ; attempt++) { |
| ⋯ |
| 137 | 185 | | } catch (error) { |
| 138 | 186 | | const wait = retryDelay(attempt); |
| 139 | 187 | | 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; |
| 141 | 190 | | // The first miss may be nothing; from the second, say so. |
| 142 | 191 | | if (attempt === 2 && !counted) { |
| 143 | 192 | | counted = true; |