openvibers/openvibe.sites Commits

openvibe-shared v3.0.1 (was v3.0.0) (#46)

OP OpenVibers committed 10 Oct 2026 · 422d9e1e94 · 6 files, +24 −17

* openvibe-shared v3.0.1 (was v3.0.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v3.0.1 release.

* Sites: rebuild dist/_shared from openvibe-shared 3.0.1 (the bell and release watcher resume from Events' cursor)

Changes

ddiff --git a/README.md b/README.md
iindex b6c752a..edee779 100644
---- a/README.md
++++ b/README.md
@@@ -72,7 +72,7 @@ The pinned `openvibe-contracts` release supplies the catalog `sites.json` mirror
## Depends on
- `openvibe-contracts` v0.129.0 — `scripts/sync-catalog.js` reads `openvibe-contracts/lib/products` (`catalog()`) to generate `sites.json`.
--- `openvibe-shared` v3.0.0 — `build.js` uses `legal` for the terms, privacy and dmca pages, `files` for the `BROWSER` list copied to `dist/_shared/`, and `app-icon` for the manifest and head tags; `test/run.js` uses `test-runner`.
++- `openvibe-shared` v3.0.1 — `build.js` uses `legal` for the terms, privacy and dmca pages, `files` for the `BROWSER` list copied to `dist/_shared/`, and `app-icon` for the manifest and head tags; `test/run.js` uses `test-runner`.
- OpenVibe.Host — the deploy wrapper calls `ovhost deploy sites --restart`, or `ovhost plan sites --restart` with `DRY_RUN=1`.
- OpenVibe.Network at `https://openvibe.network` — the frozen pages and shared browser files load its assets and API in the browser; `NET.networkUrl` in `build.js` is that base.
@@@ -108,5 +108,5 @@ Report security issues through [SECURITY.md](SECURITY.md).
<!-- versions:start -->
- openvibe-contracts: v0.129.0
--- openvibe-shared: v3.0.0
++- openvibe-shared: v3.0.1
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex e0515ee..2fc6225 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -23,7 +23,7 @@
"status.openvibe.network (status notice)"
],
"contracts": "openvibe-contracts v0.129.0: sites.json mirrors products.catalog() (scripts/sync-catalog.js)",
-- "shared": "openvibe-shared v3.0.0",
++ "shared": "openvibe-shared v3.0.1",
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy sites --restart` (OpenVibe.Host strategy static-build since the 2026-09-27 cutover): /opt/openvibe.sites, nginx serves dist/<domain>/ through deploy/nginx/<domain>.conf; no unit, no port, no env file",
"features": [
ddiff --git a/dist/_shared/notification-live.js b/dist/_shared/notification-live.js
iindex fdb708a..1e95d0e 100644
---- a/dist/_shared/notification-live.js
++++ b/dist/_shared/notification-live.js
@@@ -3,10 +3,11 @@
* ADR-005 amendment 2). notification-ui.js loads it on demand when a site turns it on (navbar notificationsRealtime).
* For every (re)connect it asks Network for a realtime ticket (POST <api>/api/v1/realtime/ticket: Bearer token, or the
* cookie on openvibe.network itself) and opens Events' stream with it, without credentials:
-- * <stream_url>?topics=network.notification.*&ticket=<ticket>[&last_event_id=<seq>]
++ * <stream_url>?topics=network.notification.*&ticket=<ticket>[&last_event_id=<cursor>]
* Events streams only the person's own network.notification.created events (subject visibility); one whose subject is
* not the ticket's is ignored all the same. Each calls onNotification(payload); `event: gap` calls onGap.
-- * A ticket opens one stream: errors close it and reconnect with a fresh ticket from the cursor, backing off 2 s to 15 min
++ * The cursor is the last SSE id Events sent (opaque, handed back as it came); an event already heard (by event_id) is
++ * skipped. A ticket opens one stream: errors close it and reconnect with a fresh ticket from the cursor, backing off 2 s to 15 min
* with jitter. Stops (the caller's polling is all there is) after 10 failures in a row, on a Content-Security-Policy
* refusal, when signed out (401/403) or when Network has realtime off (404/503). Hidden 5 min: closed; shown: resumed.
* OVNotificationLive.create({ ticketUrl, token, credentials, onNotification, onGap, onState }) → { start, stop, restart, state }
@@@ -17,10 +18,12 @@
const TYPE_RE = /^network\.notification\./;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const MAX_FAILURES = 10; // 2 s, 4 s, … 512 s: about 9 to 17 minutes of retries before polling only
++ const SEEN_MAX = 256; // event ids remembered for dedupe
function create(o) {
-- const rt = { state: 'off', failures: 0, lastSeq: null, subject: null, origin: null, events: 0, ignored: 0, gaps: 0, tickets: 0 };
++ const rt = { state: 'off', failures: 0, lastId: null, subject: null, origin: null, events: 0, ignored: 0, gaps: 0, tickets: 0 };
let es = null; let stopped = true; let gen = 0;
++ const seen = new Set();
const t = {}; // timers: retry, hide
const later = (k, f, ms) => { root.clearTimeout(t[k]); t[k] = root.setTimeout(() => { t[k] = null; f(); }, ms); };
const call = (f, ...a) => { try { if (typeof f === 'function') f(...a); } catch { /* the page's handler must not break the feed */ } };
@@@ -54,7 +57,7 @@
if (!/^https?:$/.test(u.protocol)) throw new Error('url');
u.searchParams.set('topics', tk.topics.join(','));
u.searchParams.set('ticket', tk.ticket);
-- if (rt.lastSeq != null) u.searchParams.set('last_event_id', String(rt.lastSeq));
++ if (rt.lastId != null) u.searchParams.set('last_event_id', rt.lastId);
} catch { fail(); return; }
rt.tickets++; rt.origin = u.origin;
rt.subject = tk.subject;
@@@ -73,9 +76,14 @@
}
function heard(e) {
let m; try { m = JSON.parse(e.data); } catch { return; }
-- const seq = m && typeof m.seq === 'number' ? m.seq : Number(e.lastEventId);
-- if (Number.isFinite(seq)) { if (rt.lastSeq != null && seq <= rt.lastSeq) return; rt.lastSeq = seq; }
const ev = m && m.event;
++ const id = ev && typeof ev.event_id === 'string' ? ev.event_id : null;
++ if (id) {
++ if (seen.has(id)) return; // already heard (a replay after a reconnect)
++ seen.add(id);
++ if (seen.size > SEEN_MAX) seen.delete(seen.values().next().value);
++ }
++ if (e.lastEventId) rt.lastId = String(e.lastEventId);
// Events already sends only this person's own; anything else is never shown.
if (!ev || !TYPE_RE.test(String(ev.event_type)) || !ev.subject || ev.subject.type !== 'user' || ev.subject.id !== rt.subject) { rt.ignored++; return; }
rt.events++;
@@@ -107,7 +115,7 @@
start() { if (!stopped) return api; stopped = false; set('off'); open(); return api; },
stop() { stopped = true; gen++; close(); Object.keys(t).forEach((k) => { root.clearTimeout(t[k]); t[k] = null; }); set('off'); return api; },
/** Another account (or a new token): forget the cursor and the subject, start again. */
-- restart() { api.stop(); rt.lastSeq = null; rt.subject = null; rt.failures = 0; return api.start(); },
++ restart() { api.stop(); rt.lastId = null; seen.clear(); rt.subject = null; rt.failures = 0; return api.start(); },
state: () => ({ ...rt }),
};
return api;
ddiff --git a/dist/_shared/release-watch.js b/dist/_shared/release-watch.js
iindex 39c01cf..4f51030 100644
---- a/dist/_shared/release-watch.js
++++ b/dist/_shared/release-watch.js
@@@ -181,7 +181,7 @@
// back off 30 s to 15 min, after 6 failures only polling is left; online reconnects at once.
const TOPIC = 'host.release.published';
const HEX = /^[0-9a-f]{7,40}$/;
-- const rt = { state: 'off', service: null, url: null, events: 0, ignored: 0, checks: 0, failures: 0, lastSeq: null };
++ const rt = { state: 'off', service: null, url: null, events: 0, ignored: 0, checks: 0, failures: 0, lastId: null };
let es = null; let queued = null; let again = false; let seen = [];
const t = {}; // timers: run (the jittered check), cool (30 s after it), retry, hide
const later = (k, f, ms) => { root.clearTimeout(t[k]); t[k] = root.setTimeout(() => { t[k] = null; f(); }, ms); };
@@@ -229,7 +229,6 @@
let m; try { m = JSON.parse(e.data); } catch { return; }
const ev = m && m.event; const p = ev && ev.payload;
if (!p || ev.event_type !== TOPIC) return;
-- if (typeof m.seq === 'number') { if (rt.lastSeq != null && m.seq <= rt.lastSeq) return; rt.lastSeq = m.seq; }
rt.lastId = e.lastEventId || rt.lastId;
if (p.service !== rt.service || typeof p.release !== 'string') return;
// Ids repeat across origins (Sites' placeholders): a named origin must be ours.
ddiff --git a/package-lock.json b/package-lock.json
iindex 15d3901..74e2206 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -7,7 +7,7 @@
"name": "openvibe-sites",
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.1"
},
"engines": {
"node": ">=20"
@@@ -197,9 +197,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "3.0.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
-- "integrity": "sha512-wR/4g5G2tD/s4sysi5ZIGlsKMZHMrXl9SSeohto+Z1rSP9qugZGJNwGdVSUOhgdZ75S7D9eg68jsIy/OoM9LhA==",
++ "version": "3.0.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.1",
++ "integrity": "sha512-7EvVTZFzhvUQw1r2zB1Lpg2lTnyU6rQl7OqkDS2ESsmvRxoKsH/ztquFtVTNSRtwhI5NR2kmW1PADkCwiBJcZA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex a672244..080a1fa 100644
---- a/package.json
++++ b/package.json
@@@ -8,7 +8,7 @@
},
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.1"
},
"engines": {
"node": ">=20"