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.

46 linesCodeBlameRaw
1import { useContext, useEffect } from "react";
2import { type Location, createPath, useNavigation, UNSAFE_DataRouterContext } from "react-router";
3import { pendingNavigationOwns } from "./navigation-owner.ts";
4
5/**
6 * How long a client navigation may load before the page gives up on it and
7 * loads the address as a whole page. Longer than any read may take on the
8 * server (lib/deadline.ts `SLOW_READ_DEADLINE_MS`): a slow page that would
9 * arrive is never cut off and started again, and a stuck service shows its
10 * error page first. This catches what the server cannot: a request lost on
11 * the way, a connection that went quiet, a route's code that never arrived.
12 */
13export const STUCK_NAVIGATION_MS = 75_000;
14
15/**
16 * Where to send a navigation that has been loading too long, or null when
17 * there is nothing safe to retry. Only "loading" is retried: it reads, even
18 * after a form was posted (the action already answered and the page is
19 * reading what it changed). "submitting" is a write still on its way, and
20 * sending it again could do it twice.
21 */
22export function stuckTarget(navigation: { state: string; location?: Pick<Location, "pathname" | "search" | "hash"> | null }): string | null {
23 if (navigation.state !== "loading" || !navigation.location) return null;
24 return createPath(navigation.location);
25}
26
27/**
28 * Never leaves the page on its loading bar: a navigation still loading
29 * after `STUCK_NAVIGATION_MS` is finished as a document load of the same
30 * address, which either arrives or shows the server's error page.
31 */
32export function useStuckNavigationRescue(ms = STUCK_NAVIGATION_MS) {
33 const context = useContext(UNSAFE_DataRouterContext);
34 const navigation = useNavigation();
35 const target = stuckTarget(navigation);
36 const key = navigation.location?.key;
37 useEffect(() => {
38 if (!target) return;
39 const timer = window.setTimeout(() => {
40 if (!context || !pendingNavigationOwns(context.router.state.navigation, key)) return;
41 console.warn(`g1t: navigation to ${target} still loading after ${ms}ms; loading it as a whole page`);
42 window.location.assign(target);
43 }, ms);
44 return () => window.clearTimeout(timer);
45 }, [target, key, ms, context?.router]);
46}