import type { Router } from 'vue-router' const RECOVERY_KEY = 'client-static-asset-recovery' const RECOVERY_COOLDOWN_MS = 60_000 const RECOVERY_STABLE_MS = 15_000 interface RecoveryRecord { href: string refreshedAt: number } function errorMessage(error: unknown) { if (error instanceof Error) return `${error.name}: ${error.message}` return typeof error === 'string' ? error : '' } export function isStaticAssetLoadError(error: unknown) { return /ChunkLoadError|Loading chunk [\w-]+ failed|Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS/i.test(errorMessage(error)) } function readRecoveryRecord(): RecoveryRecord | null { try { const raw = window.sessionStorage.getItem(RECOVERY_KEY) if (!raw) return null const value = JSON.parse(raw) as Partial return typeof value.href === 'string' && typeof value.refreshedAt === 'number' ? { href: value.href, refreshedAt: value.refreshedAt } : null } catch { return null } } function writeRecoveryRecord(record: RecoveryRecord) { try { window.sessionStorage.setItem(RECOVERY_KEY, JSON.stringify(record)) return true } catch { // Without a persistent guard, reloading could loop while the deployment is still unavailable. return false } } function clearRecoveryRecord() { try { window.sessionStorage.removeItem(RECOVERY_KEY) } catch { // Storage availability must not affect application startup. } } function recoverFromStaticAssetFailure(error: unknown, confirmedAssetFailure = false) { if (!confirmedAssetFailure && !isStaticAssetLoadError(error)) return false const now = Date.now() const previous = readRecoveryRecord() if (previous?.href === window.location.href && now - previous.refreshedAt < RECOVERY_COOLDOWN_MS) return false if (!writeRecoveryRecord({ href: window.location.href, refreshedAt: now })) return false window.location.reload() return true } function failedApplicationAsset(event: Event) { const target = event.target if (target instanceof HTMLScriptElement) return target.src if (target instanceof HTMLLinkElement && ['stylesheet', 'modulepreload', 'preload'].includes(target.rel)) return target.href return '' } export function installStaticAssetRecovery(router: Router) { window.addEventListener('vite:preloadError', (event) => { const preloadEvent = event as Event & { payload?: unknown } if (recoverFromStaticAssetFailure(preloadEvent.payload, true)) preloadEvent.preventDefault() }) window.addEventListener('error', (event) => { const assetUrl = failedApplicationAsset(event) if (!assetUrl) return try { if (new URL(assetUrl, window.location.href).origin !== window.location.origin) return } catch { return } recoverFromStaticAssetFailure(assetUrl, true) }, true) router.onError((error) => { recoverFromStaticAssetFailure(error) }) window.setTimeout(clearRecoveryRecord, RECOVERY_STABLE_MS) }