75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
const RECOVERY_STORAGE_KEY = 'cc-admin-route-asset-recovery:v1'
|
|
const RECOVERY_WINDOW_MS = 30_000
|
|
|
|
const staleAssetErrorPatterns = [
|
|
/failed to fetch dynamically imported module/i,
|
|
/error loading dynamically imported module/i,
|
|
/importing a module script failed/i,
|
|
/chunkloaderror/i,
|
|
/loading (css )?chunk [\w-]+ failed/i,
|
|
]
|
|
|
|
export interface NavigationRecoveryDependencies {
|
|
storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
|
|
now: () => number
|
|
hasUnsavedWork: () => boolean
|
|
confirmDiscard: () => Promise<boolean>
|
|
discardUnsavedWork: () => void
|
|
navigate: (target: string) => void
|
|
showError: (message: string) => void
|
|
}
|
|
|
|
interface RecoveryAttempt {
|
|
target: string
|
|
attemptedAt: number
|
|
}
|
|
|
|
function errorMessage(error: unknown) {
|
|
if (error instanceof Error) return error.message
|
|
return typeof error === 'string' ? error : ''
|
|
}
|
|
|
|
export function isStaleRouteAssetError(error: unknown) {
|
|
const message = errorMessage(error)
|
|
return staleAssetErrorPatterns.some((pattern) => pattern.test(message))
|
|
}
|
|
|
|
function recentAttempt(dependencies: NavigationRecoveryDependencies, target: string) {
|
|
try {
|
|
const parsed = JSON.parse(dependencies.storage.getItem(RECOVERY_STORAGE_KEY) || 'null') as RecoveryAttempt | null
|
|
return Boolean(parsed
|
|
&& parsed.target === target
|
|
&& dependencies.now() - parsed.attemptedAt < RECOVERY_WINDOW_MS)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function recoverNavigationError(
|
|
error: unknown,
|
|
target: string,
|
|
dependencies: NavigationRecoveryDependencies,
|
|
) {
|
|
if (!isStaleRouteAssetError(error)) {
|
|
dependencies.showError('页面切换失败,请重试')
|
|
return false
|
|
}
|
|
if (recentAttempt(dependencies, target)) {
|
|
dependencies.showError('页面资源加载失败,请稍后重试或手动刷新')
|
|
return false
|
|
}
|
|
if (dependencies.hasUnsavedWork() && !await dependencies.confirmDiscard()) return true
|
|
|
|
dependencies.storage.setItem(RECOVERY_STORAGE_KEY, JSON.stringify({
|
|
target,
|
|
attemptedAt: dependencies.now(),
|
|
} satisfies RecoveryAttempt))
|
|
dependencies.discardUnsavedWork()
|
|
dependencies.navigate(target)
|
|
return true
|
|
}
|
|
|
|
export function clearNavigationRecoveryAttempt(storage: Pick<Storage, 'removeItem'>) {
|
|
storage.removeItem(RECOVERY_STORAGE_KEY)
|
|
}
|