openvibers/openvibe.live Commits

Deploy notice: with Live's Events outbox on, OpenVibe.Chat learns a deploy from live.release.deployed alone and the chat-remote deployNotice bridge hop is skipped (compatibility register C-84: the 22:14 UTC deploy produced one card, the event folding onto the bridge's by head commit). The bridge stays only as the fallback while Events publishing is off

OP OpenVibers committed 23 Sept 2026 · 5324f40332 · 2 files, +23 −10


Changes

ddiff --git a/server/chat/deploy-notice.js b/server/chat/deploy-notice.js
iindex 62f7bb77..4e220659 100644
---- a/server/chat/deploy-notice.js
++++ b/server/chat/deploy-notice.js
@@@ -17,9 +17,10 @@
// appending, so reconnects and repeats can never duplicate it.
// • The deploy is also a durable OpenVibe.Events event, live.release.deployed
// (server/events/release-events.js), queued in the SAME transaction that records the commits
--// as announced (and, without the Chat service, stores the chat row). The chat notice itself
--// still goes to OpenVibe.Chat over the bridge (chat-remote deployNotice) until Chat consumes
--// the event; then that hop can go.
++// as announced (and, without the Chat service, stores the chat row). OpenVibe.Chat consumes
++// that event and folds the card by head commit (compatibility register C-84, proven on the
++// 2026-09-23 22:14 UTC deploy), so with Live's outbox on the bridge hop is not used; it stays
++// only as the fallback while Events publishing is off.
// ═══════════════════════════════════════════════════════════════
const { execFile } = require('child_process');
const path = require('path');
@@@ -93,7 +94,7 @@ async function announce({ db, chatServer, log = console }) {
// Live's outbox exists before the transaction (idempotent; null while Events is off).
let outbox = null;
-- try { outbox = require('../events/stream-events'); outbox.init(); } catch (err) { log.warn('[Deploy notice] Events outbox unavailable:', err.message); outbox = null; }
++ try { outbox = require('../events/stream-events'); if (!outbox.init()) outbox = null; } catch (err) { log.warn('[Deploy notice] Events outbox unavailable:', err.message); outbox = null; }
let eventId = null;
// Recording the commits as announced and queueing live.release.deployed are one commit: the
// event exists if and only if this deploy counts as announced.
@@@ -107,9 +108,10 @@ async function announce({ db, chatServer, log = console }) {
const inTransaction = (fn) => db.getDb().transaction(fn)();
// CHAT_AUTHORITY=chat: Live still decides what shipped; OpenVibe.Chat stores the rolling
-- // message and shows it (its own copy of this module), so hand it the commits.
++ // message and shows it (its own copy of this module). It learns the commits from the
++ // live.release.deployed event; only with Events publishing off are they handed over the bridge.
if (chatServer && chatServer.remote) {
-- chatServer.deployNotice(commits);
++ if (!outbox) chatServer.deployNotice(commits);
try { inTransaction(recordDeploy); } catch (err) { log.warn('[Deploy notice] not recorded:', err.message); return { announced: commits.length, event_id: null }; }
if (outbox) outbox.kick();
return { announced: commits.length, event_id: eventId };
ddiff --git a/test/release-events.test.js b/test/release-events.test.js
iindex 7f556847..7fca92ea 100644
---- a/test/release-events.test.js
++++ b/test/release-events.test.js
@@@ -102,19 +102,30 @@ const stub = http.createServer((req, res) => {
assert.strictEqual(deployRows().map(x => x.metadata).join('|'), chatBefore, 'the chat row rolled back too');
assert.strictEqual(outboxRows().length, 1);
-- // CHAT_AUTHORITY=chat: the chat notice still goes over the bridge, and the event is queued.
++ // CHAT_AUTHORITY=chat with Events on: Chat learns the deploy from the event alone (C-84); no bridge hop.
const bridged = [];
r = await dn.announce({ db, chatServer: { remote: true, deployNotice: (commits) => bridged.push(commits) }, log: quiet });
assert.ok(r.announced >= 1);
-- assert.strictEqual(bridged.length, 1);
-- assert.strictEqual(bridged[0][0].hash, head);
++ assert.strictEqual(bridged.length, 0, 'the event carries the deploy to Chat');
assert.strictEqual(db.getSetting(dn.SETTING), head);
rows = outboxRows();
assert.strictEqual(rows.length, 2);
assert.strictEqual(rows[1].event_id, r.event_id);
++ // Events publishing off: the bridge is the fallback, so Chat still hears about it.
++ streamEvents._reset();
++ process.env.EVENTS_PUBLISH = 'off';
++ db.setSetting(dn.SETTING, '');
++ r = await dn.announce({ db, chatServer: { remote: true, deployNotice: (commits) => bridged.push(commits) }, log: quiet });
++ delete process.env.EVENTS_PUBLISH;
++ assert.ok(r.announced >= 1);
++ assert.strictEqual(bridged.length, 1, 'without Events the bridge carries it');
++ assert.strictEqual(bridged[0][0].hash, head);
++ assert.strictEqual(r.event_id, null);
++ assert.strictEqual(outboxRows().length, 2, 'nothing queued while Events is off');
++
streamEvents._reset();
stub.close();
-- console.log('release events: live.release.deployed queued with the announcement, published, chat notice kept — all checks passed');
++ console.log('release events: live.release.deployed queued with the announcement, published; Chat gets it from the event, the bridge only without Events — all checks passed');
process.exit(0);
})().catch((err) => { console.error(err); process.exit(1); });