openvibers/openvibe.shared Commits

4.138.0: 38 KB off most first visits; every remaining sibling file by content hash (#326)

OP OpenVibers committed 6 days ago · 088deda4a0 · 13 files, +88 −17

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a866456..60e6a68 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,19 @@
# Changelog
++## 4.138.0 — 2026-11-07
++
++- **38 KB off most first visits** (the owner, 11-07: "make things faster and more optimized"):
++ - the footer's "shipped" line (`shipped.js`, 12 KB) is fetched only when the footer comes near the screen;
++ - brand logos (`brands.js`, 26 KB, the footer's social links) wait until a logo is near the screen, or opens
++ from a fold;
++ - most visits never scroll that far.
++- **The rest of the files loaded from beside Shared's scripts go by content hash** (4.137.0 did the bar's):
++ - `shipped.js` from the footer and from `frame.js`'s updates pages;
++ - `brands.js` from the glyphs, through their build;
++ - `route-transition.js` from Boost.
++ - `scripts/sibling-revs.js` writes FOOTER_REV and BOOST_REV; `scripts/build-glyphs.js` writes BRANDS_REV.
++ - Run the glyph build first, then sibling-revs.
++
## 4.137.0 — 2026-11-07
- **Releases reach phones at once.** The bar loaded the menu (`search.js`), the Dock, presence, panels, notifications,
ddiff --git a/README.md b/README.md
iindex d02e44c..2b398aa 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.137.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.138.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 6f17603..1c086f9 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,13 +4,14 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.137.0",
++ "version": "4.138.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"features": [
++ "the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
"every script the bar, the Dock and the notifications load goes by its content hash (?v=), so a release reaches every open page at once instead of up to a day later; one menu button on a phone (4.137.0)",
"the Dock's Agent panel reads the timeline again (the token is resolved when a call is made), and the Dock can open at a step: OpenVibeNavbar.openDock('agent', { task }) marks it and brings it into view (4.134.0)",
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
ddiff --git a/boost.js b/boost.js
iindex e72bb0f..8095b97 100644
---- a/boost.js
++++ b/boost.js
@@@ -112,13 +112,15 @@
});
}
++ // route-transition.js's content hash (scripts/sibling-revs.js writes it), so a release gives it a new URL (4.138.0).
++ const BOOST_REV = { 'route-transition.js': '776674802ce8' };
function fx(mainEl) {
if (o.fx === false) return Promise.resolve(null);
if (root.OVRouteFx) return Promise.resolve(root.OVRouteFx);
if (!selfSrc) return Promise.resolve(null);
return new Promise((done) => {
const t = document.createElement('script');
-- t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js');
++ t.src = selfSrc.replace(/boost\.js(\?.*)?$/, 'route-transition.js') + (BOOST_REV['route-transition.js'] ? `?v=${BOOST_REV['route-transition.js']}` : '');
t.onload = () => done(root.OVRouteFx || null); t.onerror = () => done(null);
document.head.appendChild(t);
});
ddiff --git a/footer.js b/footer.js
iindex 2418c67..49be2dd 100644
---- a/footer.js
++++ b/footer.js
@@@ -526,7 +526,7 @@
}
try { if (typeof window !== 'undefined' && typeof window.ovMarkMount === 'function') window.ovMarkMount(_mounted); } catch { /* */ }
fillAccount({ ..._cfg, variant });
-- if (_cfg.shipped !== false) loadShipped();
++ if (_cfg.shipped !== false) shippedWhenNear(_mounted);
watchPulse();
}
@@@ -585,6 +585,16 @@
// shipped.js mounts every [data-ov-shipped] element when it loads; loaded once, lazily, after the footer.
let _shippedLoading = false;
++ // shipped.js's content hash (scripts/sibling-revs.js writes it), and the footer's "shipped" line is fetched only when the
++ // footer comes near the screen (4.138.0): most visits never scroll there, and it is 12 KB.
++ const FOOTER_REV = { 'shipped.js': 'ee1a121fe4f7' };
++ let _shippedSeen = null;
++ function shippedWhenNear(el) {
++ if (typeof IntersectionObserver !== 'function' || !el || !el.getBoundingClientRect) { loadShipped(); return; }
++ if (_shippedSeen) return;
++ _shippedSeen = new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { _shippedSeen.disconnect(); _shippedSeen = null; loadShipped(); } }, { rootMargin: '900px 0px' });
++ _shippedSeen.observe(el);
++ }
function loadShipped() {
if (typeof document === 'undefined') return;
if (root.OpenVibeShipped && typeof root.OpenVibeShipped.scan === 'function') { try { root.OpenVibeShipped.scan(document); } catch { /* */ } return; }
@@@ -593,7 +603,7 @@
const sc = document.createElement('script');
let src = `${NETWORK_URL}/shared/shipped.js`;
try { if (OWN_SRC) src = new URL('shipped.js', OWN_SRC).href; } catch { /* */ }
-- sc.src = src;
++ sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');
sc.defer = true;
sc.setAttribute('data-ov-shipped-src', '');
(document.head || document.documentElement).appendChild(sc);
ddiff --git a/frame.js b/frame.js
iindex a123f16..c32558b 100644
---- a/frame.js
++++ b/frame.js
@@@ -46,8 +46,17 @@ function updatesBody({ service, siteName = null, base = NETWORK } = {}) {
+ `</section>`;
}
++// shipped.js by its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.
++let _shippedRev = null;
++function shippedRev() {
++ if (_shippedRev === null) {
++ try { _shippedRev = require('./assets').hashAsset(require('fs').readFileSync(require('path').join(__dirname, 'shipped.js'))); } catch { _shippedRev = ''; }
++ }
++ return _shippedRev;
++}
function shippedScript({ base = NETWORK } = {}) {
-- return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js" defer></script>`;
++ const v = shippedRev();
++ return `<script src="${esc(String(base).replace(/\/+$/, ''))}/shared/shipped.js${v ? `?v=${v}` : ''}" defer></script>`;
}
module.exports = { footer: (cfg) => footerModule.ssr(cfg), noscriptNav, shipped, updatesBody, shippedScript };
ddiff --git a/navbar.js b/navbar.js
iindex 9b44acc..8fee5a3 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -513,7 +513,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '715972edd968', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991', 'search.js': '08f0452e6a79', 'dock.js': 'e13066db0aac', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': '3ceb5266e7b8', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
ddiff --git a/ov-glyphs.js b/ov-glyphs.js
iindex fa0ecd6..5c277a3 100644
---- a/ov-glyphs.js
++++ b/ov-glyphs.js
@@@ -53,6 +53,17 @@
const SRC = (document.currentScript && document.currentScript.src) || '';
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
const waiting = new Set();
++ const BRANDS_REV = '0da501c80e60';
++ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits
++ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.
++ let brandSeen = null;
++ function wantBrands(el) {
++ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }
++ const r = el.getBoundingClientRect();
++ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }
++ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });
++ brandSeen.observe(el);
++ }
function loadBrands() {
if (brandState) return;
if (window.OVBrands) { brandState = 2; return; }
@@@ -61,7 +72,7 @@
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const sc = document.createElement('script');
-- sc.src = dir + 'brands.js';
++ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');
sc.async = true;
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
@@@ -73,7 +84,7 @@
if (!fa) return;
const slug = FB[fa];
if (el.dataset.ovg === 'brand:' + slug) return;
-- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }
++ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }
const b = window.OVBrands.get(slug);
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
el.classList.add('ovg', 'ovg-brand');
ddiff --git a/package.json b/package.json
iindex 2bc0c09..d4e0676 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.137.0",
++ "version": "4.138.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/scripts/build-glyphs.js b/scripts/build-glyphs.js
iindex 6cc6018..16a49e1 100644
---- a/scripts/build-glyphs.js
++++ b/scripts/build-glyphs.js
@@@ -92,6 +92,8 @@ const CSS = [
].join('');
const brands = require('../brands');
++// brands.js's content hash, so ov-glyphs.js fetches it by a URL that changes with each release (4.138.0).
++const brandsRev = require('../assets').hashAsset(fs.readFileSync(path.join(__dirname, '..', 'brands.js')));
const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
/**
* openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
@@@ -147,6 +149,17 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
const SRC = (document.currentScript && document.currentScript.src) || '';
let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
const waiting = new Set();
++ const BRANDS_REV = '${brandsRev}';
++ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits
++ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.
++ let brandSeen = null;
++ function wantBrands(el) {
++ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }
++ const r = el.getBoundingClientRect();
++ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }
++ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });
++ brandSeen.observe(el);
++ }
function loadBrands() {
if (brandState) return;
if (window.OVBrands) { brandState = 2; return; }
@@@ -155,7 +168,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
const base = SRC ? SRC.split(/[?#]/)[0] : '';
const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
const sc = document.createElement('script');
-- sc.src = dir + 'brands.js';
++ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');
sc.async = true;
sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
sc.onerror = () => { brandState = 3; waiting.clear(); };
@@@ -167,7 +180,7 @@ const out = `/* Generated by scripts/build-glyphs.js from glyphs.js: edit those,
if (!fa) return;
const slug = FB[fa];
if (el.dataset.ovg === 'brand:' + slug) return;
-- if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); loadBrands(); } return; }
++ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }
const b = window.OVBrands.get(slug);
if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
el.classList.add('ovg', 'ovg-brand');
ddiff --git a/scripts/sibling-revs.js b/scripts/sibling-revs.js
iindex 93cd3a4..c2acedb 100644
---- a/scripts/sibling-revs.js
++++ b/scripts/sibling-revs.js
@@@ -16,15 +16,16 @@ const FILES = ['ov-mark.js', 'ov-glyphs.js', 'ov-brand.woff2', 'ov-look.js', 'ov
'search.js', 'dock.js', 'presence.js', 'panels.js', 'notification-ui.js', 'history.js', 'site-switcher.js', 'appbar.js', 'release-watch.js', 'ov-icons.js', 'sso-client.js'];
// The files dock.js and notification-ui.js load from beside themselves: their own tables, in their own files.
const OWN = { 'dock.js': { name: 'DOCK_REV', files: ['dock-vibe.js', 'action-center.js', 'dock-timeline.js', 'dock-agent.js', 'dock-timer.js'] },
-- 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] } };
++ 'notification-ui.js': { name: 'NOTIFY_REV', files: ['notification-live.js'] },
++ 'footer.js': { name: 'FOOTER_REV', files: ['shipped.js'] }, 'boost.js': { name: 'BOOST_REV', files: ['route-transition.js'] } };
const table = (name, files) => ` const ${name} = { ${files.map((f) => `'${f}': '${hashAsset(fs.readFileSync(path.join(ROOT, f)))}'`).join(', ')} };`;
function writeOwn() {
for (const [file, { name, files }] of Object.entries(OWN)) {
const p = path.join(ROOT, file);
const src = fs.readFileSync(p, 'utf8');
-- const re = new RegExp(`^ {4}const ${name} = \\{[^\\n]*\\};$`, 'm');
++ const re = new RegExp(`^( *)const ${name} = \\{[^\\n]*\\};$`, 'm');
if (!re.test(src)) throw new Error(`${file} has no ${name} line to update`);
-- fs.writeFileSync(p, src.replace(re, table(name, files)));
++ fs.writeFileSync(p, src.replace(re, (m, indent) => indent + table(name, files).trimStart()));
}
}
const revs = () => Object.fromEntries(FILES.map((f) => [f, hashAsset(fs.readFileSync(path.join(ROOT, f)))]));
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex b9fe440..36d3598 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -16,7 +16,8 @@ const body = frame.updatesBody({ service: 'wiki' });
assert.ok(body.includes('<h1>What shipped on OpenVibe.Wiki</h1>'));
assert.ok(body.includes('data-ov-shipped="log" data-service="wiki"'));
assert.ok(body.includes('https://openvibe.network/updates?site=wiki'), 'the no-JavaScript fallback is the network log');
--assert.strictEqual(frame.shippedScript(), '<script src="https://openvibe.network/shared/shipped.js" defer></script>');
++// By its content hash (4.138.0), so an updates page never runs a copy an edge kept from before a release.
++assert.match(frame.shippedScript(), /^<script src="https:\/\/openvibe\.network\/shared\/shipped\.js\?v=[0-9a-f]{12}" defer><\/script>$/);
const foot = frame.footer({ service: 'wiki', variant: 'full', updates: '/updates' });
assert.ok(foot.includes('data-ov-shipped="latest"') && foot.includes('href="/updates"'));
ddiff --git a/test/sibling-revs.test.js b/test/sibling-revs.test.js
iindex cb6a496..e728898 100644
---- a/test/sibling-revs.test.js
++++ b/test/sibling-revs.test.js
@@@ -23,4 +23,13 @@ for (const [file, { name, files }] of Object.entries(OWN)) {
}
const dock = fs.readFileSync(path.join(__dirname, '..', 'dock.js'), 'utf8');
assert.ok(dock.includes("sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : '');"), 'the Dock loads its siblings by hash');
--console.log('sibling revs: every script the navbar, the Dock and the notifications load goes by its content hash');
++// 4.138.0: the footer, Boost and the glyphs load theirs by hash too; the footer's shipped.js and the glyphs' brands.js
++// wait until the footer (or a logo) comes near the screen.
++const footer = fs.readFileSync(path.join(__dirname, '..', 'footer.js'), 'utf8');
++assert.ok(/if \(_cfg\.shipped !== false\) shippedWhenNear\(_mounted\);/.test(footer), 'the footer fetches shipped.js when it nears the screen');
++assert.ok(footer.includes("sc.src = src + (FOOTER_REV['shipped.js'] ? `?v=${FOOTER_REV['shipped.js']}` : '');"), 'and by its hash');
++const glyphsOut = fs.readFileSync(path.join(__dirname, '..', 'ov-glyphs.js'), 'utf8');
++assert.ok(/const BRANDS_REV = '[0-9a-f]{12}';/.test(glyphsOut) && glyphsOut.includes('waiting.add(el); wantBrands(el);'), 'brands.js by hash, when a logo nears the screen');
++const boost = fs.readFileSync(path.join(__dirname, '..', 'boost.js'), 'utf8');
++assert.ok(/BOOST_REV\['route-transition\.js'\]/.test(boost), 'route-transition.js by hash');
++console.log('sibling revs: every script the navbar, the Dock, the notifications, the footer, Boost and the glyphs load goes by its content hash');