When a new version is deployed, open tabs no longer silently run stale code until a manual F5. - Server stamps every response with X-App-Version (src/middleware/version.ts, onSend hook in server.ts). - Client bakes its build version via Vite define (__APP_VERSION__); apiFetch compares the header to it (no polling — piggybacks existing traffic) and latches a mismatch in a small store (appVersion.ts). - UpdateBanner: a persistent bottom Snackbar "Je k dispozici nová verze aplikace. — Obnovit" → location.reload(). User-initiated, so an in-progress form is never interrupted. - lazyWithReload: every React.lazy page reloads once if its hashed chunk 404s after a deploy (sessionStorage-guarded against loops), fixing the "old chunk gone" error screen. Approach chosen after researching current SPA practice (version header + banner beats polling / a service worker for a non-PWA admin app). Tests: +4 (server header, client store latch/notify). Full suite 669 pass, tsc -b clean, lint 0. Verified live via Playwright (header emitted; banner appears on a simulated mismatch). Detection applies to deploys from the NEXT release onward (current clients lack the detection code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
/**
|
|
* New-deploy detection for the SPA.
|
|
*
|
|
* The server stamps every response with `X-App-Version`. `apiFetch` feeds that
|
|
* header here via `reportServerVersion`. When it differs from the version baked
|
|
* into THIS bundle at build time (`BUILD_VERSION`), a deploy has happened — we
|
|
* latch it and notify subscribers (the refresh banner). We never force a
|
|
* reload; the user picks the moment (they may be mid-form).
|
|
*/
|
|
|
|
// Injected by Vite `define` at build time; falls back to "dev" in tests / when
|
|
// running unbuilt. `typeof` guard avoids a ReferenceError when undefined.
|
|
declare const __APP_VERSION__: string;
|
|
export const BUILD_VERSION: string =
|
|
typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "dev";
|
|
|
|
let newVersion: string | null = null;
|
|
const listeners = new Set<() => void>();
|
|
|
|
/**
|
|
* Compare a server-reported version against this bundle's build version. Latches
|
|
* the first genuine mismatch and notifies subscribers exactly once.
|
|
*/
|
|
export function reportServerVersion(
|
|
serverVersion: string | null | undefined,
|
|
): void {
|
|
if (newVersion) return; // already latched — banner is up
|
|
if (!serverVersion || serverVersion === BUILD_VERSION) return;
|
|
newVersion = serverVersion;
|
|
for (const l of listeners) l();
|
|
}
|
|
|
|
export function subscribeNewVersion(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
export function getNewVersion(): string | null {
|
|
return newVersion;
|
|
}
|
|
|
|
/** Test-only: reset the module singleton between cases. */
|
|
export function __resetVersionStateForTest(): void {
|
|
newVersion = null;
|
|
listeners.clear();
|
|
}
|