feat(admin): reload once when a chunk fails to load after a deploy (quick-261028-rqb)

- installChunkReloadGuard reloads on vite:preloadError, at most once per
  60 s through a sessionStorage timestamp; no storage means no reload
This commit is contained in:
Jakub Zych
2026-10-28 21:21:28 +01:00
parent 291b18fb1e
commit 64452a4919
3 changed files with 68 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
// Reload once when a chunk fails to load. After a deploy, an old tab that
// asks for a hashed chunk the new build removed fails the dynamic import, and
// Vite's preload helper dispatches vite:preloadError on window. One reload
// loads the new index and its chunks. A timestamp in sessionStorage stops a
// reload loop when the reload does not help, and without usable storage the
// guard never reloads. It does not call preventDefault: the error still
// reaches the importer (RichEditorField switches to source mode), so when the
// guard declines, the existing failure state shows.
export const CHUNK_RELOAD_KEY = 'summer-admin.chunk-reload'
/** No second automatic reload within 60 seconds of the last one. */
export const CHUNK_RELOAD_WINDOW = 60_000
/**
* Whether the guard may reload now. It records now under CHUNK_RELOAD_KEY
* when it says yes. Null storage, a reload within CHUNK_RELOAD_WINDOW or
* storage that throws means no.
*/
export function shouldReload(storage: Storage | null, now: number): boolean {
if (storage === null) {
return false
}
try {
const last = Number.parseInt(storage.getItem(CHUNK_RELOAD_KEY) ?? '', 10)
if (Number.isFinite(last) && now - last >= 0 && now - last < CHUNK_RELOAD_WINDOW) {
return false
}
storage.setItem(CHUNK_RELOAD_KEY, String(now))
return true
} catch {
return false
}
}
function sessionStore(): Storage | null {
try {
return window.sessionStorage
} catch {
return null
}
}
export interface ChunkReloadOptions {
target?: Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
storage?: () => Storage | null
reload?: () => void
now?: () => number
}
/** Installs the vite:preloadError guard and returns its uninstall function. */
export function installChunkReloadGuard(options: ChunkReloadOptions = {}): () => void {
const target = options.target ?? window
const storage = options.storage ?? sessionStore
const reload = options.reload ?? (() => window.location.reload())
const now = options.now ?? Date.now
const listener = (): void => {
if (shouldReload(storage(), now())) {
reload()
}
}
target.addEventListener('vite:preloadError', listener)
return () => target.removeEventListener('vite:preloadError', listener)
}

View File

@@ -3,6 +3,7 @@ import App from './App.vue'
import { createAdminRouter } from './app/router'
import { onUnauthorized } from './api/client'
import { startBuildWatch } from './app/buildUpdate'
import { installChunkReloadGuard } from './app/chunkReload'
import { loadFieldTypes } from './app/fieldTypes'
import { loadStrings } from './app/i18n'
import { expireSession, me } from './state/useAuth'
@@ -12,6 +13,8 @@ import { loadSettings } from './state/useSettings'
import './styles/main.css'
async function boot(): Promise<void> {
// A chunk that fails to load after a deploy reloads the page once.
installChunkReloadGuard()
// The last theme choice (system unless the admin picked light or dark) is
// applied before the first await, so nothing renders in the wrong theme;
// me() then applies the admin's stored preference.