fix(editor): prove the lazy-chunk reload landed before suppressing crash reports

- lazy-with-retry: reload guard stores the requesting document's identity, so a
  vetoed reload() no longer reads as "recovery ran" (crash b860def2)
- lazy-with-retry: bound the post-reload suspension so a vetoed navigation
  surfaces the real error instead of hanging the pane on a spinner
- RichMarkdownErrorBoundary: contain the LazyChunkLoadError sentinel without a
  crash report, but record a lazy_chunk_boundary_degraded breadcrumb
- EditorContent: name the rich markdown chunk at the lazy call site

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-01 03:56:47 -07:00
co-authored by Orca
parent 7fbf3130ac
commit 5dfe9f2daf
7 changed files with 276 additions and 36 deletions
@@ -33,7 +33,11 @@ import { ExternalFileChangeBanner } from './ExternalFileChangeBanner'
const MonacoEditor = lazy(() => import('./MonacoEditor'))
const DiffViewer = lazy(() => import('./DiffViewer'))
const CombinedDiffViewer = lazy(() => import('./CombinedDiffViewer'))
const RichMarkdownEditor = lazy(() => import('./RichMarkdownEditor'))
// Why keyed: the reload breadcrumb is the only pre-crash evidence, and an unkeyed
// site logs `reloadKey: "unknown"` (see crash b860def2).
const RichMarkdownEditor = lazy(() => import('./RichMarkdownEditor'), {
reloadKey: 'rich-markdown-editor'
})
const MarkdownPreview = lazy(() => import('./MarkdownPreview'))
const ImageViewer = lazy(() => import('./ImageViewer'))
const ImageDiffViewer = lazy(() => import('./ImageDiffViewer'))
@@ -1,5 +1,7 @@
// @vitest-environment happy-dom
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { Suspense, act, type ReactElement, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -8,12 +10,20 @@ import { lazyWithRetry } from '@/lib/lazy-with-retry'
import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary'
const reportCrashMock = vi.hoisted(() => vi.fn())
const recordBreadcrumbMock = vi.hoisted(() => vi.fn())
vi.mock('@/lib/react-error-boundary-reporting', () => ({
reportReactErrorBoundaryCrash: reportCrashMock
}))
vi.mock('@/lib/crash-breadcrumb-recorder', () => ({
recordRendererCrashBreadcrumb: recordBreadcrumbMock
}))
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
// A guard left by a *different* document is what proves the reload landed; only then
// does lazy-with-retry produce the sentinel this boundary suppresses.
const LANDED_RELOAD_GUARD_VALUE = 'doc-before-the-reload'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
@@ -23,11 +33,20 @@ function createContainer(): { container: HTMLDivElement; root: Root } {
return { container, root: createRoot(container) }
}
// Mirrors EditorContent: the Suspense sits above the boundary, which wraps the lazy editor.
function BoundaryHarness({ children }: { children: ReactNode }): ReactElement {
// Mirrors EditorContent: the Suspense sits above the boundary, which EditorContent
// re-keys per open file (`key={viewStateScopeId}`), and which wraps the lazy editor.
function BoundaryHarness({
children,
boundaryKey = 'pane-a'
}: {
children: ReactNode
boundaryKey?: string
}): ReactElement {
return (
<Suspense fallback={<div>Loading...</div>}>
<RichMarkdownErrorBoundary fileId="file-1">{children}</RichMarkdownErrorBoundary>
<RichMarkdownErrorBoundary key={boundaryKey} fileId="file-1">
{children}
</RichMarkdownErrorBoundary>
</Suspense>
)
}
@@ -38,6 +57,8 @@ async function flushReactWork(): Promise<void> {
})
}
// The breadcrumb dedupe is module-scoped (it has to outlive boundary remounts), so
// every test below must use a distinct parse-error message to start from a clean slate.
describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
let root: Root | null = null
let container: HTMLDivElement | null = null
@@ -45,6 +66,7 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
beforeEach(() => {
reportCrashMock.mockReset()
recordBreadcrumbMock.mockReset()
window.sessionStorage.clear()
consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined)
})
@@ -60,10 +82,11 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
consoleError.mockRestore()
})
// Repro for crash b860def2: an app update swapped chunk hashes, lazy-with-retry
// burned its one guarded reload, then surfaced LazyChunkLoadError here.
// The only honest sentinel case: a landed reload already refetched the chunk and it
// still fails to parse, so the report is not actionable. (b860def2 itself is NOT this
// case — its reload was vetoed, so lazy-with-retry now re-throws the real error.)
it('renders the fallback without reporting after guarded dynamic import exhaustion', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError("Unexpected token ':'")),
{ retries: 0 }
@@ -82,6 +105,70 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
expect(container?.textContent).toContain('rich markdown editor')
expect(reportCrashMock).not.toHaveBeenCalled()
// The suppressed report was the only artifact naming this surface; keep evidence.
expect(recordBreadcrumbMock).toHaveBeenCalledWith('lazy_chunk_boundary_degraded', {
boundaryId: 'editor.rich-markdown',
cause: "SyntaxError: Unexpected token ':'"
})
})
// The breadcrumb ring holds 30 entries and does not coalesce this name, so an
// impatient Retry streak on a permanently rejected chunk would erase the trail.
it('records the degraded breadcrumb once across repeated Retry clicks', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError("Unexpected token '}'")),
{ retries: 0 }
)
;({ container, root } = createContainer())
await act(async () => {
root?.render(
<BoundaryHarness>
<LazyRejectingImport />
</BoundaryHarness>
)
})
await flushReactWork()
await flushReactWork()
for (let click = 0; click < 3; click += 1) {
const retry = container?.querySelector('button')
expect(retry).not.toBeNull()
await act(async () => {
retry?.click()
})
await flushReactWork()
}
expect(container?.textContent).toContain('rich markdown editor')
expect(recordBreadcrumbMock).toHaveBeenCalledTimes(1)
})
// EditorContent re-keys the boundary per open file, so instance-scoped dedupe would
// re-record on every markdown tab switch — the same ring flush the Retry streak causes.
it('records the degraded breadcrumb once across boundary remounts', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError("Unexpected token '<'")),
{ retries: 0 }
)
;({ container, root } = createContainer())
for (const boundaryKey of ['pane-a', 'pane-b', 'pane-c']) {
await act(async () => {
root?.render(
<BoundaryHarness boundaryKey={boundaryKey}>
<LazyRejectingImport />
</BoundaryHarness>
)
})
await flushReactWork()
await flushReactWork()
}
expect(container?.textContent).toContain('rich markdown editor')
expect(recordBreadcrumbMock).toHaveBeenCalledTimes(1)
})
it('still reports ordinary render errors', async () => {
@@ -100,6 +187,7 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
})
expect(container?.textContent).toContain('rich markdown editor')
expect(recordBreadcrumbMock).not.toHaveBeenCalled()
expect(reportCrashMock).toHaveBeenCalledTimes(1)
expect(reportCrashMock).toHaveBeenCalledWith(
expect.objectContaining({
@@ -109,4 +197,14 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
})
)
})
// b860def2's pre-crash breadcrumb read `reloadKey: "unknown"`, which cannot tell
// the rich editor chunk apart from the seven other lazy chunks in EditorContent.
it('names the rich editor chunk at the EditorContent lazy call site', () => {
const source = readFileSync(
join(process.cwd(), 'src/renderer/src/components/editor/EditorContent.tsx'),
'utf8'
)
expect(source).toContain("reloadKey: 'rich-markdown-editor'")
})
})
@@ -1,4 +1,5 @@
import React from 'react'
import { recordRendererCrashBreadcrumb } from '@/lib/crash-breadcrumb-recorder'
import { isLazyChunkLoadError } from '@/lib/lazy-with-retry'
import { reportReactErrorBoundaryCrash } from '@/lib/react-error-boundary-reporting'
import { translate } from '@/i18n/i18n'
@@ -13,6 +14,20 @@ type State = {
fileId: string
}
// Module-scoped, not per instance: EditorContent re-keys this boundary per open file,
// so an instance field would re-record on every markdown tab switch and flush the
// 30-entry breadcrumb ring that this very evidence lives in. Bounded by the number of
// distinct chunk failures a document can see (effectively one).
const recordedLazyChunkCauses = new Set<string>()
function describeLazyChunkCause(error: Error): string {
const cause = (error as { cause?: unknown }).cause
if (cause instanceof Error) {
return `${cause.name}: ${cause.message}`
}
return cause === undefined ? 'unknown' : String(cause)
}
// Why: a thrown exception inside the TipTap/ProseMirror render or in the
// effect that runs `setContent` + empty-list repair on external-reload
// would escape to the React root and — without this boundary — cause React
@@ -39,10 +54,18 @@ export class RichMarkdownErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error('[RichMarkdownEditor] render crash contained by boundary', error, info)
// Why: lazy-with-retry already spent its retries and its one guarded reload, so a
// chunk-hash swap after an app update is expected degradation, not a crash. The
// fallback below still renders (matches RecoverableRenderErrorBoundary).
// Retries and the one guarded reload are already spent, so a crash report is not
// actionable — but it was the only artifact naming this surface, so leave a
// breadcrumb. Fallback still renders (matches RecoverableRenderErrorBoundary).
if (isLazyChunkLoadError(error)) {
const cause = describeLazyChunkCause(error)
if (!recordedLazyChunkCauses.has(cause)) {
recordedLazyChunkCauses.add(cause)
recordRendererCrashBreadcrumb('lazy_chunk_boundary_degraded', {
boundaryId: 'editor.rich-markdown',
cause
})
}
return
}
void reportReactErrorBoundaryCrash({
@@ -14,6 +14,9 @@ vi.mock('@/lib/react-error-boundary-reporting', () => ({
}))
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
// Only a guard left by a *different* document proves the reload landed, which is
// what makes lazy-with-retry emit the sentinel this boundary suppresses.
const LANDED_RELOAD_GUARD_VALUE = 'doc-before-the-reload'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
@@ -60,7 +63,7 @@ describe('RecoverableRenderErrorBoundary lazy chunk containment', () => {
})
it('renders the fallback without reporting after guarded dynamic import exhaustion', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() =>
Promise.reject(
@@ -13,8 +13,8 @@ import { isLazyChunkLoadError, loadLazyWithRetry } from './lazy-with-retry'
// const SourceControl = lazy(() => import('./SourceControl')) // lazyWithRetry
//
// So the corrupt-chunk recovery IS wired in. The crash was a BLIND SPOT in that
// recovery: after the single guarded window.location.reload() has already fired
// once this session (sessionStorage 'orca:lazy-chunk-reload-attempted' === '1'),
// recovery: after the single guarded window.location.reload() has already landed
// once this session (the guard holds a *previous* document's identity),
// loadLazyWithRetry only converted the failure into a recoverable
// LazyChunkLoadError when isKnownDynamicImportFailure(error) was true. A corrupt /
// truncated chunk that parses as invalid JS rejects import() with a native
@@ -25,6 +25,10 @@ import { isLazyChunkLoadError, loadLazyWithRetry } from './lazy-with-retry'
// corrupt-chunk failure; these tests pin that behavior.
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
// Only a token left by a *different* document proves the reload landed; a token
// matching this document means the reload was requested and vetoed.
const LANDED_RELOAD_GUARD_VALUE = 'doc-before-the-reload'
const VETOED_RELOAD_GUARD_VALUE = (): string => String(performance.timeOrigin)
// The exact error the renderer received from the corrupt right-sidebar chunk.
const reportedCrashError = (): SyntaxError => new SyntaxError("Unexpected token ')'")
@@ -57,9 +61,9 @@ afterEach(() => {
describe('right-sidebar lazy chunk SyntaxError crash (regression)', () => {
it('recovers a corrupt right-sidebar chunk SyntaxError instead of surfacing it to the boundary', async () => {
const reload = spyOnReload()
// The one guarded reload already happened earlier this session: the user has
// navigated/reloaded once after the stale chunk was first hit.
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
// The one guarded reload already landed earlier this session: the guard was
// written by the document that reloaded, not by this one.
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
// import('./SourceControl') rejects with the native parse error from the
// corrupt chunk — exactly what the crash report captured.
@@ -87,7 +91,7 @@ describe('right-sidebar lazy chunk SyntaxError crash (regression)', () => {
// chunk, surfaced as a fetch failure, IS recovered; surfaced as a parse error,
// it is not. Both are the same unrecoverable corrupt right-sidebar chunk.
const recover = async (makeError: () => Error): Promise<unknown> => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const factory = vi.fn(() => Promise.reject(makeError()))
const loaded = loadLazyWithRetry(factory, { retries: 0, reloadKey: 'right-sidebar' })
try {
@@ -109,4 +113,31 @@ describe('right-sidebar lazy chunk SyntaxError crash (regression)', () => {
// The parse error from the very same corrupt chunk must be recovered too.
expect(isLazyChunkLoadError(parseOutcome)).toBe(true)
})
// Deliberate narrowing of the recovery above: a guard this document wrote itself
// means its reload() was vetoed, so no refetch ever happened and the sentinel
// would hide a live failure from the boundary.
it('surfaces the raw SyntaxError when this document is the one whose reload was vetoed', async () => {
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, VETOED_RELOAD_GUARD_VALUE())
const error = reportedCrashError()
const factory = vi.fn(() => Promise.reject(error))
const loaded = loadLazyWithRetry(factory, { retries: 0, reloadKey: 'right-sidebar' })
// Track settlement rather than awaiting: a regression leaves this pending forever.
let caught: unknown = 'pending'
void loaded.then(
(value) => {
caught = value
},
(rejection: unknown) => {
caught = rejection
}
)
await vi.advanceTimersByTimeAsync(5000)
expect(caught).toBe(error)
expect(isLazyChunkLoadError(caught)).toBe(false)
expect(reload).not.toHaveBeenCalled() // re-arming would loop against the veto
})
})
+62 -9
View File
@@ -5,6 +5,9 @@ import type { ComponentType } from 'react'
import { isLazyChunkLoadError, loadLazyWithRetry } from './lazy-with-retry'
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
// A guard holding a *different* document's token is the only proof the reload
// landed; a token matching this document means the reload was vetoed.
const LANDED_RELOAD_GUARD_VALUE = 'doc-before-the-reload'
const Comp: ComponentType = () => null
const chunkParseError = (): SyntaxError => new SyntaxError("Unexpected token ']'")
const chunkFetchError = (): TypeError =>
@@ -103,15 +106,45 @@ describe('loadLazyWithRetry', () => {
expect(factory).toHaveBeenCalledTimes(3)
expect(reload).toHaveBeenCalledTimes(1)
expect(window.sessionStorage.getItem(RELOAD_GUARD_KEY)).toBe('1')
// The load promise must suspend (never settle) while the page reloads, so the
// error boundary never flashes.
// The guard stores the requesting document's identity, not a bare flag.
expect(window.sessionStorage.getItem(RELOAD_GUARD_KEY)).toBe(String(performance.timeOrigin))
// The load promise must stay pending across the reload window, so the error
// boundary never flashes ahead of the navigation.
expect(settled).toBe(false)
})
// Crash b860def2: reload() was called at 19:05 and the document was still alive at
// 19:49 (no renderer bootstrap in between), so the requesting pane suspended forever.
it('surfaces the original error when the guarded reload never tears the document down', async () => {
const reload = spyOnReload()
stubCrashReportsBreadcrumb()
const error = chunkParseError()
const factory = vi.fn(() => Promise.reject(error))
const loaded = loadLazyWithRetry(factory, { retries: 0 })
let settled: unknown = 'pending'
void loaded.then(
(value) => {
settled = value
},
(rejection) => {
settled = rejection
}
)
await vi.advanceTimersByTimeAsync(5000)
expect(reload).toHaveBeenCalledTimes(1)
// Still suspended inside the grace window, so a landed reload shows no fallback.
expect(settled).toBe('pending')
await vi.advanceTimersByTimeAsync(10_000)
expect(settled).toBe(error)
expect(reload).toHaveBeenCalledTimes(1)
})
it('does NOT reload twice — wraps known chunk failures once the guard is already set', async () => {
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const error = chunkFetchError()
const factory = vi.fn(() => Promise.reject(error))
@@ -128,9 +161,29 @@ describe('loadLazyWithRetry', () => {
expect(isLazyChunkLoadError(caught)).toBe(true)
})
// Crash b860def2: the guarded reload() was vetoed, the document lived on for 44
// minutes, and the still-set guard turned the next failure into the sentinel that
// error boundaries suppress — hiding a recovery that never ran.
it('reports the original error when the guard belongs to this same document (reload vetoed)', async () => {
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, String(performance.timeOrigin))
const error = chunkParseError()
const factory = vi.fn(() => Promise.reject(error))
const loaded = loadLazyWithRetry(factory, { retries: 0 })
const assertion = expect(loaded).rejects.toBe(error)
await vi.advanceTimersByTimeAsync(5000)
await assertion
// Re-arming the reload here would loop against whatever vetoed the first one.
expect(reload).not.toHaveBeenCalled()
const caught = await loaded.catch((rejection) => rejection)
expect(isLazyChunkLoadError(caught)).toBe(false)
})
it('preserves the original error when the guarded failure is not a dynamic import failure', async () => {
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const error = new Error('render bug from lazy module evaluation')
const factory = vi.fn(() => Promise.reject(error))
@@ -152,7 +205,7 @@ describe('loadLazyWithRetry', () => {
// same dead import). Regression guard for crash report e08749bb (right
// sidebar, "Unexpected token ')'").
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const error = chunkParseError()
const factory = vi.fn(() => Promise.reject(error))
@@ -172,7 +225,7 @@ describe('loadLazyWithRetry', () => {
// An ordinary Error from a lazy module is a genuine evaluation bug, not a
// corrupt chunk; it must still surface raw after the guard is set.
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const error = new Error('render bug from lazy module evaluation')
const factory = vi.fn(() => Promise.reject(error))
@@ -250,14 +303,14 @@ describe('loadLazyWithRetry', () => {
it('keeps the reload guard set across a successful load (no second reload in one session)', async () => {
const reload = spyOnReload()
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const factory = vi.fn(() => Promise.resolve({ default: Comp }))
await loadLazyWithRetry(factory)
// The guard must survive a healthy load — otherwise a sibling chunk's success
// would re-arm the reload and an auto-mounted corrupt chunk would loop.
expect(window.sessionStorage.getItem(RELOAD_GUARD_KEY)).toBe('1')
expect(window.sessionStorage.getItem(RELOAD_GUARD_KEY)).toBe(LANDED_RELOAD_GUARD_VALUE)
expect(reload).not.toHaveBeenCalled()
})
})
+38 -10
View File
@@ -18,7 +18,7 @@ type AnyComponent = ComponentType<any>
type LazyFactory<T extends AnyComponent> = () => Promise<{ default: T }>
type ReloadGuardState = 'not-attempted' | 'attempted' | 'unavailable'
type ReloadGuardState = 'not-attempted' | 'reload-landed' | 'reload-not-landed' | 'unavailable'
export type LazyWithRetryOptions = {
retries?: number
@@ -46,15 +46,31 @@ export function isLazyChunkLoadError(error: unknown): error is LazyChunkLoadErro
// otherwise a sibling chunk's healthy load would re-arm the reload and an
// auto-mounted corrupt chunk would loop.
const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted'
// Stand-in document identity for hosts without a usable performance.timeOrigin; a
// reload re-evaluates this module, so a fresh value still means the reload landed.
const FALLBACK_RELOAD_TOKEN = `doc-${Math.random().toString(36).slice(2)}`
const DEFAULT_RETRIES = 2
const DEFAULT_BASE_DELAY_MS = 250
// The guard stores the requesting document's identity, not a bare flag: reload() can be
// vetoed (beforeunload preventDefault → Electron cancels the navigation) and a bare flag
// would then read as "recovery ran" forever inside a document that never reloaded (crash
// b860def2). timeOrigin changes per navigation, so a value still ours means no reload.
function currentDocumentReloadToken(): string {
const timeOrigin = typeof performance === 'undefined' ? Number.NaN : performance.timeOrigin
return Number.isFinite(timeOrigin) && timeOrigin > 0 ? String(timeOrigin) : FALLBACK_RELOAD_TOKEN
}
function readChunkReloadGuardState(): ReloadGuardState {
if (typeof window === 'undefined') {
return 'unavailable'
}
try {
return window.sessionStorage.getItem(RELOAD_GUARD_KEY) === '1' ? 'attempted' : 'not-attempted'
const stored = window.sessionStorage.getItem(RELOAD_GUARD_KEY)
if (stored === null) {
return 'not-attempted'
}
return stored === currentDocumentReloadToken() ? 'reload-not-landed' : 'reload-landed'
} catch {
// Why: when storage is blocked we cannot prove a reload happened, but still
// fail closed on reloads so a broken chunk never loops.
@@ -64,7 +80,7 @@ function readChunkReloadGuardState(): ReloadGuardState {
function markChunkReloadAttempted(): boolean {
try {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.sessionStorage.setItem(RELOAD_GUARD_KEY, currentDocumentReloadToken())
return true
} catch {
// A reload without a durable guard can loop, so treat write failure as unavailable.
@@ -86,9 +102,19 @@ function recordReloadBreadcrumb(reloadKey: string, message: string): void {
const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
// Suspends the React.lazy boundary while window.location.reload() tears the page
// down, so the error fallback never flashes in the moment before the reload lands.
const SUSPEND_UNTIL_RELOAD = new Promise<never>(() => undefined)
// Grace window for reload() to tear the document down. Long enough that the error
// fallback never flashes ahead of a real navigation.
const RELOAD_SETTLE_GRACE_MS = 10_000
// Suspends the boundary while reload() tears the page down. Suspending forever would
// hang the pane on a spinner when the reload is vetoed (Orca preventDefault()s
// beforeunload for dirty editor tabs), so surface the real failure once the grace
// window proves the navigation never happened.
function suspendUntilReloadLands(loadError: unknown): Promise<never> {
return new Promise<never>((_resolve, reject) => {
setTimeout(() => reject(loadError), RELOAD_SETTLE_GRACE_MS)
})
}
function isKnownDynamicImportFailure(error: unknown): boolean {
if (!(error instanceof Error)) {
@@ -150,15 +176,17 @@ export async function loadLazyWithRetry<T extends AnyComponent>(
lastError instanceof Error ? lastError.message : String(lastError)
)
window.location.reload()
return SUSPEND_UNTIL_RELOAD
return suspendUntilReloadLands(lastError)
}
if (reloadGuardState === 'attempted' && isKnownDynamicImportFailure(lastError)) {
if (reloadGuardState === 'reload-landed' && isKnownDynamicImportFailure(lastError)) {
throw new LazyChunkLoadError(lastError)
}
// No proven reload attempt (SSR / node / blocked storage) or unknown failure:
// re-throw the original error so normal error reporting semantics stay intact.
// No proven reload (SSR / node / blocked storage / a reload this document asked
// for but never got) or unknown failure: re-throw the original error so normal
// error reporting semantics stay intact. Boundaries suppress the sentinel, so
// wrapping an unproven recovery here would hide the failure entirely.
throw lastError
}