fix: include Immich chunk manifest hash in inject-version
After an Immich upgrade, Svelte chunk filenames rotate but the cached PWA shell still imports old ones — dynamic imports 500 or 404. Our auto-update mechanism only caught changes to our injected script, not upstream changes, so the stuck PWA never self-healed. Now /api/custom/inject-version returns a hash of (custom script || Immich chunk manifest fingerprint), so any upgrade bumps the version, the PWA detects it on next poll, unregisters the service worker, clears caches, and reloads with fresh chunks. We extract only the /_app/immutable/... references from the HTML to avoid hashing server-rendered per-user state (CSRF tokens etc) that would cause spurious version churn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4cb479404a
commit
af3468e0d5
|
|
@ -13,15 +13,35 @@ const UPLOAD_TIMEOUT = 10 * 60 * 1000;
|
|||
|
||||
// Load the custom JS to inject. Version hash lets PWAs detect stale caches.
|
||||
const customJS = fs.readFileSync(path.join(__dirname, 'live-search.js'), 'utf8');
|
||||
const INJECT_VERSION = crypto
|
||||
const CUSTOM_JS_HASH = crypto
|
||||
.createHash('sha256')
|
||||
.update(customJS)
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
const SCRIPT_TAG =
|
||||
`<script>window.__LS_VERSION=${JSON.stringify(INJECT_VERSION)};</script>` +
|
||||
`<script>${customJS}</script>`;
|
||||
console.log(`Injected script version: ${INJECT_VERSION}`);
|
||||
|
||||
// `currentVersion` combines our custom script with a fingerprint of Immich's
|
||||
// served HTML so that any upstream update (chunk-hash rotation, etc.) also
|
||||
// bumps the version. Stuck PWAs polling /api/custom/inject-version will see
|
||||
// the change and self-reload. Initialized with just the custom hash until the
|
||||
// first HTML response lands.
|
||||
let currentVersion = CUSTOM_JS_HASH;
|
||||
console.log(`Injected script hash: ${CUSTOM_JS_HASH} (initial version: ${currentVersion})`);
|
||||
|
||||
function makeScriptTag(version) {
|
||||
return (
|
||||
`<script>window.__LS_VERSION=${JSON.stringify(version)};</script>` +
|
||||
`<script>${customJS}</script>`
|
||||
);
|
||||
}
|
||||
|
||||
// Extract just the chunk/manifest references from Immich's HTML (the parts
|
||||
// that actually change on upgrades). Hashing the full HTML would include
|
||||
// server-rendered state (CSRF tokens, session user id) that change per-user
|
||||
// per-request and would cause spurious version churn.
|
||||
function extractShellFingerprint(html) {
|
||||
const matches = html.match(/\/_app\/immutable\/[^"'\s]+/g) || [];
|
||||
return matches.sort().join('|');
|
||||
}
|
||||
|
||||
// Helper: make an internal request to Immich (for small JSON requests only)
|
||||
function immichRequest(method, apiPath, headers, body) {
|
||||
|
|
@ -57,7 +77,7 @@ const server = http.createServer((req, res) => {
|
|||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate'
|
||||
});
|
||||
res.end(JSON.stringify({ version: INJECT_VERSION }));
|
||||
res.end(JSON.stringify({ version: currentVersion }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +149,20 @@ const server = http.createServer((req, res) => {
|
|||
proxyRes.on('data', chunk => htmlChunks.push(chunk));
|
||||
proxyRes.on('end', () => {
|
||||
let html = Buffer.concat(htmlChunks).toString('utf8');
|
||||
html = html.replace('</body>', SCRIPT_TAG + '</body>');
|
||||
|
||||
// Compute a per-response version: custom script hash XOR'd
|
||||
// with the upstream HTML's chunk/manifest fingerprint. This
|
||||
// way an Immich upgrade that rotates chunk hashes bumps our
|
||||
// version and stuck PWAs will self-heal on the next poll.
|
||||
const shellHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(customJS)
|
||||
.update(extractShellFingerprint(html))
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
currentVersion = shellHash;
|
||||
|
||||
html = html.replace('</body>', makeScriptTag(shellHash) + '</body>');
|
||||
|
||||
const resHeaders = { ...proxyRes.headers };
|
||||
delete resHeaders['content-length'];
|
||||
|
|
|
|||
Loading…
Reference in New Issue