From 55f9597e1adae67014f51dd566e4e353ae22ce3e Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 30 Mar 2026 22:42:55 -0700 Subject: [PATCH] feat: add tagSessions and mergeSources for multi-source parallel polling These two functions enable multi-source session federation: - tagSessions(sessions, deviceName, sourceUrl): tags each session with deviceName, sourceUrl, and sessionKey (format: sourceUrl::name), returns new objects without mutating originals - mergeSources(results): takes [{source, sessions}] objects, tags each source's sessions and concatenates into a single flat array Both functions are exported in module.exports. 6 new tests added covering all specified behaviors. --- muxplex/frontend/app.js | 41 +++++++++++++++++++ muxplex/frontend/tests/test_app.mjs | 61 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index b35230d..24bbb5b 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1663,6 +1663,44 @@ function bindStaticEventListeners() { // ─── Test-only helpers ──────────────────────────────────────────────────────── +// ─── Multi-source parallel polling ───────────────────────────────────────── + +/** + * Tag each session in the array with deviceName, sourceUrl, and sessionKey. + * Returns new session objects; does NOT mutate originals. + * sessionKey format: sourceUrl + '::' + name + * @param {object[]} sessions + * @param {string} deviceName + * @param {string} sourceUrl + * @returns {object[]} + */ +function tagSessions(sessions, deviceName, sourceUrl) { + return (sessions || []).map(function(s) { + return Object.assign({}, s, { + deviceName: deviceName, + sourceUrl: sourceUrl, + sessionKey: sourceUrl + '::' + (s.name || ''), + }); + }); +} + +/** + * Merge sessions from multiple sources into a single flat array. + * Each result is an object with {source, sessions}. + * Tags each source's sessions with deviceName/sourceUrl/sessionKey. + * @param {Array<{source: {name: string, url: string}, sessions: object[]}>} results + * @returns {object[]} + */ +function mergeSources(results) { + var all = []; + for (var i = 0; i < results.length; i++) { + var r = results[i]; + var tagged = tagSessions(r.sessions, r.source.name, r.source.url); + for (var j = 0; j < tagged.length; j++) { all.push(tagged[j]); } + } + return all; +} + /** Test-only: set _currentSessions directly. */ function _setCurrentSessions(sessions) { _currentSessions = sessions; @@ -1778,6 +1816,9 @@ if (typeof module !== 'undefined' && module.exports) { createNewSession, // Kill session killSession, + // Multi-source parallel polling + tagSessions, + mergeSources, // Test-only helpers _setCurrentSessions, _setViewMode, diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 5ef0479..c458314 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2072,4 +2072,65 @@ test('buildSources strips trailing slash from remote URLs', () => { assert.strictEqual(sources[2].url, 'https://server2.example.com', 'multiple trailing slashes should be stripped from remote URL'); }); +// --- tagSessions --- + +test('tagSessions adds deviceName and sourceUrl to each session', () => { + const sessions = [{ name: 'work' }, { name: 'play' }]; + const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); + assert.strictEqual(result[0].deviceName, 'Laptop', 'first session should have deviceName set'); + assert.strictEqual(result[0].sourceUrl, 'https://host.example.com', 'first session should have sourceUrl set'); + assert.strictEqual(result[1].deviceName, 'Laptop', 'second session should have deviceName set'); + assert.strictEqual(result[1].sourceUrl, 'https://host.example.com', 'second session should have sourceUrl set'); +}); + +test('tagSessions adds sessionKey formatted as sourceUrl::name', () => { + const sessions = [{ name: 'my-session' }]; + const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); + assert.strictEqual(result[0].sessionKey, 'https://host.example.com::my-session', 'sessionKey should be sourceUrl::name'); +}); + +test('tagSessions handles empty sessions input (returns empty array)', () => { + const result = app.tagSessions([], 'Laptop', 'https://host.example.com'); + assert.deepStrictEqual(result, [], 'tagSessions should return empty array for empty input'); +}); + +test('tagSessions does not mutate the original session objects', () => { + const original = { name: 'work' }; + const sessions = [original]; + const result = app.tagSessions(sessions, 'Laptop', 'https://host.example.com'); + assert.ok(!('deviceName' in original), 'original session should not be mutated'); + assert.ok(!('sourceUrl' in original), 'original session should not have sourceUrl added'); + assert.ok(!('sessionKey' in original), 'original session should not have sessionKey added'); + assert.notStrictEqual(result[0], original, 'returned session should be a new object, not the original'); +}); + +// --- mergeSources --- + +test('mergeSources combines sessions from multiple sources with correct sessionKeys', () => { + const results = [ + { + source: { name: 'Laptop', url: '' }, + sessions: [{ name: 'local-session' }], + }, + { + source: { name: 'Server', url: 'https://server.example.com' }, + sessions: [{ name: 'remote-session' }], + }, + ]; + const merged = app.mergeSources(results); + assert.strictEqual(merged.length, 2, 'merged array should have 2 sessions total'); + const local = merged.find((s) => s.name === 'local-session'); + const remote = merged.find((s) => s.name === 'remote-session'); + assert.ok(local, 'local-session should be present'); + assert.ok(remote, 'remote-session should be present'); + assert.strictEqual(local.sessionKey, '::local-session', 'local session key should be ::local-session'); + assert.strictEqual(remote.sessionKey, 'https://server.example.com::remote-session', 'remote session key should include url'); + assert.strictEqual(remote.deviceName, 'Server', 'remote session should have correct deviceName'); +}); + +test('mergeSources returns empty array for empty input', () => { + const result = app.mergeSources([]); + assert.deepStrictEqual(result, [], 'mergeSources should return empty array for empty input'); +}); +