openvibers/openvibe-git Commits

An asset's version follows its file, with no restart (#54)

OP OpenVibers committed 17 h ago · 902d0568d5 · 2 files, +34 −3

public/ is a no-restart deploy path, but each asset's content hash was kept for the life of the process, so after a CSS-only deploy pages kept linking the old file (and Cloudflare kept serving it under the old ?v=). The new-repository form's radio fix (#52) deployed and stayed unseen. The hash is now kept only while the file's mtime, size and inode are unchanged.

Changes

ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 93c1038..65cc00b 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -27,12 +27,19 @@ const NAV = [
];
const API_BASE = 'https://git.openvibe.codes';
++// An asset's version is its content hash, kept until the file changes: a static-only deploy (public/ is a no-restart
++// path) swaps the file under a running process, and a hash kept for good sent pages on to the old file for as long as
++// the process lived (11-12: the new-repository form's fixed radios stayed broken after their deploy).
const hashes = new Map();
function assetVersion(rel) {
-- if (hashes.has(rel)) return hashes.get(rel);
++ const file = path.join(PUBLIC_DIR, rel);
++ let stamp = null;
++ try { const st = fs.statSync(file); stamp = `${st.mtimeMs}:${st.size}:${st.ino}`; } catch { /* missing asset */ }
++ const kept = hashes.get(rel);
++ if (kept && kept.stamp === stamp) return kept.v;
let v = 'dev';
-- try { v = crypto.createHash('sha256').update(fs.readFileSync(path.join(PUBLIC_DIR, rel))).digest('hex').slice(0, 10); } catch { /* missing asset */ }
-- hashes.set(rel, v);
++ try { v = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 10); } catch { /* missing asset */ }
++ hashes.set(rel, { stamp, v });
return v;
}
const asset = (rel) => `/${rel}?v=${assetVersion(rel)}`;
ddiff --git a/test/asset-version.test.js b/test/asset-version.test.js
nnew file mode 100644
iindex 0000000..4dfe535
---- /dev/null
++++ b/test/asset-version.test.js
@@@ -0,0 +1,24 @@
++'use strict';
++// An asset's ?v= follows its file: a static-only deploy swaps public/ under a running process, and the page must point
++// at the new file at once (11-12: a hash kept for the life of the process left the fixed radios unseen).
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { assetVersion, asset } = require('../server/render/layout');
++const rel = `css/__asset-version-${process.pid}.css`;
++const file = path.join(__dirname, '..', 'public', rel);
++try {
++ fs.writeFileSync(file, '.a{color:red}');
++ const first = assetVersion(rel);
++ assert.match(first, /^[0-9a-f]{10}$/);
++ assert.strictEqual(assetVersion(rel), first, 'unchanged file, same version');
++ fs.writeFileSync(file, '.a{color:blue}');
++ const later = new Date(Date.now() + 5000);
++ fs.utimesSync(file, later, later);
++ const second = assetVersion(rel);
++ assert.notStrictEqual(second, first, 'a changed file gets a new version without a restart');
++ assert.strictEqual(asset(rel), `/${rel}?v=${second}`);
++ fs.rmSync(file);
++ assert.strictEqual(assetVersion(rel), 'dev', 'a missing file is dev, not its old hash');
++ console.log('asset version: follows the file, no restart needed');
++} finally { fs.rmSync(file, { force: true }); }