openvibers/openvibe.live Commits
Live declares its release components, so a stylesheet-only release is swapped into open tabs in place (D46 scenario 1; Host docs/release-acceptance.md gate 1e, which was open). server/web/release-components.js has three components. styles: every stylesheet under public/css, kind style, applied in place with no reload, so a broadcaster's desk or a viewer's player never reloads for a colour. shell: scripts, pages, fragments, the feature registry, the service worker, OBS overlays, server/web, server/seo and the lockfile (the /shared files come from the pin), kind script, prompted. server: the rest, nothing to do in tabs. Before, Live declared none, so every release prompted a reload. The release root is the current link (OV_APP_ROOT) and the manifest is rebuilt when files change (recheckMs, lazily, no timer). The release id is read from where current points at every rebuild, so a static-only switch, which does not restart the process, reports the release it is. test/release-components.test.js: a stylesheet changes styles only, a script the shell, server code server only, page-stamping server code the shell, and the real tree lists all its stylesheets. npm test 142/142.
Changes
@@@ -152,13 +152,24 @@ const server = http.createServer(app);// What this server runs (ADR-016): GET /release.json below, and release_info in /metrics.// In the release layout (/opt/openvibe.live/releases/<time>-<sha8>, a root-owned git worktree) the// service user cannot run git there, so the release id also comes from the directory's name.--const release = require('openvibe-shared/release').createRelease({ service: 'live', root: path.join(__dirname, '..'), env: releaseEnv() });++// Components (server/web/release-components.js): a styles-only release is swapped into open tabs in place; the++// root is the `current` link (OV_APP_ROOT), and the manifest is rebuilt when files change (recheckMs), so a++// static-only release switch, which does not restart this process, is reported as the release it is.++const appRoot = process.env.OV_APP_ROOT || path.join(__dirname, '..');++const release = require('openvibe-shared/release').createRelease({++ service: 'live', root: appRoot, env: releaseEnv(), components: require('./web/release-components').componentsFor(appRoot), recheckMs: 30000,++});function releaseEnv() {if (process.env.RELEASE_COMMIT) return process.env;-- let dir = path.join(__dirname, '..');-- try { dir = require('fs').realpathSync(dir); } catch { /* keep */ }-- const m = /-([0-9a-f]{7,40})$/.exec(path.basename(dir));-- return m ? { ...process.env, RELEASE_COMMIT: m[1] } : process.env;++ // The release id is the directory `current` points at (…/releases/<time>-<sha8>), read at every rebuild.++ const env = { ...process.env };++ Object.defineProperty(env, 'RELEASE_COMMIT', { enumerable: true, get() {++ let dir = appRoot;++ try { dir = require('fs').realpathSync(appRoot); } catch { /* keep */ }++ const m = /-([0-9a-f]{7,40})$/.exec(path.basename(dir));++ return m ? m[1] : undefined;++ } });++ return env;}// Metrics first, so every request is counted (by route template, never by raw URL). GET /metrics// answers direct loopback callers only; through nginx it is a 404 (server/web/observability.js).nnew file mode 100644@@@ -0,0 +1,49 @@++'use strict';++/**++ * What an open tab does with each Live release (ADR-016, D43/D46 scenario 1; openvibe-shared/release components):++ *++ * styles every stylesheet under public/css (kind style): a styles-only release is swapped into open tabs in++ * place, so a broadcaster's desk or a viewer's player is never reloaded for a colour or a margin++ * shell every other file the page runs (kind script): scripts, pages, fragments, the feature registry, the++ * service worker, the OBS overlays, the server code that stamps pages (server/web, server/seo) and the++ * lockfile (the /shared files come from the openvibe-shared pin): a change prompts, never reloads mid-use++ * server the rest of server/ (kind server): nothing to do in open tabs++ * Images and other media need nothing. Before this, Live declared no components, so every release (a stylesheet++ * included) prompted open tabs to reload (Host docs/release-acceptance.md, gate 1e).++ */++const fs = require('fs');++const path = require('path');++++const SHELL_FILES = [++ 'public/js', 'public/fragments', 'public/obs', 'public/index.html', 'public/features.json', 'public/service-worker.js',++ 'public/live-notify.js', 'public/manifest.webmanifest', 'public/banned.html', 'public/dmca.html', 'public/kiosk.html',++ 'public/media-player.html', 'public/popout-chat.html', 'public/privacy.html', 'public/tos.html', 'public/whip-publisher.html',++ 'server/web', 'server/seo', 'package-lock.json',++];++++/** Every stylesheet under <root>/public/css as a URL path (/css/…), sorted. */++function stylesheets(root) {++ const base = path.join(root, 'public');++ const out = [];++ const walk = (dir) => {++ let entries = [];++ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }++ for (const e of entries) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p);++ else if (e.name.endsWith('.css')) out.push('/' + path.relative(base, p).split(path.sep).join('/'));++ }++ };++ walk(path.join(base, 'css'));++ return out.sort();++}++++function componentsFor(root) {++ return {++ styles: { kind: 'style', assets: stylesheets(root) },++ shell: { kind: 'script', files: SHELL_FILES.filter((f) => fs.existsSync(path.join(root, f))) },++ server: { kind: 'server', files: ['server'] },++ };++}++++module.exports = { componentsFor, stylesheets, SHELL_FILES };nnew file mode 100644@@@ -0,0 +1,43 @@++'use strict';++// Live's release components (server/web/release-components.js; Host docs/release-acceptance.md gate 1e): a++// stylesheet-only release changes only `styles` (kind style: open tabs swap it in place, nothing reloads), a script++// or page change changes `shell` (prompted), server code only `server`; the real tree lists every stylesheet.++// node test/release-components.test.js++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { createRelease } = require('openvibe-shared/release');++const { componentsFor, stylesheets } = require('../server/web/release-components');++++const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-live-rel-'));++const put = (p, s) => { fs.mkdirSync(path.dirname(path.join(root, p)), { recursive: true }); fs.writeFileSync(path.join(root, p), s); };++put('public/css/style.css', 'body{color:red}');++put('public/css/features/chat.css', '.c{}');++put('public/js/app.js', 'console.log(1)');++put('public/index.html', '<html></html>');++put('server/index.js', 'module.exports=1');++put('server/web/assets.js', 'module.exports=2');++put('package-lock.json', '{}');++try {++ const rel = createRelease({ service: 'live', root, env: { RELEASE_COMMIT: 'abcdef1' }, components: componentsFor(root), recheckMs: 0 });++ const v = () => { const m = rel.refresh ? rel.refresh() : rel.full(); const c = (m && m.components) || rel.full().components; return { styles: c.styles.version, shell: c.shell.version, server: c.server.version, kinds: [c.styles.kind, c.shell.kind, c.server.kind] }; };++ const a = v();++ assert.deepStrictEqual(a.kinds, ['style', 'script', 'server']);++ put('public/css/style.css', 'body{color:blue}');++ const b = v();++ assert.deepStrictEqual([b.styles !== a.styles, b.shell === a.shell, b.server === a.server], [true, true, true], 'a stylesheet changes styles only');++ put('public/js/app.js', 'console.log(2)');++ const c = v();++ assert.deepStrictEqual([c.shell !== b.shell, c.styles === b.styles], [true, true], 'a script changes the shell');++ put('server/index.js', 'module.exports=3');++ const d = v();++ assert.deepStrictEqual([d.server !== c.server, d.shell === c.shell, d.styles === c.styles], [true, true, true], 'server-only code changes server only');++ put('server/web/assets.js', 'module.exports=4');++ assert.notStrictEqual(v().shell, d.shell, 'the page-stamping server code is shell');++ // The real tree: every stylesheet, and the files named exist.++ const real = componentsFor(path.join(__dirname, '..'));++ assert.ok(real.styles.assets.includes('/css/style.css') && real.styles.assets.length === stylesheets(path.join(__dirname, '..')).length && real.styles.assets.length >= 20, `${real.styles.assets.length} stylesheets`);++ assert.ok(real.shell.files.includes('public/js') && real.shell.files.includes('server/web'));++} finally { fs.rmSync(root, { recursive: true, force: true }); }++console.log('release components: all checks passed');
@@ -152,13 +152,24 @@ const server = http.createServer(app);
@@ -152,13 +152,24 @@ const server = http.createServer(app);
// What this server runs (ADR-016): GET /release.json below, and release_info in /metrics.
// What this server runs (ADR-016): GET /release.json below, and release_info in /metrics.
// In the release layout (/opt/openvibe.live/releases/<time>-<sha8>, a root-owned git worktree) the
// In the release layout (/opt/openvibe.live/releases/<time>-<sha8>, a root-owned git worktree) the
// service user cannot run git there, so the release id also comes from the directory's name.
// service user cannot run git there, so the release id also comes from the directory's name.
-const release = require('openvibe-shared/release').createRelease({ service: 'live', root: path.join(__dirname, '..'), env: releaseEnv() });
+// Components (server/web/release-components.js): a styles-only release is swapped into open tabs in place; the
+// root is the `current` link (OV_APP_ROOT), and the manifest is rebuilt when files change (recheckMs), so a
+// static-only release switch, which does not restart this process, is reported as the release it is.
+const appRoot = process.env.OV_APP_ROOT || path.join(__dirname, '..');
+const release = require('openvibe-shared/release').createRelease({
+ service: 'live', root: appRoot, env: releaseEnv(), components: require('./web/release-components').componentsFor(appRoot), recheckMs: 30000,
+});
function releaseEnv() {
function releaseEnv() {
if (process.env.RELEASE_COMMIT) return process.env;
if (process.env.RELEASE_COMMIT) return process.env;
- let dir = path.join(__dirname, '..');
- try { dir = require('fs').realpathSync(dir); } catch { /* keep */ }
- const m = /-([0-9a-f]{7,40})$/.exec(path.basename(dir));
- return m ? { ...process.env, RELEASE_COMMIT: m[1] } : process.env;
+ // The release id is the directory `current` points at (…/releases/<time>-<sha8>), read at every rebuild.
+ const env = { ...process.env };
+ Object.defineProperty(env, 'RELEASE_COMMIT', { enumerable: true, get() {
+ let dir = appRoot;
+ try { dir = require('fs').realpathSync(appRoot); } catch { /* keep */ }
+ const m = /-([0-9a-f]{7,40})$/.exec(path.basename(dir));
+ return m ? m[1] : undefined;
+ } });
+ return env;
}
}
// Metrics first, so every request is counted (by route template, never by raw URL). GET /metrics
// Metrics first, so every request is counted (by route template, never by raw URL). GET /metrics
// answers direct loopback callers only; through nginx it is a 404 (server/web/observability.js).
// answers direct loopback callers only; through nginx it is a 404 (server/web/observability.js).
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
+'use strict';
+/**
+ * What an open tab does with each Live release (ADR-016, D43/D46 scenario 1; openvibe-shared/release components):
+ *
+ * styles every stylesheet under public/css (kind style): a styles-only release is swapped into open tabs in
+ * place, so a broadcaster's desk or a viewer's player is never reloaded for a colour or a margin
+ * shell every other file the page runs (kind script): scripts, pages, fragments, the feature registry, the
+ * service worker, the OBS overlays, the server code that stamps pages (server/web, server/seo) and the
+ * lockfile (the /shared files come from the openvibe-shared pin): a change prompts, never reloads mid-use
+ * server the rest of server/ (kind server): nothing to do in open tabs
+ * Images and other media need nothing. Before this, Live declared no components, so every release (a stylesheet
+ * included) prompted open tabs to reload (Host docs/release-acceptance.md, gate 1e).
+ */
+const fs = require('fs');
+const path = require('path');
+
+const SHELL_FILES = [
+ 'public/js', 'public/fragments', 'public/obs', 'public/index.html', 'public/features.json', 'public/service-worker.js',
+ 'public/live-notify.js', 'public/manifest.webmanifest', 'public/banned.html', 'public/dmca.html', 'public/kiosk.html',
+ 'public/media-player.html', 'public/popout-chat.html', 'public/privacy.html', 'public/tos.html', 'public/whip-publisher.html',
+ 'server/web', 'server/seo', 'package-lock.json',
+];
+
+/** Every stylesheet under <root>/public/css as a URL path (/css/…), sorted. */
+function stylesheets(root) {
+ const base = path.join(root, 'public');
+ const out = [];
+ const walk = (dir) => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const e of entries) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p);
+ else if (e.name.endsWith('.css')) out.push('/' + path.relative(base, p).split(path.sep).join('/'));
+ }
+ };
+ walk(path.join(base, 'css'));
+ return out.sort();
+}
+
+function componentsFor(root) {
+ return {
+ styles: { kind: 'style', assets: stylesheets(root) },
+ shell: { kind: 'script', files: SHELL_FILES.filter((f) => fs.existsSync(path.join(root, f))) },
+ server: { kind: 'server', files: ['server'] },
+ };
+}
+
+module.exports = { componentsFor, stylesheets, SHELL_FILES };
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+'use strict';
+// Live's release components (server/web/release-components.js; Host docs/release-acceptance.md gate 1e): a
+// stylesheet-only release changes only `styles` (kind style: open tabs swap it in place, nothing reloads), a script
+// or page change changes `shell` (prompted), server code only `server`; the real tree lists every stylesheet.
+// node test/release-components.test.js
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { createRelease } = require('openvibe-shared/release');
+const { componentsFor, stylesheets } = require('../server/web/release-components');
+
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-live-rel-'));
+const put = (p, s) => { fs.mkdirSync(path.dirname(path.join(root, p)), { recursive: true }); fs.writeFileSync(path.join(root, p), s); };
+put('public/css/style.css', 'body{color:red}');
+put('public/css/features/chat.css', '.c{}');
+put('public/js/app.js', 'console.log(1)');
+put('public/index.html', '<html></html>');
+put('server/index.js', 'module.exports=1');
+put('server/web/assets.js', 'module.exports=2');
+put('package-lock.json', '{}');
+try {
+ const rel = createRelease({ service: 'live', root, env: { RELEASE_COMMIT: 'abcdef1' }, components: componentsFor(root), recheckMs: 0 });
+ const v = () => { const m = rel.refresh ? rel.refresh() : rel.full(); const c = (m && m.components) || rel.full().components; return { styles: c.styles.version, shell: c.shell.version, server: c.server.version, kinds: [c.styles.kind, c.shell.kind, c.server.kind] }; };
+ const a = v();
+ assert.deepStrictEqual(a.kinds, ['style', 'script', 'server']);
+ put('public/css/style.css', 'body{color:blue}');
+ const b = v();
+ assert.deepStrictEqual([b.styles !== a.styles, b.shell === a.shell, b.server === a.server], [true, true, true], 'a stylesheet changes styles only');
+ put('public/js/app.js', 'console.log(2)');
+ const c = v();
+ assert.deepStrictEqual([c.shell !== b.shell, c.styles === b.styles], [true, true], 'a script changes the shell');
+ put('server/index.js', 'module.exports=3');
+ const d = v();
+ assert.deepStrictEqual([d.server !== c.server, d.shell === c.shell, d.styles === c.styles], [true, true, true], 'server-only code changes server only');
+ put('server/web/assets.js', 'module.exports=4');
+ assert.notStrictEqual(v().shell, d.shell, 'the page-stamping server code is shell');
+ // The real tree: every stylesheet, and the files named exist.
+ const real = componentsFor(path.join(__dirname, '..'));
+ assert.ok(real.styles.assets.includes('/css/style.css') && real.styles.assets.length === stylesheets(path.join(__dirname, '..')).length && real.styles.assets.length >= 20, `${real.styles.assets.length} stylesheets`);
+ assert.ok(real.shell.files.includes('public/js') && real.shell.files.includes('server/web'));
+} finally { fs.rmSync(root, { recursive: true, force: true }); }
+console.log('release components: all checks passed');