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
Changes
@@@ -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 };@@@ -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); });
@@ -17,9 +17,10 @@
@@ -17,9 +17,10 @@
// appending, so reconnects and repeats can never duplicate it.
// appending, so reconnects and repeats can never duplicate it.
// • The deploy is also a durable OpenVibe.Events event, live.release.deployed
// • 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
// (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 { execFile } = require('child_process');
const path = require('path');
const path = require('path');
@@ -93,7 +94,7 @@ async function announce({ db, chatServer, log = console }) {
@@ -93,7 +94,7 @@ async function announce({ db, chatServer, log = console }) {
// Live's outbox exists before the transaction (idempotent; null while Events is off).
// Live's outbox exists before the transaction (idempotent; null while Events is off).
let outbox = null;
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;
let eventId = null;
// Recording the commits as announced and queueing live.release.deployed are one commit: the
// 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.
// event exists if and only if this deploy counts as announced.
@@ -107,9 +108,10 @@ async function announce({ db, chatServer, log = console }) {
@@ -107,9 +108,10 @@ async function announce({ db, chatServer, log = console }) {
const inTransaction = (fn) => db.getDb().transaction(fn)();
const inTransaction = (fn) => db.getDb().transaction(fn)();
// CHAT_AUTHORITY=chat: Live still decides what shipped; OpenVibe.Chat stores the rolling
// 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) {
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 }; }
try { inTransaction(recordDeploy); } catch (err) { log.warn('[Deploy notice] not recorded:', err.message); return { announced: commits.length, event_id: null }; }
if (outbox) outbox.kick();
if (outbox) outbox.kick();
return { announced: commits.length, event_id: eventId };
return { announced: commits.length, event_id: eventId };
@@ -102,19 +102,30 @@ const stub = http.createServer((req, res) => {
@@ -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(deployRows().map(x => x.metadata).join('|'), chatBefore, 'the chat row rolled back too');
assert.strictEqual(outboxRows().length, 1);
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 = [];
const bridged = [];
r = await dn.announce({ db, chatServer: { remote: true, deployNotice: (commits) => bridged.push(commits) }, log: quiet });
r = await dn.announce({ db, chatServer: { remote: true, deployNotice: (commits) => bridged.push(commits) }, log: quiet });
assert.ok(r.announced >= 1);
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);
assert.strictEqual(db.getSetting(dn.SETTING), head);
rows = outboxRows();
rows = outboxRows();
assert.strictEqual(rows.length, 2);
assert.strictEqual(rows.length, 2);
assert.strictEqual(rows[1].event_id, r.event_id);
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();
streamEvents._reset();
stub.close();
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);
process.exit(0);
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });