openvibers/openvibe.shared Commits
release-watch subscribes to topics=host.release.published (1.17.0, WS-P task 9)
Changes
@@@ -6,8 +6,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## 1.17.0 — 2026-09-26--**Release notifications in `release-watch.js`** (roadmap WS-P task 9, ADR-016 amendment 1). When a release goes live, OpenVibe.Host publishes `host.deploy.activated` to OpenVibe.Events (contract `host.deploy.activated@1`, openvibe-contracts 0.58.0; `ovhost deploy` and `ovhost announce`). The event is public, has subject `release` and carries `service`, `release`, `commit` and `origin`. Tabs now hear about a release within seconds instead of at their next poll:--- **One stream per tab.** release-watch opens one EventSource, without credentials, on `https://events.openvibe.network/realtime/stream?topics=host.deploy.activated`, once the page's release and service are known.++**Release notifications in `release-watch.js`** (roadmap WS-P task 9, ADR-016 amendment 1). When a release goes live, OpenVibe.Host publishes `host.release.published` to OpenVibe.Events (contract `host.release.published@1`, openvibe-contracts 0.58.0; `ovhost deploy` and `ovhost announce`). The event is public, has subject `release` and carries `service`, `release`, `commit` and `origin`. Tabs now hear about a release within seconds instead of at their next poll:++- **One stream per tab.** release-watch opens one EventSource, without credentials, on `https://events.openvibe.network/realtime/stream?topics=host.release.published`, once the page's release and service are known.- **Which events count.** Only those for the page's service: `OVReleaseConfig.service`, the meta tag's `data-service`, else `/release.json`'s `service`.- **Generations.** The tab ignores the release it runs, the one it already knows from `/release.json` and one already queued (a hex prefix of the other counts as the same release). It also ignores a repeated event id and a seq at or below the last one seen.- **Coalescing.** A new release runs the usual `check(true)` after a random 0–20 s delay, so tabs do not all fetch at once. Everything arriving while that check is pending collapses into it, and after a check there are 30 s of quiet, after which whatever arrived meanwhile gets one more check. The update plan, the prompt and the safe-reload rules are unchanged.@@@ -193,9 +193,9 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /releA stylesheet whose URL path is not the asset's logical path needs `data-ov-asset="/css/app.css"`.Mark anything that must not be replaced with `data-ov-protected`.- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes-- `host.deploy.activated` (public, `ovhost deploy` / `ovhost announce`). release-watch opens one++ `host.release.published` (public, `ovhost deploy` / `ovhost announce`). release-watch opens oneEventSource per tab, without credentials, on-- `https://events.openvibe.network/realtime/stream?topics=host.deploy.activated`.++ `https://events.openvibe.network/realtime/stream?topics=host.release.published`.- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runsor already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual@@@ -7,7 +7,7 @@* 2 minutes, no focused field, nothing protected (form[data-dirty="true"], [data-ov-protected], playing* media, a live camera/mic, window.OVProtected()). Outcomes are beaconed to the metrics URL (D46).* Release notifications (1.17.0, WS-P task 9): one anonymous EventSource on the Events realtime stream-- * (topic host.deploy.activated, public). An event naming this page's service with a release it neither++ * (topic host.release.published, public). An event naming this page's service with a release it neither* runs nor knows runs check(true) after a 0-20 s jitter, at most once per 30 s. Polling stays the fallback.* OVReleaseConfig: { url, metricsUrl, updateUrl, inPlace: false, service, eventsUrl (false: off) }; the meta* tag may carry data-service and data-events. See README "Releases".@@@ -167,12 +167,12 @@return m;}-- // ── Release notifications: OpenVibe.Host publishes host.deploy.activated (public) to OpenVibe.Events ──++ // ── Release notifications: OpenVibe.Host publishes host.release.published (public) to OpenVibe.Events ──// One EventSource per tab, without credentials: the events are public, so an account switch changes// nothing (and a second load of this script returns early). Closed after 5 min hidden (the poll and the// visibility check cover a hidden tab), reopened on return with last_event_id; errors back off 30 s to// 15 min, and after 6 failures in a row only polling is left.-- const TOPIC = 'host.deploy.activated';++ 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 };let es = null; let queued = null; let again = false; let seen = [];@@@ -1,6 +1,6 @@'use strict';// release-watch.js release notifications (1.17.0, WS-P task 9): one anonymous EventSource on the Events--// realtime stream (topic host.deploy.activated); 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// runs or already knows is ignored, errors back off, hidden tabs close the stream, stop() closes it, and// polling is unchanged. A fake EventSource stands in for the browser's.@@@ -30,7 +30,7 @@ function fakeEventSource() {let n = 0;const released = (service, release, extra = {}) => ({-- event_id: `evt_01JAB2C3D4E5F6G7H8J9K0M${String(++n).padStart(3, '0')}`, event_type: 'host.deploy.activated', version: 1, source: 'host',++ event_id: `evt_01JAB2C3D4E5F6G7H8J9K0M${String(++n).padStart(3, '0')}`, event_type: 'host.release.published', version: 1, source: 'host',visibility: 'public', subject: { type: 'release', id: `${service}:${release}` },payload: { service, release, commit: null, origin: 'https://openvibe.live', deployed_at: '2026-09-26T08:00:00.000Z' }, ...extra,});@@@ -57,7 +57,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nuconst p = await open();assert.strictEqual(p.es.all.length, 1);const s = p.es.all[0];-- assert.strictEqual(s.url, `${DEFAULT}?topics=host.deploy.activated`);++ assert.strictEqual(s.url, `${DEFAULT}?topics=host.release.published`);assert.strictEqual(s.init, undefined, 'no withCredentials: public events need no account');assert.strictEqual(p.rt().state, 'connecting');s.opened();@@@ -67,7 +67,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu// Other services, tenant activations, the running release (or a hex prefix of it), other topics.s.send(1, released('tools', B));-- s.send(2, { ...released('live', B), payload: { project_id: 'prj_x', site_id: 'site_x', site: 'x', deploy_id: 'dpl_x', previous_deploy_id: null, rollback: false } });++ s.send(2, { ...released('live', B), event_type: 'host.deploy.activated', subject: { type: 'deploy', id: 'dpl_x' }, payload: { project_id: 'prj_x', site_id: 'site_x', site: 'x', deploy_id: 'dpl_x', previous_deploy_id: null, rollback: false } });s.send(3, released('live', A));s.send(4, released('live', 'aaaaaaa'));s.send(5, { ...released('live', B), event_type: 'live.stream.started' });@@@ -155,7 +155,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.deploy.activated&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=41`, 'resumes after the last seq seen');const waits = [];for (let i = 1; i < 6; i++) {p.es.all[i].fail();@@@ -227,7 +227,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nuoff.release.stop();const metaUrl = await open({ html: PAGE(' data-events="https://events.example.test/realtime/stream" data-service="docs"') });-- assert.strictEqual(metaUrl.es.all[0].url, 'https://events.example.test/realtime/stream?topics=host.deploy.activated');++ assert.strictEqual(metaUrl.es.all[0].url, 'https://events.example.test/realtime/stream?topics=host.release.published');assert.strictEqual(metaUrl.rt().service, 'docs', 'data-service names the service');metaUrl.release.stop();@@@ -236,7 +236,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = numetaOff.release.stop();const cfgService = await open({ config: { service: 'tools', eventsUrl: '/realtime/stream' } });-- assert.strictEqual(cfgService.es.all[0].url, 'https://openvibe.live/realtime/stream?topics=host.deploy.activated');++ assert.strictEqual(cfgService.es.all[0].url, 'https://openvibe.live/realtime/stream?topics=host.release.published');cfgService.es.all[0].send(1, released('tools', B));assert.deepStrictEqual(cfgService.timeouts(), [10000], 'OVReleaseConfig.service decides which events count');cfgService.release.stop();
@@ -6,8 +6,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,8 +6,8 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## 1.17.0 — 2026-09-26
## 1.17.0 — 2026-09-26
-**Release notifications in `release-watch.js`** (roadmap WS-P task 9, ADR-016 amendment 1). When a release goes live, OpenVibe.Host publishes `host.deploy.activated` to OpenVibe.Events (contract `host.deploy.activated@1`, openvibe-contracts 0.58.0; `ovhost deploy` and `ovhost announce`). The event is public, has subject `release` and carries `service`, `release`, `commit` and `origin`. Tabs now hear about a release within seconds instead of at their next poll:
-- **One stream per tab.** release-watch opens one EventSource, without credentials, on `https://events.openvibe.network/realtime/stream?topics=host.deploy.activated`, once the page's release and service are known.
+**Release notifications in `release-watch.js`** (roadmap WS-P task 9, ADR-016 amendment 1). When a release goes live, OpenVibe.Host publishes `host.release.published` to OpenVibe.Events (contract `host.release.published@1`, openvibe-contracts 0.58.0; `ovhost deploy` and `ovhost announce`). The event is public, has subject `release` and carries `service`, `release`, `commit` and `origin`. Tabs now hear about a release within seconds instead of at their next poll:
+- **One stream per tab.** release-watch opens one EventSource, without credentials, on `https://events.openvibe.network/realtime/stream?topics=host.release.published`, once the page's release and service are known.
- **Which events count.** Only those for the page's service: `OVReleaseConfig.service`, the meta tag's `data-service`, else `/release.json`'s `service`.
- **Which events count.** Only those for the page's service: `OVReleaseConfig.service`, the meta tag's `data-service`, else `/release.json`'s `service`.
- **Generations.** The tab ignores the release it runs, the one it already knows from `/release.json` and one already queued (a hex prefix of the other counts as the same release). It also ignores a repeated event id and a seq at or below the last one seen.
- **Generations.** The tab ignores the release it runs, the one it already knows from `/release.json` and one already queued (a hex prefix of the other counts as the same release). It also ignores a repeated event id and a seq at or below the last one seen.
- **Coalescing.** A new release runs the usual `check(true)` after a random 0–20 s delay, so tabs do not all fetch at once. Everything arriving while that check is pending collapses into it, and after a check there are 30 s of quiet, after which whatever arrived meanwhile gets one more check. The update plan, the prompt and the safe-reload rules are unchanged.
- **Coalescing.** A new release runs the usual `check(true)` after a random 0–20 s delay, so tabs do not all fetch at once. Everything arriving while that check is pending collapses into it, and after a check there are 30 s of quiet, after which whatever arrived meanwhile gets one more check. The update plan, the prompt and the safe-reload rules are unchanged.
@@ -193,9 +193,9 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
@@ -193,9 +193,9 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
A stylesheet whose URL path is not the asset's logical path needs `data-ov-asset="/css/app.css"`.
A stylesheet whose URL path is not the asset's logical path needs `data-ov-asset="/css/app.css"`.
Mark anything that must not be replaced with `data-ov-protected`.
Mark anything that must not be replaced with `data-ov-protected`.
- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes
- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes
- `host.deploy.activated` (public, `ovhost deploy` / `ovhost announce`). release-watch opens one
+ `host.release.published` (public, `ovhost deploy` / `ovhost announce`). release-watch opens one
EventSource per tab, without credentials, on
EventSource per tab, without credentials, on
- `https://events.openvibe.network/realtime/stream?topics=host.deploy.activated`.
+ `https://events.openvibe.network/realtime/stream?topics=host.release.published`.
- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,
- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,
the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runs
the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runs
or already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual
or already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
* 2 minutes, no focused field, nothing protected (form[data-dirty="true"], [data-ov-protected], playing
* 2 minutes, no focused field, nothing protected (form[data-dirty="true"], [data-ov-protected], playing
* media, a live camera/mic, window.OVProtected()). Outcomes are beaconed to the metrics URL (D46).
* media, a live camera/mic, window.OVProtected()). Outcomes are beaconed to the metrics URL (D46).
* Release notifications (1.17.0, WS-P task 9): one anonymous EventSource on the Events realtime stream
* Release notifications (1.17.0, WS-P task 9): one anonymous EventSource on the Events realtime stream
- * (topic host.deploy.activated, public). An event naming this page's service with a release it neither
+ * (topic host.release.published, public). An event naming this page's service with a release it neither
* runs nor knows runs check(true) after a 0-20 s jitter, at most once per 30 s. Polling stays the fallback.
* runs nor knows runs check(true) after a 0-20 s jitter, at most once per 30 s. Polling stays the fallback.
* OVReleaseConfig: { url, metricsUrl, updateUrl, inPlace: false, service, eventsUrl (false: off) }; the meta
* OVReleaseConfig: { url, metricsUrl, updateUrl, inPlace: false, service, eventsUrl (false: off) }; the meta
* tag may carry data-service and data-events. See README "Releases".
* tag may carry data-service and data-events. See README "Releases".
@@ -167,12 +167,12 @@
@@ -167,12 +167,12 @@
return m;
return m;
}
}
- // ── Release notifications: OpenVibe.Host publishes host.deploy.activated (public) to OpenVibe.Events ──
+ // ── Release notifications: OpenVibe.Host publishes host.release.published (public) to OpenVibe.Events ──
// One EventSource per tab, without credentials: the events are public, so an account switch changes
// One EventSource per tab, without credentials: the events are public, so an account switch changes
// nothing (and a second load of this script returns early). Closed after 5 min hidden (the poll and the
// nothing (and a second load of this script returns early). Closed after 5 min hidden (the poll and the
// visibility check cover a hidden tab), reopened on return with last_event_id; errors back off 30 s to
// visibility check cover a hidden tab), reopened on return with last_event_id; errors back off 30 s to
// 15 min, and after 6 failures in a row only polling is left.
// 15 min, and after 6 failures in a row only polling is left.
- const TOPIC = 'host.deploy.activated';
+ const TOPIC = 'host.release.published';
const HEX = /^[0-9a-f]{7,40}$/;
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, lastSeq: null };
let es = null; let queued = null; let again = false; let seen = [];
let es = null; let queued = null; let again = false; let seen = [];
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
'use strict';
'use strict';
// 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.deploy.activated); 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
// runs or already knows is ignored, errors back off, hidden tabs close the stream, stop() closes it, and
// runs or already knows is ignored, errors back off, hidden tabs close the stream, stop() closes it, and
// polling is unchanged. A fake EventSource stands in for the browser's.
// polling is unchanged. A fake EventSource stands in for the browser's.
@@ -30,7 +30,7 @@ function fakeEventSource() {
@@ -30,7 +30,7 @@ function fakeEventSource() {
let n = 0;
let n = 0;
const released = (service, release, extra = {}) => ({
const released = (service, release, extra = {}) => ({
- event_id: `evt_01JAB2C3D4E5F6G7H8J9K0M${String(++n).padStart(3, '0')}`, event_type: 'host.deploy.activated', version: 1, source: 'host',
+ event_id: `evt_01JAB2C3D4E5F6G7H8J9K0M${String(++n).padStart(3, '0')}`, event_type: 'host.release.published', version: 1, source: 'host',
visibility: 'public', subject: { type: 'release', id: `${service}:${release}` },
visibility: 'public', subject: { type: 'release', id: `${service}:${release}` },
payload: { service, release, commit: null, origin: 'https://openvibe.live', deployed_at: '2026-09-26T08:00:00.000Z' }, ...extra,
payload: { service, release, commit: null, origin: 'https://openvibe.live', deployed_at: '2026-09-26T08:00:00.000Z' }, ...extra,
});
});
@@ -57,7 +57,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -57,7 +57,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
const p = await open();
const p = await open();
assert.strictEqual(p.es.all.length, 1);
assert.strictEqual(p.es.all.length, 1);
const s = p.es.all[0];
const s = p.es.all[0];
- assert.strictEqual(s.url, `${DEFAULT}?topics=host.deploy.activated`);
+ assert.strictEqual(s.url, `${DEFAULT}?topics=host.release.published`);
assert.strictEqual(s.init, undefined, 'no withCredentials: public events need no account');
assert.strictEqual(s.init, undefined, 'no withCredentials: public events need no account');
assert.strictEqual(p.rt().state, 'connecting');
assert.strictEqual(p.rt().state, 'connecting');
s.opened();
s.opened();
@@ -67,7 +67,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -67,7 +67,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
// Other services, tenant activations, the running release (or a hex prefix of it), other topics.
// Other services, tenant activations, the running release (or a hex prefix of it), other topics.
s.send(1, released('tools', B));
s.send(1, released('tools', B));
- s.send(2, { ...released('live', B), payload: { project_id: 'prj_x', site_id: 'site_x', site: 'x', deploy_id: 'dpl_x', previous_deploy_id: null, rollback: false } });
+ s.send(2, { ...released('live', B), event_type: 'host.deploy.activated', subject: { type: 'deploy', id: 'dpl_x' }, payload: { project_id: 'prj_x', site_id: 'site_x', site: 'x', deploy_id: 'dpl_x', previous_deploy_id: null, rollback: false } });
s.send(3, released('live', A));
s.send(3, released('live', A));
s.send(4, released('live', 'aaaaaaa'));
s.send(4, released('live', 'aaaaaaa'));
s.send(5, { ...released('live', B), event_type: 'live.stream.started' });
s.send(5, { ...released('live', B), event_type: 'live.stream.started' });
@@ -155,7 +155,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -155,7 +155,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.deploy.activated&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=41`, 'resumes after the last seq seen');
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();
@@ -227,7 +227,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -227,7 +227,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
off.release.stop();
off.release.stop();
const metaUrl = await open({ html: PAGE(' data-events="https://events.example.test/realtime/stream" data-service="docs"') });
const metaUrl = await open({ html: PAGE(' data-events="https://events.example.test/realtime/stream" data-service="docs"') });
- assert.strictEqual(metaUrl.es.all[0].url, 'https://events.example.test/realtime/stream?topics=host.deploy.activated');
+ assert.strictEqual(metaUrl.es.all[0].url, 'https://events.example.test/realtime/stream?topics=host.release.published');
assert.strictEqual(metaUrl.rt().service, 'docs', 'data-service names the service');
assert.strictEqual(metaUrl.rt().service, 'docs', 'data-service names the service');
metaUrl.release.stop();
metaUrl.release.stop();
@@ -236,7 +236,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -236,7 +236,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
metaOff.release.stop();
metaOff.release.stop();
const cfgService = await open({ config: { service: 'tools', eventsUrl: '/realtime/stream' } });
const cfgService = await open({ config: { service: 'tools', eventsUrl: '/realtime/stream' } });
- assert.strictEqual(cfgService.es.all[0].url, 'https://openvibe.live/realtime/stream?topics=host.deploy.activated');
+ assert.strictEqual(cfgService.es.all[0].url, 'https://openvibe.live/realtime/stream?topics=host.release.published');
cfgService.es.all[0].send(1, released('tools', B));
cfgService.es.all[0].send(1, released('tools', B));
assert.deepStrictEqual(cfgService.timeouts(), [10000], 'OVReleaseConfig.service decides which events count');
assert.deepStrictEqual(cfgService.timeouts(), [10000], 'OVReleaseConfig.service decides which events count');
cfgService.release.stop();
cfgService.release.stop();