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.

View File

@@ -45,6 +45,8 @@ When the server offers more than one admin language (`meta.locales` of `GET .../
A tab opened before a deploy would otherwise keep running the old admin against the new server, because the session refreshes silently. The SPA reads the build it booted with from the `summer-admin-build` meta of the served `index.html` and asks `<prefix>/build.json` again when the window gets focus, when the tab becomes visible and every 5 minutes while it is visible, at most once every 30 seconds. When the server answers a different build, a banner at the bottom right says that a new version is available, with Reload and Close buttons. The SPA never reloads by itself: Reload goes through the usual page reload, so a form with unsaved changes still asks first, and Close hides the banner until yet another build is deployed. When the served index carries no build, as under the Vite dev server, the check is off.
A tab that still runs the old build may ask for a script chunk the new build no longer has. A failed chunk load (Vite's `vite:preloadError` event) reloads the page once, which loads the new build. A timestamp in `sessionStorage` (`summer-admin.chunk-reload`) blocks a second automatic reload within 60 seconds, so a reload that does not help never loops, and without usable `sessionStorage` the SPA does not reload at all.
## Keyboard shortcuts
As in WinterCMS, a shortcut presses a button on the screen. Ctrl and Cmd are both accepted on every platform; the tooltips name Cmd (⌘) on macOS.