mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
96e4ff5932
* feat(client): retry a failed page chunk once before giving up A dynamic import fails for two different reasons, and only one of them is worth a second attempt. If the file is gone because a deploy happened while the tab was open, nothing but a reload helps and reloadOnceForChunk already owns that path. If the fetch itself went wrong — hotel wifi, an aborted request, a service worker handing back a truncated entry — one retry fixes it. The retry cannot reuse the original specifier, since Vite rewrites it into a hashed asset URL at build time; the only usable URL is the one the browser names in the failure message, and a fresh query on it misses both the HTTP cache and the Workbox precache. This is also where a healthy load is acknowledged. Clearing the reload marker used to sit in main.tsx right after render(), which runs before React commits and long before any chunk is fetched, so the marker was gone before it was ever needed. * perf(client): load the page chunks on demand Every one of the twenty pages was a static import, so the entry chunk carried the planner, the journal, the atlas and the vacation planner for anyone who only opened the dashboard. They now load through lazyWithRetry; the entry chunk drops from 5,183 kB to 1,243 kB (gzip 1,300 -> 349). LoginPage stays static on purpose: anyone logged out lands there, and every other route redirects there first. The Suspense boundary sits above <Routes> rather than inside ProtectedRoute so it stays mounted across navigations. react-router runs location updates inside a transition, which keeps the current page on screen while the next chunk arrives instead of flashing a spinner on every jump. The precache is unchanged by design: workbox picks up the new chunks, so the install size stays where it was (107 -> 178 entries, +31 KiB) and every route remains available offline. This buys first paint, not install size.
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
import { lazy } from 'react';
|
|
import type { ComponentType, LazyExoticComponent } from 'react';
|
|
import { clearChunkReloadMarker, isChunkLoadError } from './chunkReload';
|
|
|
|
/**
|
|
* React.lazy with a single second attempt — and the place where a healthy load is
|
|
* acknowledged.
|
|
*
|
|
* A route chunk fails for two very different reasons. Either the file is gone
|
|
* because a deploy happened while the tab was open; nothing but a reload helps, and
|
|
* reloadOnceForChunk() already owns that path through the route boundary. Or the
|
|
* fetch itself went wrong: a blip on hotel wifi, an aborted request, a service
|
|
* worker handing back a truncated entry. That one heals on a second try, and it is
|
|
* the only case this wrapper exists for.
|
|
*
|
|
* The retry cannot reuse the original specifier. Vite rewrites
|
|
* import('./pages/AtlasPage') into a hashed asset URL at build time, so a query
|
|
* appended in source stops it being statically analysable: the build leaves a
|
|
* runtime path that resolves against the wrong directory, while dev — which really
|
|
* does serve the source file — looks perfectly fine. The only usable URL is the one
|
|
* the browser names in the failure message. A fresh query on it misses the HTTP
|
|
* cache and the Workbox precache, both of which key on the full URL.
|
|
*/
|
|
|
|
const retried = new Set<string>();
|
|
|
|
/** The cache-busted URL for one failed chunk, or null when a retry is pointless. */
|
|
export function retryUrlFor(error: unknown): string | null {
|
|
if (!isChunkLoadError(error)) return null;
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const match = /https?:\/\/[^\s'")]+/.exec(message);
|
|
// Safari says "Importing a module script failed." and names no URL at all, and a
|
|
// relative path gives us nothing to bust. Both belong to the boundary, not here.
|
|
if (!match) return null;
|
|
|
|
const url = match[0];
|
|
// The CSS preload wording carries a stylesheet, which is not ours to import.
|
|
if (url.endsWith('.css') || retried.has(url)) return null;
|
|
retried.add(url);
|
|
|
|
const busted = new URL(url);
|
|
busted.searchParams.set('t', String(Date.now()));
|
|
return busted.href;
|
|
}
|
|
|
|
export function lazyWithRetry<T extends ComponentType<any>>(
|
|
load: () => Promise<{ default: T }>
|
|
): LazyExoticComponent<T> {
|
|
return lazy(() =>
|
|
load()
|
|
.catch((error: unknown) => {
|
|
const url = retryUrlFor(error);
|
|
// Rethrown on purpose: React.lazy hands the rejection to the nearest
|
|
// boundary, and that one knows how to reload once per session.
|
|
if (!url) throw error;
|
|
return import(/* @vite-ignore */ url) as Promise<{ default: T }>;
|
|
})
|
|
.then(module => {
|
|
// A chunk arrived, so the asset pipeline is intact and a later deploy may
|
|
// spend its own reload. This used to sit in main.tsx right after render(),
|
|
// which runs before React commits and long before any chunk is fetched — the
|
|
// marker was cleared before it was ever needed, and a chunk that is genuinely
|
|
// missing could put the tab in the reload loop the marker exists to prevent.
|
|
clearChunkReloadMarker();
|
|
return module;
|
|
})
|
|
);
|
|
}
|