fix(auth): prevent repeated stale chunk reloads

Signed-off-by: ylhu16 <ylhu16@iflytek.com>
This commit is contained in:
ylhu16 2026-07-28 15:47:14 +08:00
parent 92ab2ddae7
commit 3c0aa56781
3 changed files with 105 additions and 9 deletions

View file

@ -32,6 +32,9 @@ function createLazyRouteComponent<TModule extends Record<string, unknown>>(
}
throw error
})
// Router resolution can finish before React.lazy imports the route module. Only clear the
// one-time reload guard after the chunk itself has loaded successfully.
clearDynamicImportReloadGuard()
return { default: module[exportName] as ComponentType<Record<string, unknown>> }
})
@ -483,10 +486,6 @@ export const router = createRouter({
defaultNotFoundComponent: DefaultNotFound,
})
router.subscribe('onResolved', () => {
clearDynamicImportReloadGuard()
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router

View file

@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
clearDynamicImportReloadGuard,
isDynamicImportFetchError,
recoverFromDynamicImportError,
} from './dynamic-import-recovery'
const values = new Map<string, string>()
const reload = vi.fn()
const sessionStorage = {
get length() {
return values.size
},
clear: vi.fn(() => values.clear()),
getItem: vi.fn((key: string) => values.get(key) ?? null),
key: vi.fn((index: number) => Array.from(values.keys())[index] ?? null),
removeItem: vi.fn((key: string) => values.delete(key)),
setItem: vi.fn((key: string, value: string) => values.set(key, value)),
} satisfies Storage
describe('dynamic import recovery', () => {
beforeEach(() => {
values.clear()
reload.mockClear()
vi.stubGlobal('window', {
location: { reload },
sessionStorage,
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
it.each([
'Failed to fetch dynamically imported module: /assets/login.js',
'error loading dynamically imported module: /assets/login.js',
'Importing a module script failed',
'ChunkLoadError: Loading chunk 42 failed',
])('recognizes a stale dynamic import error: %s', (message) => {
expect(isDynamicImportFetchError(new Error(message))).toBe(true)
})
it('ignores unrelated errors', () => {
expect(isDynamicImportFetchError(new Error('Request failed with status 500'))).toBe(false)
})
it('recognizes errors whose name is ChunkLoadError', () => {
const error = new Error('Loading chunk 42 failed')
error.name = 'ChunkLoadError'
expect(isDynamicImportFetchError(error)).toBe(true)
})
it('reloads only once while the recovery guard is active', () => {
const error = new Error('Failed to fetch dynamically imported module')
expect(recoverFromDynamicImportError(error)).toBe(true)
expect(recoverFromDynamicImportError(error)).toBe(false)
expect(recoverFromDynamicImportError(error)).toBe(false)
expect(reload).toHaveBeenCalledTimes(1)
})
it('allows recovery again after a dynamic import succeeds', () => {
const error = new Error('Failed to fetch dynamically imported module')
expect(recoverFromDynamicImportError(error)).toBe(true)
clearDynamicImportReloadGuard()
expect(recoverFromDynamicImportError(error)).toBe(true)
expect(reload).toHaveBeenCalledTimes(2)
})
it('does not mask the original import error when session storage is unavailable', () => {
vi.stubGlobal('window', {
location: { reload },
get sessionStorage() {
throw new DOMException('Access denied', 'SecurityError')
},
})
const error = new Error('Failed to fetch dynamically imported module')
expect(recoverFromDynamicImportError(error)).toBe(false)
expect(() => clearDynamicImportReloadGuard()).not.toThrow()
expect(reload).not.toHaveBeenCalled()
})
})

View file

@ -9,7 +9,8 @@ function resolveErrorMessage(error: unknown): string {
export function isDynamicImportFetchError(error: unknown): boolean {
const message = resolveErrorMessage(error)
return message.includes('Failed to fetch dynamically imported module')
return (error instanceof Error && error.name === 'ChunkLoadError')
|| message.includes('Failed to fetch dynamically imported module')
|| message.includes('error loading dynamically imported module')
|| message.includes('Importing a module script failed')
|| message.includes('ChunkLoadError')
@ -20,12 +21,17 @@ export function recoverFromDynamicImportError(error: unknown): boolean {
return false
}
if (window.sessionStorage.getItem(RELOAD_GUARD_KEY) === '1') {
window.sessionStorage.removeItem(RELOAD_GUARD_KEY)
let sessionStorage: Storage
try {
sessionStorage = window.sessionStorage
if (sessionStorage.getItem(RELOAD_GUARD_KEY) === '1') {
return false
}
sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
} catch {
return false
}
window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.location.reload()
return true
}
@ -34,5 +40,9 @@ export function clearDynamicImportReloadGuard(): void {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.removeItem(RELOAD_GUARD_KEY)
try {
window.sessionStorage.removeItem(RELOAD_GUARD_KEY)
} catch {
// Session storage can be unavailable in restricted browsing contexts.
}
}