feat(app): detect new deploy and prompt refresh (+ lazy-chunk recovery)
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>
This commit is contained in:
46
src/__tests__/app-version-store.test.ts
Normal file
46
src/__tests__/app-version-store.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import {
|
||||
reportServerVersion,
|
||||
subscribeNewVersion,
|
||||
getNewVersion,
|
||||
BUILD_VERSION,
|
||||
__resetVersionStateForTest,
|
||||
} from "../admin/utils/appVersion";
|
||||
|
||||
/**
|
||||
* Client store backing the "new version available" banner. The server stamps
|
||||
* X-App-Version on every response; apiFetch feeds it here. A version that
|
||||
* differs from the one baked into THIS bundle (BUILD_VERSION) means a deploy
|
||||
* happened → latch it and notify subscribers (the banner).
|
||||
*/
|
||||
|
||||
describe("appVersion store", () => {
|
||||
beforeEach(() => __resetVersionStateForTest());
|
||||
|
||||
it("ignores the same version, empty, or null", () => {
|
||||
reportServerVersion(BUILD_VERSION);
|
||||
reportServerVersion("");
|
||||
reportServerVersion(null);
|
||||
reportServerVersion(undefined);
|
||||
expect(getNewVersion()).toBeNull();
|
||||
});
|
||||
|
||||
it("latches + notifies once when the server reports a different version", () => {
|
||||
let notified = 0;
|
||||
const unsub = subscribeNewVersion(() => {
|
||||
notified++;
|
||||
});
|
||||
|
||||
reportServerVersion(`${BUILD_VERSION}-next`);
|
||||
expect(getNewVersion()).toBe(`${BUILD_VERSION}-next`);
|
||||
expect(notified).toBe(1);
|
||||
|
||||
// Idempotent: once a new version is known, further reports don't re-notify
|
||||
// or overwrite (the banner is already showing).
|
||||
reportServerVersion("99.0.0");
|
||||
expect(getNewVersion()).toBe(`${BUILD_VERSION}-next`);
|
||||
expect(notified).toBe(1);
|
||||
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
24
src/__tests__/version-header.test.ts
Normal file
24
src/__tests__/version-header.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Fastify from "fastify";
|
||||
import { registerVersionHeader, readAppVersion } from "../middleware/version";
|
||||
|
||||
/**
|
||||
* The server stamps every response with X-App-Version so the SPA can detect a
|
||||
* deploy (the client compares it to the version baked into its bundle).
|
||||
*/
|
||||
|
||||
describe("registerVersionHeader", () => {
|
||||
it("adds X-App-Version to responses", async () => {
|
||||
const app = Fastify({ logger: false });
|
||||
registerVersionHeader(app, "9.9.9");
|
||||
app.get("/ping", async () => ({ ok: true }));
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/ping" });
|
||||
expect(res.headers["x-app-version"]).toBe("9.9.9");
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("defaults to the package.json version (semver string)", () => {
|
||||
expect(readAppVersion()).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user