Preserve legacy cache rules and pin service worker bytes at boot
This commit is contained in:
@@ -41,7 +41,7 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
||||
}
|
||||
}
|
||||
// Missing public is the historical fixed fallback used by local tests.
|
||||
if (!existsSync(publicDir)) return { manifest: { buildTag: override || 'dev-build', files: {}, groups: {}, contracts: {} }, index: null, sw: null, bytes };
|
||||
if (!existsSync(publicDir)) return { manifest: { buildTag: override || 'dev-build', files: {}, groups: {}, contracts: {} }, index: null, sw: null, swSource: null, bytes };
|
||||
walk(publicDir);
|
||||
const definition = bytes.has('/assets.json') ? JSON.parse(bytes.get('/assets.json').toString()) : {
|
||||
groups: { core: { contract: 1, eager: true, files: [...bytes.keys()].sort() } },
|
||||
@@ -74,10 +74,14 @@ export function createAssetManifest(publicDir = './public', { hashing = true, bu
|
||||
if (sw !== null) files['/sw.js'] = { ...files['/sw.js'], h: assetHash(sw), s: Buffer.byteLength(sw) };
|
||||
if (index !== null) bytes.set('/index.html', Buffer.from(index));
|
||||
if (sw !== null) bytes.set('/sw.js', Buffer.from(sw));
|
||||
return { manifest: { buildTag, files, groups, contracts }, index, sw, bytes };
|
||||
return { manifest: { buildTag, files, groups, contracts }, index, sw, swSource, bytes };
|
||||
}
|
||||
|
||||
export function assetCacheControl(path, version, manifest, hashing = true) {
|
||||
const shortCache = /(^|\/)(fonts|icons)\//.test(path) ? 'public, max-age=2592000' : 'no-cache';
|
||||
// The legacy index route always revalidates; legacy binary delivery ignored v.
|
||||
if (!hashing && path === '/index.html') return 'no-cache';
|
||||
if (!hashing && !/\.(?:js|css|html|json|webmanifest|svg|txt)$/i.test(path)) return shortCache;
|
||||
const current = hashing ? manifest.files[path]?.h : manifest.buildTag;
|
||||
if (version !== undefined) {
|
||||
if (version === current) return 'public, max-age=31536000, immutable';
|
||||
|
||||
@@ -350,7 +350,7 @@ registerCatalogRoutes(app, { resolveListener: recommendationListener });
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/version
|
||||
// Returns version string + a build tag that changes on every server restart/deploy.
|
||||
// Returns version and a deterministic tag that changes only with shipped files.
|
||||
// Clients poll this to detect when a new build is live and prompt a reload.
|
||||
app.get('/api/manifest', (c) => {
|
||||
const etag = `"${BUILD_TAG}"`;
|
||||
@@ -2432,15 +2432,9 @@ app.post('/api/admin/p2p/revoke', notes.requireAdminOrToken, async (c) => {
|
||||
// Served with no-store cache headers so browsers always re-fetch it and
|
||||
// pick up the substituted value rather than a browser-cached stale copy.
|
||||
// ============================================================================
|
||||
let _swSource = null;
|
||||
const _swSource = assets.swSource;
|
||||
app.get('/sw.js', (c) => {
|
||||
if (!_swSource) {
|
||||
try {
|
||||
_swSource = readFileSync('./public/sw.js', 'utf8');
|
||||
} catch {
|
||||
return c.text('Service worker not found', 404);
|
||||
}
|
||||
}
|
||||
if (_swSource === null) return c.text('Service worker not found', 404);
|
||||
// Inject the build tag: replace the whole fallback expression with the
|
||||
// real value. Matched by REGEX, not an exact string — an exact match broke
|
||||
// the moment the fallback literal in sw.js was bumped ('v1.0.3' → 'v1.0.4'),
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('static delivery characterization', () => {
|
||||
expect(fallback.buildTag).toBe('dev-build');
|
||||
});
|
||||
|
||||
test('index stamps only relative local css/js and serves expected cache headers', async () => {
|
||||
test('index stamps relative and root-local css/js and serves expected cache headers', async () => {
|
||||
const index = await fetch(`${server.base}/`);
|
||||
const html = await index.text();
|
||||
const { buildTag, files } = await (await fetch(`${server.base}/api/manifest`)).json();
|
||||
@@ -123,7 +123,6 @@ describe('static delivery characterization', () => {
|
||||
expect(await notModified.text()).toBe('');
|
||||
});
|
||||
|
||||
|
||||
test('manifest API, ETag/304 and hashes cover compressed and binary assets', async () => {
|
||||
const response = await fetch(`${server.base}/api/manifest`);
|
||||
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||
@@ -163,6 +162,9 @@ describe('static delivery characterization', () => {
|
||||
expect(html).toContain('href="/root.css"');
|
||||
expect((await fetch(`${server.base}/app.js?v=${buildTag}`)).headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
||||
expect((await fetch(`${server.base}/app.js?v=stale`)).headers.get('cache-control')).toBe('no-cache');
|
||||
expect((await fetch(`${server.base}/index.html?v=${buildTag}`)).headers.get('cache-control')).toBe('no-cache');
|
||||
expect((await fetch(`${server.base}/fonts/font.woff2?v=${buildTag}`)).headers.get('cache-control')).toBe('public, max-age=2592000');
|
||||
expect((await fetch(`${server.base}/fonts/font.woff2?v=stale`)).headers.get('cache-control')).toBe('public, max-age=2592000');
|
||||
} finally { await stopServer(server); server = await startServer(); }
|
||||
});
|
||||
|
||||
@@ -190,6 +192,20 @@ describe('static delivery characterization', () => {
|
||||
server = await startServer();
|
||||
});
|
||||
|
||||
test('served bytes remain pinned to their boot-time hashes', async () => {
|
||||
const before = await (await fetch(`${server.base}/app.js`)).text();
|
||||
const swBefore = await (await fetch(`${server.base}/sw.js`)).text();
|
||||
writeFileSync(join(publicDir, 'app.js'), 'changed after boot');
|
||||
writeFileSync(join(publicDir, 'sw.js'), 'changed after boot');
|
||||
try {
|
||||
expect(await (await fetch(`${server.base}/app.js`)).text()).toBe(before);
|
||||
expect(await (await fetch(`${server.base}/sw.js`)).text()).toBe(swBefore);
|
||||
} finally {
|
||||
writeFileSync(join(publicDir, 'app.js'), before);
|
||||
writeFileSync(join(publicDir, 'sw.js'), "const BUILD_TAG = 'missing-placeholder';\n");
|
||||
}
|
||||
});
|
||||
|
||||
test('SPA routes fall back to the stamped index', async () => {
|
||||
const { buildTag } = await (await fetch(`${server.base}/api/version`)).json();
|
||||
for (const path of ['/search', '/playlist/x']) {
|
||||
|
||||
Reference in New Issue
Block a user