Skip to content

Commit

Merge branch 'worktree-agent-a64e51c8f386f46ac'

syntaqxcommitted Parents9e9f54652ba2f4Browse files
3 files+73−90/3 viewed
+21−1
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { reloadFixes } from "./stale-build.ts";
4+import { RELOADED_KEY, clientNavigated, reloadFixes, reloadedBefore } from "./stale-build.ts";
55
66 test("a client navigation that hits an older build reloads the page", () => {
77 const seen = { clientNavigation: true, caughtByCatchAll: false };
2222 assert.equal(reloadFixes({ error: new Error("D1_ERROR: no such table"), clientNavigation: true, caughtByCatchAll: false }), false);
2323 assert.equal(reloadFixes({ error: {}, status: 500, clientNavigation: true, caughtByCatchAll: false }), false);
2424 });
25+
26+test("the location a tab's document loaded at is no client navigation, whatever its key", () => {
27+ // ScrollRestoration's inline script gives the first entry a random key.
28+ assert.equal(clientNavigated("kvhli4udgd", "kvhli4udgd"), false);
29+ assert.equal(clientNavigated("default", "default"), false);
30+ assert.equal(clientNavigated("a1b2c3", "kvhli4udgd"), true);
31+ // The server never navigated.
32+ assert.equal(clientNavigated("default", undefined), false);
33+ assert.equal(clientNavigated("a1b2c3", undefined), false);
34+});
35+
36+test("an address loaded again once is not loaded again", () => {
37+ const storage = (value: string | null) => ({ getItem: (key: string) => (key === RELOADED_KEY ? value : null) });
38+ assert.equal(reloadedBefore("/flagon-io/-/docs", storage("/flagon-io/-/docs")), true);
39+ assert.equal(reloadedBefore("/flagon-io/-/docs", storage("/flagon-io/g1t/nope")), false);
40+ assert.equal(reloadedBefore("/flagon-io/-/docs", storage(null)), false);
41+ // No storage, or storage that throws: nothing remembered.
42+ assert.equal(reloadedBefore("/flagon-io/-/docs", undefined), false);
43+ assert.equal(reloadedBefore("/flagon-io/-/docs", { getItem: () => { throw new Error("denied"); } }), false);
44+});
+39−0
3737 return STALE_MESSAGES.some((pattern) => pattern.test(message));
3838 }
3939
40+/**
41+ * The key React Router gives the location a tab's document loaded at. It is
42+ * the history entry's own key, and not "default": ScrollRestoration's inline
43+ * script gives the entry a key before the app starts, so a key alone cannot
44+ * tell a document load from a client navigation. Read when this module first
45+ * runs, which is before the app hydrates; undefined on the server.
46+ */
47+const DOCUMENT_KEY: string | undefined =
48+ typeof window === "undefined" ? undefined : ((window.history.state as { key?: string } | null)?.key ?? "default");
49+
50+/**
51+ * Whether the page at this location key got there by a client navigation.
52+ * The server, and the render that hydrates the server's page, always say
53+ * no, so both render the same error page.
54+ */
55+export function clientNavigated(key: string, documentKey: string | undefined = DOCUMENT_KEY): boolean {
56+ return documentKey !== undefined && key !== documentKey;
57+}
58+
4059 /** The session key that remembers which address was reloaded, so it is reloaded once. */
4160 export const RELOADED_KEY = "g1t-reloaded";
61+
62+/** Whether this address was already loaded again once, so another try would not help. */
63+export function reloadedBefore(href: string, storage: Pick<Storage, "getItem"> | undefined = sessionStorageOrNone()): boolean {
64+ try {
65+ return storage?.getItem(RELOADED_KEY) === href;
66+ } catch {
67+ return false;
68+ }
69+}
70+
71+/** How long the page waits for the reload to start before it shows the error instead. */
72+export const RELOAD_GIVE_UP_MS = 5000;
73+
74+function sessionStorageOrNone(): Storage | undefined {
75+ try {
76+ return typeof window === "undefined" ? undefined : window.sessionStorage;
77+ } catch {
78+ return undefined;
79+ }
80+}
+13−8
1111 Search,
1212 Settings,
1313 } from "lucide-react";
14−import { useEffect, useRef, useState } from "react";
14+import { useEffect, useMemo, useRef, useState } from "react";
1515 import {
1616 Form,
1717 isRouteErrorResponse,
6969 import { registrationMode } from "./lib/registration.server";
7070 import { addresses } from "./lib/addresses.server";
7171 import { useSignUpCopy } from "./lib/registration";
72−import { RELOADED_KEY, reloadFixes } from "./lib/stale-build";
72+import { RELOADED_KEY, RELOAD_GIVE_UP_MS, clientNavigated, reloadFixes, reloadedBefore } from "./lib/stale-build";
7373 import { useNonce } from "./lib/nonce";
7474 import { isNeedsSignIn } from "./lib/website-token";
7575 import { LiveNotifications } from "./components/notifications/live-notifications";
658658 export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
659659 const location = useLocation();
660660 const matches = useMatches();
661+ const href = location.pathname + location.search + location.hash;
661662 // A tab left open across a deploy: load the address again as a whole
662− // page, once, rather than show an error the reload fixes.
663− const reload = reloadFixes({
663+ // page, once, rather than show an error the reload fixes. A document load
664+ // never reloads, so the server and the hydrating client render the same.
665+ const reloadable = reloadFixes({
664666 error,
665667 status: isRouteErrorResponse(error) ? error.status : undefined,
666− // The first page a tab loads has the key "default"; a client navigation's is its own.
667− clientNavigation: location.key !== "default",
668+ clientNavigation: clientNavigated(location.key),
668669 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
669670 });
670− const href = location.pathname + location.search + location.hash;
671+ // Already loaded again once, or the reload never started: show the page.
672+ const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
673+ const [gaveUp, setGaveUp] = useState<string | null>(null);
674+ const reload = reloadable && !tried && gaveUp !== href;
671675 useEffect(() => {
672676 if (!reload) return;
673677 try {
674− if (sessionStorage.getItem(RELOADED_KEY) === href) return;
675678 sessionStorage.setItem(RELOADED_KEY, href);
676679 } catch {
677680 // No session storage: reload anyway; a document load never asks again.
678681 }
679682 window.location.assign(href);
683+ const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
684+ return () => window.clearTimeout(timer);
680685 }, [reload, href]);
681686 if (reload) {
682687 return (