From 22a68c2ec620793a45d4fbe994fb0fde5465b43e Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 6 Oct 2026 19:04:56 +1300 Subject: [PATCH 01/29] Limit sendUpdate event listener --- server/app.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/app.js b/server/app.js index adc7c0136..a15f298ad 100644 --- a/server/app.js +++ b/server/app.js @@ -195,6 +195,7 @@ export default async function createApp(vite) { }); // Create Event Stream source for pages to listen to + let initStreamListener = false; app.get('/stream', (req, res)=>{ res.writeHead(200, { 'Content-Type' : 'text/event-stream', @@ -203,10 +204,13 @@ export default async function createApp(vite) { 'Content-Encoding' : 'none' }); - Stream.on('sendUpdate', (event, data)=>{ - if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); } - res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); - }); + if(!initStreamListener){ + initStreamListener = true; + Stream.on('sendUpdate', (event, data)=>{ + if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); } + res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); + }); + } }); // After Stream starts, send initStream event From aeea09b6f3b3d67897ae5c0951567c50b220282c Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 6 Oct 2026 20:08:21 +1300 Subject: [PATCH 02/29] Add eventSource.close() to SharePage useEffect return function --- client/homebrew/pages/sharePage/sharePage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 5280fc850..b56d9a6bd 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -68,6 +68,7 @@ const SharePage = (props)=>{ return ()=>{ document.removeEventListener('keydown', handleControlKeys); + eventSource.close(); }; }, []); From 885feb31b0bbbb10b226b3cf588c2fd0c700e98b Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 7 Oct 2026 00:07:56 +1300 Subject: [PATCH 03/29] Initial pass at individual brew streams --- client/homebrew/pages/sharePage/sharePage.jsx | 8 +++++--- server/app.js | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index b56d9a6bd..b973602cb 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -54,12 +54,13 @@ const SharePage = (props)=>{ fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); // listen for changes in the brew version - const eventSource = new EventSource('/stream'); + const eventSource = new EventSource(`/stream/${props.brew.shareId}`); eventSource.addEventListener('message', (evt)=>{ const messageData = JSON.parse(evt.data); if(messageData.eventType == 'brewUpdated'){ - if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) { + // Share ID check no longer required + if(messageData.version != currentBrew.version) { console.log('should fetch brew'); fetchUpdatedBrew(); } @@ -68,7 +69,8 @@ const SharePage = (props)=>{ return ()=>{ document.removeEventListener('keydown', handleControlKeys); - eventSource.close(); + eventSource.removeEventListener('message'); + // eventSource.close(); }; }, []); diff --git a/server/app.js b/server/app.js index a15f298ad..bdbe16977 100644 --- a/server/app.js +++ b/server/app.js @@ -196,19 +196,27 @@ export default async function createApp(vite) { // Create Event Stream source for pages to listen to let initStreamListener = false; - app.get('/stream', (req, res)=>{ + app.get('/stream/:id', (req, res)=>{ + const id = req.params.id; + res.writeHead(200, { 'Content-Type' : 'text/event-stream', - 'Cache-Control' : 'no-cache', + 'Cache-Control' : 'no-cache, no-transform', 'Connection' : 'keep-alive', 'Content-Encoding' : 'none' }); + res.write(`data: ${JSON.stringify({ init: Date.now().toString(), shareId: id, eventType: 'message' })}\n\n`); + if(!initStreamListener){ initStreamListener = true; Stream.on('sendUpdate', (event, data)=>{ - if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); } - res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); + if(isLocalEnvironment) { + console.log('Event:', event, '\nData:', data); + } + if(id == data.shareId) { + res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); + }; }); } }); From d61741461267d797fb4ffadd81f5ad4f01b2a44a Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 7 Oct 2026 00:08:55 +1300 Subject: [PATCH 04/29] Standardize property names --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index bdbe16977..096707de3 100644 --- a/server/app.js +++ b/server/app.js @@ -206,7 +206,7 @@ export default async function createApp(vite) { 'Content-Encoding' : 'none' }); - res.write(`data: ${JSON.stringify({ init: Date.now().toString(), shareId: id, eventType: 'message' })}\n\n`); + res.write(`data: ${JSON.stringify({ time: new Date, shareId: id, eventType: 'message' })}\n\n`); if(!initStreamListener){ initStreamListener = true; From f64ffb27dfd5c0c78d4799fdabd7a02ca66a66fc Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 7 Oct 2026 18:24:37 +1300 Subject: [PATCH 05/29] Revert "Standardize property names" This reverts commit d61741461267d797fb4ffadd81f5ad4f01b2a44a. --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index 096707de3..bdbe16977 100644 --- a/server/app.js +++ b/server/app.js @@ -206,7 +206,7 @@ export default async function createApp(vite) { 'Content-Encoding' : 'none' }); - res.write(`data: ${JSON.stringify({ time: new Date, shareId: id, eventType: 'message' })}\n\n`); + res.write(`data: ${JSON.stringify({ init: Date.now().toString(), shareId: id, eventType: 'message' })}\n\n`); if(!initStreamListener){ initStreamListener = true; From 9b93c799bd859efbf8ef4bdad55db73935db778c Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 7 Oct 2026 18:24:44 +1300 Subject: [PATCH 06/29] Revert "Initial pass at individual brew streams" This reverts commit 885feb31b0bbbb10b226b3cf588c2fd0c700e98b. --- client/homebrew/pages/sharePage/sharePage.jsx | 8 +++----- server/app.js | 16 ++++------------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index b973602cb..b56d9a6bd 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -54,13 +54,12 @@ const SharePage = (props)=>{ fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); // listen for changes in the brew version - const eventSource = new EventSource(`/stream/${props.brew.shareId}`); + const eventSource = new EventSource('/stream'); eventSource.addEventListener('message', (evt)=>{ const messageData = JSON.parse(evt.data); if(messageData.eventType == 'brewUpdated'){ - // Share ID check no longer required - if(messageData.version != currentBrew.version) { + if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) { console.log('should fetch brew'); fetchUpdatedBrew(); } @@ -69,8 +68,7 @@ const SharePage = (props)=>{ return ()=>{ document.removeEventListener('keydown', handleControlKeys); - eventSource.removeEventListener('message'); - // eventSource.close(); + eventSource.close(); }; }, []); diff --git a/server/app.js b/server/app.js index bdbe16977..a15f298ad 100644 --- a/server/app.js +++ b/server/app.js @@ -196,27 +196,19 @@ export default async function createApp(vite) { // Create Event Stream source for pages to listen to let initStreamListener = false; - app.get('/stream/:id', (req, res)=>{ - const id = req.params.id; - + app.get('/stream', (req, res)=>{ res.writeHead(200, { 'Content-Type' : 'text/event-stream', - 'Cache-Control' : 'no-cache, no-transform', + 'Cache-Control' : 'no-cache', 'Connection' : 'keep-alive', 'Content-Encoding' : 'none' }); - res.write(`data: ${JSON.stringify({ init: Date.now().toString(), shareId: id, eventType: 'message' })}\n\n`); - if(!initStreamListener){ initStreamListener = true; Stream.on('sendUpdate', (event, data)=>{ - if(isLocalEnvironment) { - console.log('Event:', event, '\nData:', data); - } - if(id == data.shareId) { - res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); - }; + if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); } + res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); }); } }); From ea69d4c0d5d01b58873383356a6fec70c8a3b360 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 9 Oct 2026 19:23:27 +1300 Subject: [PATCH 07/29] Move to Subscription model --- client/homebrew/pages/sharePage/sharePage.jsx | 12 +++---- server/app.js | 18 ++-------- server/eventStreamSource.js | 34 ++++++++++++++++++- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index b56d9a6bd..4b03cffdf 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -54,15 +54,13 @@ const SharePage = (props)=>{ fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); // listen for changes in the brew version - const eventSource = new EventSource('/stream'); - eventSource.addEventListener('message', (evt)=>{ + const eventSource = new EventSource(`/stream/${props.brew.shareId}`); + eventSource.addEventListener('brewUpdated', (evt)=>{ const messageData = JSON.parse(evt.data); - if(messageData.eventType == 'brewUpdated'){ - if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) { - console.log('should fetch brew'); - fetchUpdatedBrew(); - } + if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) { + console.log('should fetch brew'); + fetchUpdatedBrew(); } }); diff --git a/server/app.js b/server/app.js index a15f298ad..9556b8a49 100644 --- a/server/app.js +++ b/server/app.js @@ -195,29 +195,17 @@ export default async function createApp(vite) { }); // Create Event Stream source for pages to listen to - let initStreamListener = false; - app.get('/stream', (req, res)=>{ + app.get('/stream/:id', (req, res)=>{ res.writeHead(200, { 'Content-Type' : 'text/event-stream', 'Cache-Control' : 'no-cache', 'Connection' : 'keep-alive', 'Content-Encoding' : 'none' }); - - if(!initStreamListener){ - initStreamListener = true; - Stream.on('sendUpdate', (event, data)=>{ - if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); } - res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); - }); - } + ; + Stream.subscribe(res); }); - // After Stream starts, send initStream event - setTimeout(()=>{ - Stream.emit('sendUpdate', 'initStream', { time: new Date }); - }, 1000); - // Local only if(isLocalEnvironment){ diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 66fdb3354..adb4e4f60 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -1,9 +1,41 @@ import { EventEmitter } from 'events'; +import { nanoid } from 'nanoid'; const Stream = new EventEmitter; +// After Stream starts, send initStream event +Stream.on('open', ()=>{ + Stream.emit('sendUpdate', 'initStream', { time: new Date }); +}); + +// Create array of stream subscribers +const subscribers = []; + +// Listener functions +const subscribe = function(res){ + const id = nanoid(); + subscribers.push({ id, 'stream': res }); + + res.write(`event: subscribe\ndata: { id: ${id} }\n\n`); + return id; +}; + +const unsubscribe = function(id){ + subscribers = subscribers.filter((sub)=>{ return sub.id != id; }); +}; + +// Create global sendUpdate listener +Stream.on('sendUpdate', (event, data)=>{ + console.log('Event:', event, '\nData:', data); + subscribers.forEach((sub, idx)=>{ + sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + }); +}); + export default { emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions on : (event, listener)=>{return Stream.on(event, listener);}, - off : (event, listener)=>{return Stream.off(event, listener);} + off : (event, listener)=>{return Stream.off(event, listener);}, + subscribe, + unsubscribe }; \ No newline at end of file From 4f744ce4d5a0c95c387595a76c164dc181085f6b Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 9 Oct 2026 19:27:56 +1300 Subject: [PATCH 08/29] Remove obsolete initStream event --- server/eventStreamSource.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index adb4e4f60..bfdcc8099 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -3,11 +3,6 @@ import { nanoid } from 'nanoid'; const Stream = new EventEmitter; -// After Stream starts, send initStream event -Stream.on('open', ()=>{ - Stream.emit('sendUpdate', 'initStream', { time: new Date }); -}); - // Create array of stream subscribers const subscribers = []; From 5a0b9d8aaee0ba8d08c93579d0f35298d9f36283 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 9 Oct 2026 20:28:04 +1300 Subject: [PATCH 09/29] Add debounced call to unsubscribe --- server/eventStreamSource.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index bfdcc8099..9d0574d40 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -1,10 +1,15 @@ import { EventEmitter } from 'events'; +import _ from 'lodash'; import { nanoid } from 'nanoid'; +const UNSUB_DELAY = 60000; //ms + const Stream = new EventEmitter; // Create array of stream subscribers -const subscribers = []; +let subscribers = []; + +const debounceUnsub = _.debounce((id)=>{ unsubscribe(id); }, UNSUB_DELAY, { leading: false, trailing: true }); // Listener functions const subscribe = function(res){ @@ -12,18 +17,29 @@ const subscribe = function(res){ subscribers.push({ id, 'stream': res }); res.write(`event: subscribe\ndata: { id: ${id} }\n\n`); + + debounceUnsub(id); return id; }; const unsubscribe = function(id){ + subscribers + .filter((sub)=>{return sub.id == id;}) + .forEach((sub)=>{ + sub.stream.write(`event: unsubscribe\ndata: { id: ${id} }\n\n`); + }); + subscribers = subscribers.filter((sub)=>{ return sub.id != id; }); }; // Create global sendUpdate listener Stream.on('sendUpdate', (event, data)=>{ console.log('Event:', event, '\nData:', data); - subscribers.forEach((sub, idx)=>{ + subscribers.forEach((sub)=>{ sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + + // invoke debounced unsub to delay execution + debounceUnsub(sub.id); }); }); From a51231535c276300af78a818835c6d6b1bb2cd2e Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sat, 10 Oct 2026 15:54:59 +1300 Subject: [PATCH 10/29] Incremental commit --- server/app.js | 2 +- server/eventStreamSource.js | 38 +++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/server/app.js b/server/app.js index 9556b8a49..c834c1616 100644 --- a/server/app.js +++ b/server/app.js @@ -203,7 +203,7 @@ export default async function createApp(vite) { 'Content-Encoding' : 'none' }); ; - Stream.subscribe(res); + Stream.subscribe(req.params.id, res); }); diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 9d0574d40..2f4acd312 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -12,21 +12,27 @@ let subscribers = []; const debounceUnsub = _.debounce((id)=>{ unsubscribe(id); }, UNSUB_DELAY, { leading: false, trailing: true }); // Listener functions -const subscribe = function(res){ - const id = nanoid(); - subscribers.push({ id, 'stream': res }); +const subscribe = function(shareId, res){ + const subscriber = { + id : nanoid(), // NanoID is assumed globally unique + shareId, + stream : res, + time : new Date + }; - res.write(`event: subscribe\ndata: { id: ${id} }\n\n`); + subscribers.push(subscriber); - debounceUnsub(id); - return id; + Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId, time: subscriber.time }); + + debounceUnsub(subscriber.id); + return subscriber.id; }; const unsubscribe = function(id){ subscribers .filter((sub)=>{return sub.id == id;}) .forEach((sub)=>{ - sub.stream.write(`event: unsubscribe\ndata: { id: ${id} }\n\n`); + Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId, time: new Date }); }); subscribers = subscribers.filter((sub)=>{ return sub.id != id; }); @@ -35,14 +41,22 @@ const unsubscribe = function(id){ // Create global sendUpdate listener Stream.on('sendUpdate', (event, data)=>{ console.log('Event:', event, '\nData:', data); - subscribers.forEach((sub)=>{ - sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + subscribers + .filter((sub)=>{return data.shareId == sub.shareId; }) + .forEach((sub)=>{ + sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); - // invoke debounced unsub to delay execution - debounceUnsub(sub.id); - }); + // invoke debounced unsub to delay execution + debounceUnsub(sub.id); + }); }); +// DEBUG: Report subscriber count periodically +const REPORT_DELAY = 1000; //ms +const reportIntervalId = setInterval(()=>{ + console.log('Subscriber count:', subscribers.length); +}, REPORT_DELAY); + export default { emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions on : (event, listener)=>{return Stream.on(event, listener);}, From 444227e44d0cb4c709e368027403734bf0addfa1 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 01:14:52 +1300 Subject: [PATCH 11/29] Filter stream updates by Share ID --- server/eventStreamSource.js | 42 ++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 2f4acd312..f6dd33042 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -2,29 +2,37 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; import { nanoid } from 'nanoid'; -const UNSUB_DELAY = 60000; //ms +// DEBUG +const DEBUG = { + showEvents : true, + showSubscriberCount : true +}; + +// Delays +const UNSUB_DELAY = 60 * 1000; //ms +const REPORT_DELAY = 5 * 1000; //ms + const Stream = new EventEmitter; // Create array of stream subscribers let subscribers = []; -const debounceUnsub = _.debounce((id)=>{ unsubscribe(id); }, UNSUB_DELAY, { leading: false, trailing: true }); - // Listener functions const subscribe = function(shareId, res){ + const id = nanoid(24); // NanoID is assumed globally unique; const subscriber = { - id : nanoid(), // NanoID is assumed globally unique + id, shareId, - stream : res, - time : new Date + stream : res, + time : new Date, + unsubTimer : setTimeout(()=>{ unsubscribe(id); }, UNSUB_DELAY) }; subscribers.push(subscriber); Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId, time: subscriber.time }); - debounceUnsub(subscriber.id); return subscriber.id; }; @@ -40,22 +48,26 @@ const unsubscribe = function(id){ // Create global sendUpdate listener Stream.on('sendUpdate', (event, data)=>{ - console.log('Event:', event, '\nData:', data); + if(DEBUG.showEvents) console.log('Event:', event, '\nData:', data); subscribers .filter((sub)=>{return data.shareId == sub.shareId; }) .forEach((sub)=>{ - sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + if(event == 'brewUpdated'){ + // Reset the unsubscription timer + clearTimeout(sub.unsubTimer); + sub.unsubTimer = setTimeout(()=>{ unsubscribe(sub.id); }, UNSUB_DELAY); + } - // invoke debounced unsub to delay execution - debounceUnsub(sub.id); + sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); }); }); // DEBUG: Report subscriber count periodically -const REPORT_DELAY = 1000; //ms -const reportIntervalId = setInterval(()=>{ - console.log('Subscriber count:', subscribers.length); -}, REPORT_DELAY); +if(DEBUG.showSubscriberCount){ + const reportIntervalId = setInterval(()=>{ + console.log('Subscriber count:', subscribers.length, ' @ ', new Date); + }, REPORT_DELAY); +} export default { emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions From 5d494c84357adf4ab43a5788a73feb588d8d317e Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 12:06:14 +1300 Subject: [PATCH 12/29] Seperate initial unsub delay from refresh on update unsub delay --- server/eventStreamSource.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index f6dd33042..ac7b1f63c 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -10,6 +10,7 @@ const DEBUG = { // Delays const UNSUB_DELAY = 60 * 1000; //ms +const REFRESH_DELAY = 60 * 1000; //ms const REPORT_DELAY = 5 * 1000; //ms @@ -55,7 +56,7 @@ Stream.on('sendUpdate', (event, data)=>{ if(event == 'brewUpdated'){ // Reset the unsubscription timer clearTimeout(sub.unsubTimer); - sub.unsubTimer = setTimeout(()=>{ unsubscribe(sub.id); }, UNSUB_DELAY); + sub.unsubTimer = setTimeout(()=>{ unsubscribe(sub.id); }, REFRESH_DELAY); } sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); From c60740092b2783d04a54f2736d0bbad9baa1583c Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 12:08:07 +1300 Subject: [PATCH 13/29] Move Stream routes to seperate API file --- server/app.js | 15 ++------------- server/stream.api.js | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 server/stream.api.js diff --git a/server/app.js b/server/app.js index c834c1616..dde6b6c27 100644 --- a/server/app.js +++ b/server/app.js @@ -20,6 +20,7 @@ import api from './homebrew.api.js'; const { homebrewApi, getBrew, getCSS } = api; import adminApi from './admin.api.js'; import vaultApi from './vault.api.js'; +import streamApi from './stream.api.js'; import pageRoutes from './page-routes.js'; import serveCompressedStaticAssets from './static-assets.mv.js'; @@ -32,7 +33,6 @@ import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; -import Stream from './eventStreamSource.js'; import dbCheck from './middleware/dbCheck.js'; import cors from 'cors'; @@ -115,6 +115,7 @@ export default async function createApp(vite) { app.use(homebrewApi); app.use(adminApi(vite)); app.use(vaultApi); + app.use(streamApi); String.prototype.replaceAll = function(s, r){return this.split(s).join(r);}; @@ -194,18 +195,6 @@ export default async function createApp(vite) { } }); - // Create Event Stream source for pages to listen to - app.get('/stream/:id', (req, res)=>{ - res.writeHead(200, { - 'Content-Type' : 'text/event-stream', - 'Cache-Control' : 'no-cache', - 'Connection' : 'keep-alive', - 'Content-Encoding' : 'none' - }); - ; - Stream.subscribe(req.params.id, res); - }); - // Local only if(isLocalEnvironment){ diff --git a/server/stream.api.js b/server/stream.api.js new file mode 100644 index 000000000..8d162d306 --- /dev/null +++ b/server/stream.api.js @@ -0,0 +1,24 @@ +import express from 'express'; + +import Stream from './eventStreamSource.js'; + +const app = express.Router(); + +// Create Event Stream source for pages to listen to +app.get('/api/stream/:id', (req, res)=>{ + res.writeHead(200, { + 'Content-Type' : 'text/event-stream', + 'Cache-Control' : 'no-cache', + 'Connection' : 'keep-alive', + 'Content-Encoding' : 'none' + }); + ; + Stream.subscribe(req.params.id, res); +}); + +app.get('/api/stream/unsubscribe/:id', (req, res)=>{ + Stream.unsubscribe(req.params?.id); + res.status(200).send(); +}); + +export default app; \ No newline at end of file From 360fd6d2cca96fd11f96cd6259289732b26db445 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 12:08:30 +1300 Subject: [PATCH 14/29] Add unsubscribe call on SharePage unload --- client/homebrew/pages/sharePage/sharePage.jsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 4b03cffdf..80d544c3a 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -54,7 +54,15 @@ const SharePage = (props)=>{ fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); // listen for changes in the brew version - const eventSource = new EventSource(`/stream/${props.brew.shareId}`); + const eventSource = new EventSource(`/api/stream/${props.brew.shareId}`); + + let subId = ''; + eventSource.addEventListener('subscribe', (evt)=>{ + const messageData = JSON.parse(evt.data); + + subId = messageData.id; + }); + eventSource.addEventListener('brewUpdated', (evt)=>{ const messageData = JSON.parse(evt.data); @@ -64,7 +72,11 @@ const SharePage = (props)=>{ } }); - return ()=>{ + window.addEventListener('beforeunload', async ()=>{ + await request.get(`/api/stream/unsubscribe/${subId}`); + }); + + return async ()=>{ document.removeEventListener('keydown', handleControlKeys); eventSource.close(); }; From d7f579b308a0b255918ad54033481d60f1f84261 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 12:46:30 +1300 Subject: [PATCH 15/29] Add randomized client identifier to reduce chance of grabbing the incorrect subscription ID --- client/homebrew/pages/sharePage/sharePage.jsx | 10 ++++++++-- server/eventStreamSource.js | 5 +++-- server/stream.api.js | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 80d544c3a..5a2ff849a 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -16,6 +16,7 @@ import request from '../../utils/request-middleware.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; +import { nanoid } from 'nanoid'; const SharePage = (props)=>{ const { disableMeta = false } = props; @@ -53,14 +54,19 @@ const SharePage = (props)=>{ document.addEventListener('keydown', handleControlKeys); fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); + // generate unique fingerprint + const clientFingerprint = nanoid(24); + // listen for changes in the brew version - const eventSource = new EventSource(`/api/stream/${props.brew.shareId}`); + const eventSource = new EventSource(`/api/stream/${props.brew.shareId}?client=${clientFingerprint}`); let subId = ''; eventSource.addEventListener('subscribe', (evt)=>{ const messageData = JSON.parse(evt.data); - subId = messageData.id; + if(messageData.client == clientFingerprint){ + subId = messageData.id; + } }); eventSource.addEventListener('brewUpdated', (evt)=>{ diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index ac7b1f63c..42539340c 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -20,7 +20,7 @@ const Stream = new EventEmitter; let subscribers = []; // Listener functions -const subscribe = function(shareId, res){ +const subscribe = function(shareId, res, clientFingerprint){ const id = nanoid(24); // NanoID is assumed globally unique; const subscriber = { id, @@ -32,7 +32,7 @@ const subscribe = function(shareId, res){ subscribers.push(subscriber); - Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId, time: subscriber.time }); + Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId, client: clientFingerprint, time: subscriber.time }); return subscriber.id; }; @@ -41,6 +41,7 @@ const unsubscribe = function(id){ subscribers .filter((sub)=>{return sub.id == id;}) .forEach((sub)=>{ + clearTimeout(sub.unsubTimer); Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId, time: new Date }); }); diff --git a/server/stream.api.js b/server/stream.api.js index 8d162d306..af84b1099 100644 --- a/server/stream.api.js +++ b/server/stream.api.js @@ -13,7 +13,7 @@ app.get('/api/stream/:id', (req, res)=>{ 'Content-Encoding' : 'none' }); ; - Stream.subscribe(req.params.id, res); + Stream.subscribe(req.params.id, res, req.query?.client); }); app.get('/api/stream/unsubscribe/:id', (req, res)=>{ From 1efefce08dfc3b1f0cc9b1bb54c8dc4dc3fa80ab Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 13:32:07 +1300 Subject: [PATCH 16/29] Clean up SharePage beforeunload listener --- client/homebrew/pages/sharePage/sharePage.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 5a2ff849a..d17fdbe39 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -78,12 +78,14 @@ const SharePage = (props)=>{ } }); - window.addEventListener('beforeunload', async ()=>{ + async function unsub(){ await request.get(`/api/stream/unsubscribe/${subId}`); - }); + }; + window.addEventListener('beforeunload', unsub); return async ()=>{ document.removeEventListener('keydown', handleControlKeys); + window.addEventListener('beforeunload', unsub); eventSource.close(); }; }, []); From 16a6b30cd2538dc85120b61402115a35b8b01f8f Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 13:46:51 +1300 Subject: [PATCH 17/29] Disable subscriber report --- server/eventStreamSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 42539340c..44457f1ef 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -5,7 +5,7 @@ import { nanoid } from 'nanoid'; // DEBUG const DEBUG = { showEvents : true, - showSubscriberCount : true + showSubscriberCount : false }; // Delays From 80884a8809a122c7ff692fceb09bae12a30df896 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 15:10:30 +1300 Subject: [PATCH 18/29] Add dev.json to config item --- .gitignore | 1 + server/config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 150d81008..57cb15ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ storage build/* config/local.* config/docker.* +config/dev.* todo.md startDB.bat diff --git a/server/config.js b/server/config.js index 69674595b..06f47c69a 100644 --- a/server/config.js +++ b/server/config.js @@ -4,4 +4,5 @@ export default nconf .argv() .env({ lowerCase: true }) .file('environment', { file: `config/${process.env.NODE_ENV}.json` }) + .file('development', { file: 'config/dev.json' }) .file('defaults', { file: 'config/default.json' }); \ No newline at end of file From 7b82971699903bb6fc8f508da2fea47cd6c67d87 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 15:11:02 +1300 Subject: [PATCH 19/29] Shift EventStream debug functions to use dev config --- server/eventStreamSource.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 44457f1ef..6c1b49123 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -2,10 +2,12 @@ import { EventEmitter } from 'events'; import _ from 'lodash'; import { nanoid } from 'nanoid'; +import config from './config.js'; + // DEBUG const DEBUG = { - showEvents : true, - showSubscriberCount : false + showEvents : config.get('showStreamEvents'), + showSubscriberCount : config.get('showStreamSubscriberCount') }; // Delays From 478cc6b444b9808aa0b1d2280a0b55e84b8f30a2 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 11 Oct 2026 20:47:22 +1300 Subject: [PATCH 20/29] Tweak stream filtering logic --- server/eventStreamSource.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 6c1b49123..a082e5f90 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -54,7 +54,10 @@ const unsubscribe = function(id){ Stream.on('sendUpdate', (event, data)=>{ if(DEBUG.showEvents) console.log('Event:', event, '\nData:', data); subscribers - .filter((sub)=>{return data.shareId == sub.shareId; }) + .filter((sub)=>{ + if(data?.id) return data.id == sub.id; + return data.shareId == sub.shareId; + }) .forEach((sub)=>{ if(event == 'brewUpdated'){ // Reset the unsubscription timer From d80085a67c819000781e9befce718cef4529d803 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 12 Oct 2026 17:52:04 +1300 Subject: [PATCH 21/29] Remove unnecessary shareID check --- client/homebrew/pages/sharePage/sharePage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index d17fdbe39..91abfb305 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -72,7 +72,7 @@ const SharePage = (props)=>{ eventSource.addEventListener('brewUpdated', (evt)=>{ const messageData = JSON.parse(evt.data); - if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) { + if(messageData.version != currentBrew.version) { console.log('should fetch brew'); fetchUpdatedBrew(); } From 195f18659de4fa588f3902400b529dd366fcddb0 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 12 Oct 2026 17:54:37 +1300 Subject: [PATCH 22/29] Fix type preventing event listener from being unloaded --- client/homebrew/pages/sharePage/sharePage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 91abfb305..d4cea70c3 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -85,7 +85,7 @@ const SharePage = (props)=>{ return async ()=>{ document.removeEventListener('keydown', handleControlKeys); - window.addEventListener('beforeunload', unsub); + window.removeEventListener('beforeunload', unsub); eventSource.close(); }; }, []); From 1bd64b397fc1d2cede9a4cfb87c3dc924d3fbd50 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Mon, 12 Oct 2026 21:35:21 +1300 Subject: [PATCH 23/29] Shit to res.close listener --- client/homebrew/pages/sharePage/sharePage.jsx | 42 +++++++------------ server/eventStreamSource.js | 21 +++------- server/stream.api.js | 11 +++-- 3 files changed, 27 insertions(+), 47 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index d4cea70c3..9c2bca04a 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -54,39 +54,29 @@ const SharePage = (props)=>{ document.addEventListener('keydown', handleControlKeys); fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme); - // generate unique fingerprint - const clientFingerprint = nanoid(24); + let eventSource; + // Start a stream if the brew has been updated in the last month + if(Date.parse(props.brew?.updatedAt) > (new Date).setDate((new Date).getDate() - 30)){ + // listen for changes in the brew version + eventSource = new EventSource(`/api/stream/${props.brew.shareId}`); - // listen for changes in the brew version - const eventSource = new EventSource(`/api/stream/${props.brew.shareId}?client=${clientFingerprint}`); + eventSource.addEventListener('brewUpdated', (evt)=>{ + const messageData = JSON.parse(evt.data); - let subId = ''; - eventSource.addEventListener('subscribe', (evt)=>{ - const messageData = JSON.parse(evt.data); + if(messageData.version != currentBrew.version) { + console.log('should fetch brew'); + fetchUpdatedBrew(); + } + }); + } - if(messageData.client == clientFingerprint){ - subId = messageData.id; - } - }); - - eventSource.addEventListener('brewUpdated', (evt)=>{ - const messageData = JSON.parse(evt.data); - - if(messageData.version != currentBrew.version) { - console.log('should fetch brew'); - fetchUpdatedBrew(); - } - }); - - async function unsub(){ - await request.get(`/api/stream/unsubscribe/${subId}`); + function unsub(){ + eventSource?.close(); }; - window.addEventListener('beforeunload', unsub); return async ()=>{ document.removeEventListener('keydown', handleControlKeys); - window.removeEventListener('beforeunload', unsub); - eventSource.close(); + unsub(); }; }, []); diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index a082e5f90..e709c6088 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -11,9 +11,7 @@ const DEBUG = { }; // Delays -const UNSUB_DELAY = 60 * 1000; //ms -const REFRESH_DELAY = 60 * 1000; //ms -const REPORT_DELAY = 5 * 1000; //ms +const REPORT_DELAY = config.get('REPORT_DELAY') || 30 * 60 * 1000; //ms const Stream = new EventEmitter; @@ -22,19 +20,17 @@ const Stream = new EventEmitter; let subscribers = []; // Listener functions -const subscribe = function(shareId, res, clientFingerprint){ +const subscribe = function(shareId, res){ const id = nanoid(24); // NanoID is assumed globally unique; const subscriber = { id, shareId, - stream : res, - time : new Date, - unsubTimer : setTimeout(()=>{ unsubscribe(id); }, UNSUB_DELAY) + stream : res }; subscribers.push(subscriber); - Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId, client: clientFingerprint, time: subscriber.time }); + Stream.emit('sendUpdate', 'subscribe', { id: subscriber.id, shareId: subscriber.shareId }); return subscriber.id; }; @@ -44,7 +40,8 @@ const unsubscribe = function(id){ .filter((sub)=>{return sub.id == id;}) .forEach((sub)=>{ clearTimeout(sub.unsubTimer); - Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId, time: new Date }); + Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId }); + sub?.stream?.status(200).end(); }); subscribers = subscribers.filter((sub)=>{ return sub.id != id; }); @@ -59,12 +56,6 @@ Stream.on('sendUpdate', (event, data)=>{ return data.shareId == sub.shareId; }) .forEach((sub)=>{ - if(event == 'brewUpdated'){ - // Reset the unsubscription timer - clearTimeout(sub.unsubTimer); - sub.unsubTimer = setTimeout(()=>{ unsubscribe(sub.id); }, REFRESH_DELAY); - } - sub?.stream?.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); }); }); diff --git a/server/stream.api.js b/server/stream.api.js index af84b1099..79636469c 100644 --- a/server/stream.api.js +++ b/server/stream.api.js @@ -12,13 +12,12 @@ app.get('/api/stream/:id', (req, res)=>{ 'Connection' : 'keep-alive', 'Content-Encoding' : 'none' }); - ; - Stream.subscribe(req.params.id, res, req.query?.client); -}); -app.get('/api/stream/unsubscribe/:id', (req, res)=>{ - Stream.unsubscribe(req.params?.id); - res.status(200).send(); + const subId = Stream.subscribe(req.params.id, res); + + res.on('close', ()=>{ + Stream.unsubscribe(subId); + }); }); export default app; \ No newline at end of file From fe1a33ecfeea413654980802bacd760eb15ba214 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 17:32:43 +1300 Subject: [PATCH 24/29] Remove unneeded NanoID import from SharePage --- client/homebrew/pages/sharePage/sharePage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 9c2bca04a..9efa2d9e3 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -16,7 +16,6 @@ import request from '../../utils/request-middleware.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; -import { nanoid } from 'nanoid'; const SharePage = (props)=>{ const { disableMeta = false } = props; From 0f3bc6822208bd0614c75d9ee4394f30fbdf4b80 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 17:33:02 +1300 Subject: [PATCH 25/29] Remove unneeded unsub function --- client/homebrew/pages/sharePage/sharePage.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 9efa2d9e3..002844c2c 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -69,13 +69,9 @@ const SharePage = (props)=>{ }); } - function unsub(){ - eventSource?.close(); - }; - - return async ()=>{ + return ()=>{ document.removeEventListener('keydown', handleControlKeys); - unsub(); + eventSource?.close(); }; }, []); From b2038e1a45a803878740b51df176dcdcbe634d3d Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 17:58:21 +1300 Subject: [PATCH 26/29] Remove obsolete reference to removed subTimer --- server/eventStreamSource.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index e709c6088..7a9e0d90e 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -39,7 +39,6 @@ const unsubscribe = function(id){ subscribers .filter((sub)=>{return sub.id == id;}) .forEach((sub)=>{ - clearTimeout(sub.unsubTimer); Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId }); sub?.stream?.status(200).end(); }); From 8672dbe1e9564a92da584c9642bc5096dec34a04 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 18:08:42 +1300 Subject: [PATCH 27/29] Comment out unused unsubscribe event --- server/eventStreamSource.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 7a9e0d90e..568acaab6 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -39,7 +39,8 @@ const unsubscribe = function(id){ subscribers .filter((sub)=>{return sub.id == id;}) .forEach((sub)=>{ - Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId }); + // Unsubscribe event - not currently used + // Stream.emit('sendUpdate', 'unsubscribe', { id: sub.id, shareId: sub.shareId }); sub?.stream?.status(200).end(); }); From 8d3edc7cd13403c2a520a9c7096c6f416025ab51 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 22:33:19 +1300 Subject: [PATCH 28/29] Add periodic stream heartbeat to stop disconnection/reconnection cycle --- server/eventStreamSource.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 568acaab6..537beb81a 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -12,6 +12,7 @@ const DEBUG = { // Delays const REPORT_DELAY = config.get('REPORT_DELAY') || 30 * 60 * 1000; //ms +const HEARTBEAT_DELAY = config.get('HEARTBEAT_DELAY') || 20 * 1000; //ms const Stream = new EventEmitter; @@ -47,6 +48,20 @@ const unsubscribe = function(id){ subscribers = subscribers.filter((sub)=>{ return sub.id != id; }); }; +// Create heartbeat +const heartbeatId = setInterval(()=>{ + if(DEBUG.showEvents){ console.log('STREAM HEARTBEAT @ ', new Date); }; + subscribers + .forEach((sub)=>{ + sub?.stream?.write(`:\n\n`); + }); +}, HEARTBEAT_DELAY); + +Stream.on('close', ()=>{ + console.log('STREAM CLOSE'); + clearInterval(heartbeatId); +}); + // Create global sendUpdate listener Stream.on('sendUpdate', (event, data)=>{ if(DEBUG.showEvents) console.log('Event:', event, '\nData:', data); From 4479b7ca95b9aef2dd7de05811ba8047ea1a3805 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 13 Oct 2026 23:13:53 +1300 Subject: [PATCH 29/29] Prevent stream heartbeat from loading in test environments --- server/eventStreamSource.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js index 537beb81a..52db436d1 100644 --- a/server/eventStreamSource.js +++ b/server/eventStreamSource.js @@ -49,13 +49,16 @@ const unsubscribe = function(id){ }; // Create heartbeat -const heartbeatId = setInterval(()=>{ - if(DEBUG.showEvents){ console.log('STREAM HEARTBEAT @ ', new Date); }; - subscribers - .forEach((sub)=>{ - sub?.stream?.write(`:\n\n`); - }); -}, HEARTBEAT_DELAY); +const heartbeatId = config.get('node_env') != 'test' ? + setInterval(()=>{ + if(DEBUG.showEvents){ console.log('STREAM HEARTBEAT @ ', new Date); }; + subscribers + .forEach((sub)=>{ + sub?.stream?.write(`:\n\n`); + }); + }, HEARTBEAT_DELAY) + : + undefined; Stream.on('close', ()=>{ console.log('STREAM CLOSE');