From 1c00ba99f31eff3d9e30780a8d543a89ebc22e59 Mon Sep 17 00:00:00 2001 From: JonnieSparkles Date: Fri, 26 Jun 2026 09:42:36 -0400 Subject: [PATCH 1/5] feat: implement global error handling and chunk load recovery - Updated TypeScript configuration to preserve JSX. - Added GlobalError component for handling loading errors in the documentation. - Introduced ChunkLoadRecoveryScript to manage chunk load failures and automatic reloads. - Integrated ChunkLoadRecoveryScript into the main layout for improved error resilience. --- src/app/global-error.tsx | 140 +++++++++++++++++++++++++ src/app/layout.tsx | 2 + src/components/chunk-load-recovery.tsx | 93 ++++++++++++++++ tsconfig.json | 2 +- 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/app/global-error.tsx create mode 100644 src/components/chunk-load-recovery.tsx diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 000000000..835a20ed8 --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useEffect } from "react"; + +type GlobalErrorProps = { + error: Error & { digest?: string }; + reset: () => void; +}; + +function isChunkLoadError(error: Error): boolean { + const message = `${error.name || ""} ${error.message || ""}`; + + return /ChunkLoadError|Failed to load chunk|Loading chunk .* failed|\/_next\/static\/chunks\//i.test( + message, + ); +} + +function scheduleReloadForChunkError(error: Error) { + if (!isChunkLoadError(error)) return; + + const storageKey = "ar-io-docs:chunk-load-recovery"; + const retryWindowMs = 30000; + const maxReloads = 2; + const now = Date.now(); + + try { + const parsed = JSON.parse(sessionStorage.getItem(storageKey) || "null") as + | { count?: number; firstSeen?: number } + | null; + + const state = + parsed && parsed.firstSeen && now - parsed.firstSeen <= retryWindowMs + ? { + count: parsed.count ?? 0, + firstSeen: parsed.firstSeen, + } + : { + count: 0, + firstSeen: now, + }; + + if (state.count >= maxReloads) return; + + sessionStorage.setItem( + storageKey, + JSON.stringify({ ...state, count: state.count + 1 }), + ); + + window.setTimeout(() => window.location.reload(), 250); + } catch { + window.setTimeout(() => window.location.reload(), 250); + } +} + +export default function GlobalError({ error, reset }: GlobalErrorProps) { + useEffect(() => { + console.error(error); + scheduleReloadForChunkError(error); + }, [error]); + + return ( + + +
+
+

+ ar.io Documentation +

+

+ The docs hit a temporary loading error. +

+

+ This can happen when a gateway returns a transient error while + loading an app chunk. Reloading usually routes the request through + a healthy response. +

+
+ + +
+
+
+ + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8660fc89e..2ae5c24f7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,6 +11,7 @@ import { AnnouncementBanner, type AnnouncementBannerProps, } from "@/components/announcement-banner"; +import { ChunkLoadRecoveryScript } from "@/components/chunk-load-recovery"; import { Plus_Jakarta_Sans } from "next/font/google"; const plusJakartaSans = Plus_Jakarta_Sans({ @@ -44,6 +45,7 @@ export default function Layout({ children }: { children: ReactNode }) { return ( + `; + +async function collectHtmlFiles(dir: string): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + entries.map(async (entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return collectHtmlFiles(full); + return entry.isFile() && entry.name.endsWith(".html") ? [full] : []; + }), + ); + return files.flat(); +} + +async function main() { + try { + await fs.access(OUT_DIR); + } catch { + throw new Error( + `Output directory not found: ${OUT_DIR}. Run "next build" first.`, + ); + } + + const htmlFiles = await collectHtmlFiles(OUT_DIR); + if (htmlFiles.length === 0) { + throw new Error(`No HTML files found under ${OUT_DIR}.`); + } + + let injected = 0; + let skipped = 0; + const missingHead: string[] = []; + + for (const file of htmlFiles) { + const html = await fs.readFile(file, "utf8"); + + if (html.includes(`id="${MARKER_ID}"`)) { + skipped += 1; + continue; + } + + const headIndex = html.indexOf(""); + if (headIndex === -1) { + missingHead.push(path.relative(OUT_DIR, file)); + continue; + } + + const insertAt = headIndex + "".length; + const next = html.slice(0, insertAt) + SCRIPT_TAG + html.slice(insertAt); + await fs.writeFile(file, next); + injected += 1; + } + + console.log( + `[inject-chunk-load-recovery] injected into ${injected} file(s), ` + + `skipped ${skipped} already-injected file(s).`, + ); + + if (missingHead.length > 0) { + throw new Error( + `No found in ${missingHead.length} HTML file(s): ` + + missingHead.slice(0, 10).join(", ") + + (missingHead.length > 10 ? ", …" : ""), + ); + } +} + +main().catch((error) => { + console.error("[inject-chunk-load-recovery]", error); + process.exit(1); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2ae5c24f7..8a1442d5f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,7 +11,6 @@ import { AnnouncementBanner, type AnnouncementBannerProps, } from "@/components/announcement-banner"; -import { ChunkLoadRecoveryScript } from "@/components/chunk-load-recovery"; import { Plus_Jakarta_Sans } from "next/font/google"; const plusJakartaSans = Plus_Jakarta_Sans({ @@ -45,7 +44,15 @@ export default function Layout({ children }: { children: ReactNode }) { return ( - + {/* + The chunk-load recovery script is NOT rendered here. Under + `output: "export"` the RSC renderer serializes any inline