import { lazy, type ComponentType, type LazyExoticComponent } from 'react'; /** * `lazy()` with recovery from chunk-load failures. * * A lazy route's `import()` can fail transiently: * • after a deploy, an already-open page still references chunk URLs that no * longer exist on the server; * • in dev, Vite's dependency optimizer can answer 504 ("Outdated Optimize * Dep") for an optimized dependency after it re-optimized. * * Either way the import rejects and, with a plain `lazy()`, the route error * boundary shows a dead "This page slammed" screen the user can do nothing with. * * Recovery strategy (bounded, loop-safe): * 1. Retry the import a few times with a short backoff — this alone clears the * optimizer's brief 504 window without any reload. * 2. If it still fails, do a full reload (fetches a fresh index + module graph). * 3. Reloads are throttled by a **cooldown** (not a once-per-session flag), so * repeated stale-hash failures across a session each self-heal, while a * genuine persistent failure can't spin the browser in a reload loop — it * falls through to the error boundary as the last resort. * * ⚠️ The old version reloaded at most ONCE per session; the second failure then * dead-ended on "This page slammed" until a manual reload. That is exactly the * bug this rewrite fixes — don't reintroduce a once-per-session cap. */ const RELOAD_TS_KEY = 'skatespots:chunk-reload-at'; const RELOAD_COOLDOWN_MS = 10_000; const IMPORT_ATTEMPTS = 3; /** * Pages take no props, so any props-compatible component type works here. Using * `ComponentType` (rather than `any`) keeps this type-safe while still * accepting every page component in the router. */ type AnyComponent = ComponentType; function isChunkLoadError(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error); return /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Outdated Optimize Dep|Failed to load module script/i.test( message, ); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** True when the last reload was long enough ago (or there was none) to reload again. */ function canReloadNow(): boolean { try { const last = Number(sessionStorage.getItem(RELOAD_TS_KEY) || 0); return !Number.isFinite(last) || Date.now() - last > RELOAD_COOLDOWN_MS; } catch { return true; } } function markReload(): void { try { sessionStorage.setItem(RELOAD_TS_KEY, String(Date.now())); } catch { /* sessionStorage unavailable — the caller's reload still runs */ } } function clearReloadMark(): void { try { sessionStorage.removeItem(RELOAD_TS_KEY); } catch { /* ignore */ } } export function lazyWithRetry( factory: () => Promise<{ default: AnyComponent }>, ): LazyExoticComponent { return lazy(async () => { let lastError: unknown; // 1) Retry the import with backoff — handles a transient 504 with no reload. for (let attempt = 0; attempt < IMPORT_ATTEMPTS; attempt++) { try { const module = await factory(); // A clean load means the module graph is healthy again. clearReloadMark(); return module; } catch (error) { lastError = error; if (!isChunkLoadError(error)) throw error; if (attempt < IMPORT_ATTEMPTS - 1) await sleep(400 * (attempt + 1)); } } // 2) Still failing → reload, if we haven't reloaded within the cooldown. if (canReloadNow()) { markReload(); window.location.reload(); // Suspend this import until the new document takes over. return await new Promise(() => undefined); } // 3) Reloaded recently and it's still broken → let the error boundary show. throw lastError; }); }