openvibers/openvibe.live Commits

Client-only changes go live with git pull — no restart, no dropped streams

OP OpenVibers committed 16 Sept 2026 · 47c0dbefd8 · 1 file, +21 −3

The SEO renderer read public/index.html once per process and cached rendered pages for five
minutes. Every client-side change bumps a ?v= in index.html, and those versioned assets are cached
immutably at Cloudflare, so shipping even a CSS fix needed a service restart before anyone saw it.
A restart drops every live RTMP stream — RTMP ingest belongs to this process; socket activation
only keeps HTTP up. Today that meant choosing between holding a security fix and cutting off the
site's top streamer mid-broadcast.

The base document is now re-read when index.html's mtime changes (checked at most every 2s), the
rendered-page cache is cleared when it does, and the check runs before the page cache is consulted
so cached routes pick it up too. Verified locally: editing a ?v= in index.html was served on /,
/vods, /pastes, /clips and /dashboard within 2s, with no restart, including routes that were
already cached.

Once this is deployed (it needs one restart itself), a change confined to public/ deploys with
`git pull` alone.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/server/seo/seo.js b/server/seo/seo.js
iindex 3cce1ee1..7cfa9e74 100644
---- a/server/seo/seo.js
++++ b/server/seo/seo.js
@@@ -402,10 +402,25 @@ function _detailSnapshot({ title, byline, desc, overview, transcript, canonicalP
}
// ── Inject metadata into the base index.html ────────────────────────────────────────────────
--let _baseHtml = null;
++// The base document is re-read when index.html changes on disk, checked at most every 2s.
++//
++// It used to be read once per process. Every client-side change bumps a ?v= in index.html, so
++// shipping even a CSS tweak needed a service restart to be seen — and a restart drops every live
++// RTMP stream, because RTMP ingest is owned by this process (socket activation only covers HTTP).
++// With this, a change that only touches public/ goes live with a git pull and no restart.
++let _baseHtml = null, _baseMtime = 0, _baseCheckedAt = 0;
function _base() {
-- if (_baseHtml == null) {
-- try { _baseHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8'); } catch { _baseHtml = ''; }
++ const now = Date.now();
++ if (_baseHtml == null || now - _baseCheckedAt > 2000) {
++ _baseCheckedAt = now;
++ let mtime = 0;
++ try { mtime = fs.statSync(INDEX_HTML_PATH).mtimeMs; } catch { /* keep what we have */ }
++ if (_baseHtml == null || (mtime && mtime !== _baseMtime)) {
++ try { _baseHtml = fs.readFileSync(INDEX_HTML_PATH, 'utf8'); _baseMtime = mtime; }
++ catch { if (_baseHtml == null) _baseHtml = ''; }
++ // Pages rendered from the old document reference old asset versions.
++ if (typeof _cache !== 'undefined') _cache.clear();
++ }
}
return _baseHtml;
}
@@@ -496,6 +511,9 @@ async function middleware(req, res, next) {
// Only rewrite HTML navigations, not fetch()/XHR/asset probes.
if (req.headers.accept && req.headers.accept.indexOf('text/html') === -1) return next();
try {
++ // Check for a changed index.html before trusting the page cache: a cache hit would otherwise
++ // skip the check and keep serving pages that point at the previous asset versions.
++ _base();
const cached = _cacheGet(p);
if (cached) { res.set('Cache-Control', 'public, max-age=300'); res.type('html'); return res.send(cached); }
const meta = await _pageMeta(p);