| 1 | import { useContext, useEffect } from "react"; |
| 2 | import { type Location, createPath, useNavigation, UNSAFE_DataRouterContext } from "react-router"; |
| 3 | import { 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 | */ |
| 13 | export 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 | */ |
| 22 | export 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 | */ |
| 32 | export 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 | } |