openvibers/openvibe.shared Commits

openvibe-shared 2.21.2: release-watch resumes from the SSE id (Events' cursor), not a computed seq (#53)

OP OpenVibers committed 10 Oct 2026 · c63c6d3556 · 5 files, +16 −7

Plan T7 (Events retires numeric positions once every consumer reads by opaque cursor). `release-watch.js` reopened its release-notification stream with `last_event_id` set to the numeric seq it read from the message data: a bare-seq resume, which Events only accepts "for one release". It now resumes from the last SSE id Events sent (the opaque cursor), and still drops a replayed message by the seq in the data.

Size: release-watch.js is 5120 bytes brotli, exactly its 5 KB budget (main was 5102), so the comments were kept to one line. The test's fake EventSource now sends cursor ids the way Events does, and the resume checks expect them back in `last_event_id`.

2.21.2. `ov test`: 15 files reached, green.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex b97156e..521fd49 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,12 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 2.21.2 — 2026-10-10
++
++- `release-watch.js` reopens its release-notification stream with `last_event_id` set to the last SSE id Events sent
++ (its opaque cursor, ADR-042), not the numeric seq it computed from the message. It still drops a replayed message
++ by the seq the data carries. This is one of the last consumers of Events' numeric positions (plan T7 retires them).
++
## 2.21.1 — 2026-10-10
- **The last fallback "V"s go.** A scan of every site's home page found ring icons drawing the "V" for names with no
ddiff --git a/package-lock.json b/package-lock.json
iindex 62bbd33..d624257 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "2.21.1",
++ "version": "2.21.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "2.21.1",
++ "version": "2.21.2",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 91ac1b1..7455773 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "2.21.1",
++ "version": "2.21.2",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/release-watch.js b/release-watch.js
iindex 2f3b5ae..39c01cf 100644
---- a/release-watch.js
++++ b/release-watch.js
@@@ -202,7 +202,7 @@
}
function open() {
let u;
-- try { u = new URL(rt.url, root.location.href); u.searchParams.set('topics', TOPIC); if (rt.lastSeq != null) u.searchParams.set('last_event_id', rt.lastSeq); } catch { rt.state = 'failed'; return; }
++ try { u = new URL(rt.url, root.location.href); u.searchParams.set('topics', TOPIC); if (rt.lastId) u.searchParams.set('last_event_id', rt.lastId); } catch { rt.state = 'failed'; return; }
rt.state = 'connecting';
let s;
try { s = es = new root.EventSource(u.href); } catch { es = null; fail(); return; }
@@@ -230,6 +230,7 @@
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.
if (p.origin) { try { if (new URL(p.origin).origin !== root.location.origin) { rt.ignored++; return; } } catch { return; } }
ddiff --git a/test/release-watch-realtime.test.js b/test/release-watch-realtime.test.js
iindex 6f1ad35..972c0ba 100644
---- a/test/release-watch-realtime.test.js
++++ b/test/release-watch-realtime.test.js
@@@ -1,4 +1,5 @@
'use strict';
++const cur = (seq) => `c1.0.${Buffer.from(String(seq), 'utf8').toString('base64url')}`;
// release-watch.js release notifications (1.17.0, WS-P task 9): one anonymous EventSource on the Events
// realtime stream (topic host.release.published); an event for this page's service with a new release runs
// the ordinary check after a 0-20 s jitter, bursts collapse into one check per 30 s, the release the tab
@@@ -21,7 +22,8 @@ function fakeEventSource() {
addEventListener(type, f) { (this.listeners[type] = this.listeners[type] || []).push(f); }
close() { this.closed = true; }
opened() { if (this.onopen) this.onopen({}); }
-- send(seq, event) { if (this.onmessage) this.onmessage({ data: JSON.stringify({ seq, event }), lastEventId: String(seq) }); }
++ // Events' SSE id is the event's opaque cursor (ADR-042), here made as Events makes it.
++ send(seq, event) { if (this.onmessage) this.onmessage({ data: JSON.stringify({ seq, event }), lastEventId: cur(seq) }); }
emit(type, data) { (this.listeners[type] || []).forEach((f) => f({ data: JSON.stringify(data) })); }
fail() { if (this.onerror) this.onerror({}); }
}
@@@ -157,7 +159,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
assert.deepStrictEqual(p.timeouts(), [15000], 'the first retry after 15-30 s');
p.fireTimeouts();
assert.strictEqual(p.es.all.length, 2);
-- assert.strictEqual(p.es.all[1].url, `${DEFAULT}?topics=host.release.published&last_event_id=41`, 'resumes after the last seq seen');
++ assert.strictEqual(p.es.all[1].url, `${DEFAULT}?topics=host.release.published&last_event_id=${cur(41)}`, 'resumes from the last SSE id seen (the cursor), not a computed number');
const waits = [];
for (let i = 1; i < 6; i++) {
p.es.all[i].fail();
@@@ -212,7 +214,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
assert.deepStrictEqual([s.closed, p.rt().state], [true, 'hidden']);
p.setHidden(false);
assert.strictEqual(p.es.all.length, 2);
-- assert.match(p.es.all[1].url, /last_event_id=50$/);
++ assert.ok(p.es.all[1].url.endsWith(`last_event_id=${cur(50)}`));
p.release.stop();
const bg = await open({ hidden: true });