openvibers/openvibe.shared Commits
openvibe-shared 2.21.2: release-watch resumes from the SSE id (Events' cursor), not a computed seq (#53)
Changes
@@@ -4,6 +4,12 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking 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@@@ -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"@@@ -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": {@@@ -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; } }@@@ -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 = nuassert.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 = nuassert.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 });
@@ -4,6 +4,12 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -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.
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).
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
## 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
- **The last fallback "V"s go.** A scan of every site's home page found ring icons drawing the "V" for names with no
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.21.1",
+ "version": "2.21.2",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.21.1",
+ "version": "2.21.2",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -202,7 +202,7 @@
@@ -202,7 +202,7 @@
}
}
function open() {
function open() {
let u;
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';
rt.state = 'connecting';
let s;
let s;
try { s = es = new root.EventSource(u.href); } catch { es = null; fail(); return; }
try { s = es = new root.EventSource(u.href); } catch { es = null; fail(); return; }
@@ -230,6 +230,7 @@
@@ -230,6 +230,7 @@
const ev = m && m.event; const p = ev && ev.payload;
const ev = m && m.event; const p = ev && ev.payload;
if (!p || ev.event_type !== TOPIC) return;
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; }
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;
if (p.service !== rt.service || typeof p.release !== 'string') return;
// Ids repeat across origins (Sites' placeholders): a named origin must be ours.
// 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; } }
if (p.origin) { try { if (new URL(p.origin).origin !== root.location.origin) { rt.ignored++; return; } } catch { return; } }
@@ -1,4 +1,5 @@
@@ -1,4 +1,5 @@
'use strict';
'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
// 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
// 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
// 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() {
@@ -21,7 +22,8 @@ function fakeEventSource() {
addEventListener(type, f) { (this.listeners[type] = this.listeners[type] || []).push(f); }
addEventListener(type, f) { (this.listeners[type] = this.listeners[type] || []).push(f); }
close() { this.closed = true; }
close() { this.closed = true; }
opened() { if (this.onopen) this.onopen({}); }
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) })); }
emit(type, data) { (this.listeners[type] || []).forEach((f) => f({ data: JSON.stringify(data) })); }
fail() { if (this.onerror) this.onerror({}); }
fail() { if (this.onerror) this.onerror({}); }
}
}
@@ -157,7 +159,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -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');
assert.deepStrictEqual(p.timeouts(), [15000], 'the first retry after 15-30 s');
p.fireTimeouts();
p.fireTimeouts();
assert.strictEqual(p.es.all.length, 2);
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 = [];
const waits = [];
for (let i = 1; i < 6; i++) {
for (let i = 1; i < 6; i++) {
p.es.all[i].fail();
p.es.all[i].fail();
@@ -212,7 +214,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -212,7 +214,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
assert.deepStrictEqual([s.closed, p.rt().state], [true, 'hidden']);
assert.deepStrictEqual([s.closed, p.rt().state], [true, 'hidden']);
p.setHidden(false);
p.setHidden(false);
assert.strictEqual(p.es.all.length, 2);
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();
p.release.stop();
const bg = await open({ hidden: true });
const bg = await open({ hidden: true });