openvibers/openvibe.live Commits
Chat's 'updating' notice no longer sticks after a Live deploy (#246)
Changes
@@@ -138,8 +138,9 @@ Client WS → OpenVibe.Chat (127.0.0.1:4400; nginx routes /ws/chat, /api/chat/,Live runs no chat WebSocket server or chat REST routes: it delivers to Chat's typed ingress through`server/chat/chat-delivery.js` (producer pushes, moderation, cache hints) and reads Chat's snapshot forpresence. The room is derived from the stream the client joined; offline channel chat applies the--channel's bans and chat rules. Reconnects are jittered; a `server_restart` notice shows "OpenVibe is--updating" and refills history on reconnect. See [chat-system.md](chat-system.md).++channel's bans and chat rules. Reconnects are jittered; Chat's `server_restart` notice makes the next reconnect quick and,++if the socket then drops, shows "OpenVibe is updating" until it is back (history refills on reconnect). Live sends++no such notice: its restarts do not touch chat sockets. See [chat-system.md](chat-system.md).## Server runtime@@@ -2787,6 +2787,7 @@ function initChat(streamId, channelUserId = null) {*/let _chatConnDropAt = 0;let _chatServerUpdating = false;++let _chatUpdatingTimer = 0;function _chatConnState(state, detail) {const { messages: container } = getChatEl();if (container) {@@@ -3428,10 +3429,14 @@ function handleChatMessage(msg) {addSystemMessage('Message blocked by this streamer\'s Anti-Slur Nudge setting.');break;case 'server_restart':-- // A deploy is restarting the server. No reload needed: the socket reconnects on its own-- // (with a short random delay, so everyone does not arrive at once) and history refills.++ // A deploy is restarting the chat server. No reload needed: if the socket drops it reconnects++ // on its own (with a short random delay, so everyone does not arrive at once), and only then++ // does the reader see "updating". A restart that never drops this socket says nothing: the++ // notice used to show "updating" at once and clear it only on a reconnect, so it stuck when++ // the socket stayed up (owner's popout, 2026-10-18)._chatServerUpdating = true;-- _chatConnState('updating');++ clearTimeout(_chatUpdatingTimer);++ _chatUpdatingTimer = setTimeout(() => { _chatServerUpdating = false; }, 60000);break;case 'update': {// Deploy notice: one rolling card per run of deploys, replaced in place by id (never duplicated).@@@ -163,16 +163,6 @@ const stopper = gracefulStop({// gating on /api/ready should see that before the socket actually closes._bootComplete = false;},-- async () => {-- // Notify all chat clients before closing connections-- try {-- await require('./chat/chat-delivery').event({ kind: 'all' }, {-- type: 'server_restart',-- message: '⚙️ Chat server restarting — you will be reconnected automatically.',-- timestamp: new Date().toISOString(),-- });-- } catch { /* non-critical */ }-- },// Kill any in-flight whisper/ffmpeg transcription children so they don't orphan// (and so their temp files get cleaned by the close handlers). The interrupted VOD// is left in 'processing' → re-queued to 'pending' on next boot (crash recovery).nnew file mode 100644@@@ -0,0 +1,24 @@++/**++ * A restart notice never leaves "OpenVibe is updating" on screen while chat is connected (owner's popout, 2026-10-18:++ * the notice stuck after a Live deploy). Live sends no notice (its restarts do not touch Chat's sockets); the client++ * shows "updating" only once the socket actually drops, and forgets the notice after a minute.++ *++ * node test/chat-restart-notice.test.js++ */++'use strict';++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const index = fs.readFileSync(path.join(root, 'server', 'index.js'), 'utf8');++const chat = fs.readFileSync(path.join(root, 'public', 'js', 'chat.js'), 'utf8');++++assert.ok(!/type: 'server_restart'/.test(index), "Live's shutdown sends no chat restart notice");++const at = chat.indexOf("case 'server_restart':");++assert.ok(at > 0, 'the client still understands the notice (Chat sends it)');++const body = chat.slice(at, chat.indexOf('break;', at));++assert.ok(!/_chatConnState\(/.test(body), 'the notice alone changes nothing on screen');++assert.ok(/_chatServerUpdating = true/.test(body) && /setTimeout\(\(\) => \{ _chatServerUpdating = false; \}, 60000\)/.test(body), 'it arms a quick reconnect for a minute');++assert.ok(/_chatConnState\(_chatServerUpdating \? 'updating' : 'reconnecting'\)/.test(chat), 'a drop after the notice says "updating"');++console.log('chat restart notice: all checks passed');@@@ -30,8 +30,8 @@ assert.match(call, /deadlineExitCode: 1/);assert.match(call, /beforeDrain:/);assert.ok(at > source.indexOf('const server = http.createServer(app);'), 'the stopper is created after the server exists');const stopAt = call.indexOf('stop: [');--assert.ok(stopAt > 0 && call.indexOf('_bootComplete = false', stopAt) < call.indexOf("require('./chat/chat-delivery')", stopAt),-- 'readiness flips before anything else stops');++const firstStep = call.slice(stopAt, call.indexOf('\n },', stopAt));++assert.ok(stopAt > 0 && firstStep.includes('_bootComplete = false'), 'readiness flips before anything else stops (the first stop step)');const fn = source.slice(source.indexOf('function shutdown('), source.indexOf("process.on('SIGTERM', shutdown)"));assert.match(fn, /if \(drill\.enabled\) \{/, 'the drill branch is still there');
@@ -138,8 +138,9 @@ Client WS → OpenVibe.Chat (127.0.0.1:4400; nginx routes /ws/chat, /api/chat/,
@@ -138,8 +138,9 @@ Client WS → OpenVibe.Chat (127.0.0.1:4400; nginx routes /ws/chat, /api/chat/,
Live runs no chat WebSocket server or chat REST routes: it delivers to Chat's typed ingress through
Live runs no chat WebSocket server or chat REST routes: it delivers to Chat's typed ingress through
`server/chat/chat-delivery.js` (producer pushes, moderation, cache hints) and reads Chat's snapshot for
`server/chat/chat-delivery.js` (producer pushes, moderation, cache hints) and reads Chat's snapshot for
presence. The room is derived from the stream the client joined; offline channel chat applies the
presence. The room is derived from the stream the client joined; offline channel chat applies the
-channel's bans and chat rules. Reconnects are jittered; a `server_restart` notice shows "OpenVibe is
-updating" and refills history on reconnect. See [chat-system.md](chat-system.md).
+channel's bans and chat rules. Reconnects are jittered; Chat's `server_restart` notice makes the next reconnect quick and,
+if the socket then drops, shows "OpenVibe is updating" until it is back (history refills on reconnect). Live sends
+no such notice: its restarts do not touch chat sockets. See [chat-system.md](chat-system.md).
## Server runtime
## Server runtime
@@ -2787,6 +2787,7 @@ function initChat(streamId, channelUserId = null) {
@@ -2787,6 +2787,7 @@ function initChat(streamId, channelUserId = null) {
*/
*/
let _chatConnDropAt = 0;
let _chatConnDropAt = 0;
let _chatServerUpdating = false;
let _chatServerUpdating = false;
+let _chatUpdatingTimer = 0;
function _chatConnState(state, detail) {
function _chatConnState(state, detail) {
const { messages: container } = getChatEl();
const { messages: container } = getChatEl();
if (container) {
if (container) {
@@ -3428,10 +3429,14 @@ function handleChatMessage(msg) {
@@ -3428,10 +3429,14 @@ function handleChatMessage(msg) {
addSystemMessage('Message blocked by this streamer\'s Anti-Slur Nudge setting.');
addSystemMessage('Message blocked by this streamer\'s Anti-Slur Nudge setting.');
break;
break;
case 'server_restart':
case 'server_restart':
- // A deploy is restarting the server. No reload needed: the socket reconnects on its own
- // (with a short random delay, so everyone does not arrive at once) and history refills.
+ // A deploy is restarting the chat server. No reload needed: if the socket drops it reconnects
+ // on its own (with a short random delay, so everyone does not arrive at once), and only then
+ // does the reader see "updating". A restart that never drops this socket says nothing: the
+ // notice used to show "updating" at once and clear it only on a reconnect, so it stuck when
+ // the socket stayed up (owner's popout, 2026-10-18).
_chatServerUpdating = true;
_chatServerUpdating = true;
- _chatConnState('updating');
+ clearTimeout(_chatUpdatingTimer);
+ _chatUpdatingTimer = setTimeout(() => { _chatServerUpdating = false; }, 60000);
break;
break;
case 'update': {
case 'update': {
// Deploy notice: one rolling card per run of deploys, replaced in place by id (never duplicated).
// Deploy notice: one rolling card per run of deploys, replaced in place by id (never duplicated).
@@ -163,16 +163,6 @@ const stopper = gracefulStop({
@@ -163,16 +163,6 @@ const stopper = gracefulStop({
// gating on /api/ready should see that before the socket actually closes.
// gating on /api/ready should see that before the socket actually closes.
_bootComplete = false;
_bootComplete = false;
},
},
- async () => {
- // Notify all chat clients before closing connections
- try {
- await require('./chat/chat-delivery').event({ kind: 'all' }, {
- type: 'server_restart',
- message: '⚙️ Chat server restarting — you will be reconnected automatically.',
- timestamp: new Date().toISOString(),
- });
- } catch { /* non-critical */ }
- },
// Kill any in-flight whisper/ffmpeg transcription children so they don't orphan
// Kill any in-flight whisper/ffmpeg transcription children so they don't orphan
// (and so their temp files get cleaned by the close handlers). The interrupted VOD
// (and so their temp files get cleaned by the close handlers). The interrupted VOD
// is left in 'processing' → re-queued to 'pending' on next boot (crash recovery).
// is left in 'processing' → re-queued to 'pending' on next boot (crash recovery).
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+/**
+ * A restart notice never leaves "OpenVibe is updating" on screen while chat is connected (owner's popout, 2026-10-18:
+ * the notice stuck after a Live deploy). Live sends no notice (its restarts do not touch Chat's sockets); the client
+ * shows "updating" only once the socket actually drops, and forgets the notice after a minute.
+ *
+ * node test/chat-restart-notice.test.js
+ */
+'use strict';
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const index = fs.readFileSync(path.join(root, 'server', 'index.js'), 'utf8');
+const chat = fs.readFileSync(path.join(root, 'public', 'js', 'chat.js'), 'utf8');
+
+assert.ok(!/type: 'server_restart'/.test(index), "Live's shutdown sends no chat restart notice");
+const at = chat.indexOf("case 'server_restart':");
+assert.ok(at > 0, 'the client still understands the notice (Chat sends it)');
+const body = chat.slice(at, chat.indexOf('break;', at));
+assert.ok(!/_chatConnState\(/.test(body), 'the notice alone changes nothing on screen');
+assert.ok(/_chatServerUpdating = true/.test(body) && /setTimeout\(\(\) => \{ _chatServerUpdating = false; \}, 60000\)/.test(body), 'it arms a quick reconnect for a minute');
+assert.ok(/_chatConnState\(_chatServerUpdating \? 'updating' : 'reconnecting'\)/.test(chat), 'a drop after the notice says "updating"');
+console.log('chat restart notice: all checks passed');
@@ -30,8 +30,8 @@ assert.match(call, /deadlineExitCode: 1/);
@@ -30,8 +30,8 @@ assert.match(call, /deadlineExitCode: 1/);
assert.match(call, /beforeDrain:/);
assert.match(call, /beforeDrain:/);
assert.ok(at > source.indexOf('const server = http.createServer(app);'), 'the stopper is created after the server exists');
assert.ok(at > source.indexOf('const server = http.createServer(app);'), 'the stopper is created after the server exists');
const stopAt = call.indexOf('stop: [');
const stopAt = call.indexOf('stop: [');
-assert.ok(stopAt > 0 && call.indexOf('_bootComplete = false', stopAt) < call.indexOf("require('./chat/chat-delivery')", stopAt),
- 'readiness flips before anything else stops');
+const firstStep = call.slice(stopAt, call.indexOf('\n },', stopAt));
+assert.ok(stopAt > 0 && firstStep.includes('_bootComplete = false'), 'readiness flips before anything else stops (the first stop step)');
const fn = source.slice(source.indexOf('function shutdown('), source.indexOf("process.on('SIGTERM', shutdown)"));
const fn = source.slice(source.indexOf('function shutdown('), source.indexOf("process.on('SIGTERM', shutdown)"));
assert.match(fn, /if \(drill\.enabled\) \{/, 'the drill branch is still there');
assert.match(fn, /if \(drill\.enabled\) \{/, 'the drill branch is still there');