This PR breaks the large web and player bootstrap files into smaller modules with clearer ownership.
Web changes: Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js. Kept web.js focused on wiring and server startup. Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh. Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. Player changes: Split websocket/runtime handling into runtime.js. Split playlist assembly and revision hashing into playlist.js. Split onboarding and player HTTP routes into dedicated modules. Split render utilities and template loading into render-helpers.js. Kept player.js mostly as startup/orchestration. Validation: Rebuilt both services with Docker Compose. Smoke-checked web and player routes after the refactor. Verified get_errors was clean on the touched modules.
This commit is contained in:
Vendored
+208
@@ -0,0 +1,208 @@
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const { createDashboardStateService } = require('./dashboard-state');
|
||||
const { createUploadSyncService } = require('./upload-sync');
|
||||
|
||||
function createWebBootstrap(options) {
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
const playerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||
const playerPublicBaseUrl = String(options && options.playerPublicBaseUrl || '').replace(/\/$/, '');
|
||||
const uploadDir = String(options && options.uploadDir || '').trim();
|
||||
const dashboardRefreshIntervalMs = Number(options && options.dashboardRefreshIntervalMs || 2000);
|
||||
const formatDashboardDate = options && options.formatDashboardDate;
|
||||
const notifyPlayerScreens = options && options.notifyPlayerScreens;
|
||||
|
||||
if (!pool || !common || !uploadDir || typeof formatDashboardDate !== 'function' || typeof notifyPlayerScreens !== 'function') {
|
||||
throw new Error('createWebBootstrap requires the web bootstrap dependencies.');
|
||||
}
|
||||
|
||||
const dashboardWs = new WebSocketServer({ noServer: true });
|
||||
const dashboardClients = new Set();
|
||||
const playerSnapshotCache = new Map();
|
||||
const playerSnapshotSockets = new Map();
|
||||
let dashboardRefreshInFlight = null;
|
||||
let broadcastDashboardState = null;
|
||||
|
||||
function getPlayerSnapshotSocketUrl(slug) {
|
||||
const url = new URL(playerInternalBaseUrl.replace(/^http/, 'ws'));
|
||||
url.pathname = `/ws/screens/${encodeURIComponent(slug)}/events`;
|
||||
url.search = '';
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function storePlayerSnapshot(slug, connections) {
|
||||
const normalizedSlug = String(slug || '').trim();
|
||||
const normalizedConnections = Array.isArray(connections) ? connections : [];
|
||||
playerSnapshotCache.set(normalizedSlug, {
|
||||
slug: normalizedSlug,
|
||||
count: normalizedConnections.length,
|
||||
connections: normalizedConnections
|
||||
});
|
||||
}
|
||||
|
||||
function clearPlayerSnapshotSocket(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
playerSnapshotSockets.delete(key);
|
||||
}
|
||||
|
||||
function ensurePlayerSnapshotSubscription(slug) {
|
||||
const key = String(slug || '').trim();
|
||||
if (!key || playerSnapshotSockets.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = new WebSocket(getPlayerSnapshotSocketUrl(key));
|
||||
playerSnapshotSockets.set(key, socket);
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
try {
|
||||
const payload = JSON.parse(String(event.data || '{}'));
|
||||
if (!payload || payload.type !== 'snapshot' || payload.slug !== key) {
|
||||
return;
|
||||
}
|
||||
storePlayerSnapshot(key, payload.connections || []);
|
||||
if (broadcastDashboardState) {
|
||||
broadcastDashboardState().catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore malformed player snapshot payloads.
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
clearPlayerSnapshotSocket(key);
|
||||
setTimeout(function () {
|
||||
ensurePlayerSnapshotSubscription(key);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
socket.onerror = function () {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_error) {
|
||||
// ignore close errors
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const dashboardStateService = createDashboardStateService({
|
||||
pool: pool,
|
||||
common: common,
|
||||
playerSnapshotCache: playerSnapshotCache,
|
||||
playerSnapshotSockets: playerSnapshotSockets,
|
||||
ensurePlayerSnapshotSubscription: ensurePlayerSnapshotSubscription,
|
||||
playerPublicBaseUrl: playerPublicBaseUrl,
|
||||
formatDashboardDate: formatDashboardDate
|
||||
});
|
||||
const buildDashboardState = dashboardStateService.buildDashboardState;
|
||||
|
||||
const uploadSyncService = createUploadSyncService({
|
||||
common: common,
|
||||
playerInternalBaseUrl: playerInternalBaseUrl,
|
||||
playerSnapshotCache: playerSnapshotCache,
|
||||
notifyPlayerScreens: notifyPlayerScreens
|
||||
});
|
||||
|
||||
const upload = uploadSyncService.createUploadMiddleware(uploadDir);
|
||||
const collectUploadReferencesFromSlide = uploadSyncService.collectUploadReferencesFromSlide;
|
||||
const collectUploadReferencesFromTemplate = uploadSyncService.collectUploadReferencesFromTemplate;
|
||||
const collectUploadReferencesFromPayload = uploadSyncService.collectUploadReferencesFromPayload;
|
||||
const syncPlaylistUploadsOnChange = uploadSyncService.syncPlaylistUploadsOnChange;
|
||||
const syncExistingUploadsToPlayer = uploadSyncService.syncExistingUploadsToPlayer;
|
||||
|
||||
async function sendDashboardStateToSocket(socket) {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
const state = await buildDashboardState();
|
||||
socket.send(JSON.stringify({ type: 'dashboard-state', state: state }));
|
||||
}
|
||||
|
||||
broadcastDashboardState = async function () {
|
||||
if (dashboardRefreshInFlight) {
|
||||
return dashboardRefreshInFlight;
|
||||
}
|
||||
|
||||
dashboardRefreshInFlight = (async function () {
|
||||
const state = await buildDashboardState();
|
||||
const payload = JSON.stringify({ type: 'dashboard-state', state: state });
|
||||
for (const socket of dashboardClients) {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(payload);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
})().finally(function () {
|
||||
dashboardRefreshInFlight = null;
|
||||
});
|
||||
|
||||
return dashboardRefreshInFlight;
|
||||
};
|
||||
|
||||
function installDashboardWebsocket(server, loadCurrentUser) {
|
||||
server.on('upgrade', async function (request, socket, head) {
|
||||
let pathname = '';
|
||||
try {
|
||||
pathname = new URL(request.url, 'http://localhost').pathname;
|
||||
} catch (_error) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname !== '/ws/admin/dashboard') {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentUser = await loadCurrentUser(pool, request);
|
||||
if (!currentUser) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
} catch (_error) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardWs.handleUpgrade(request, socket, head, function (ws) {
|
||||
dashboardWs.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardWs.on('connection', function (socket) {
|
||||
dashboardClients.add(socket);
|
||||
sendDashboardStateToSocket(socket);
|
||||
|
||||
socket.on('close', function () {
|
||||
dashboardClients.delete(socket);
|
||||
});
|
||||
|
||||
socket.on('error', function () {
|
||||
dashboardClients.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(function () {
|
||||
broadcastDashboardState().catch(function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
}, dashboardRefreshIntervalMs);
|
||||
}
|
||||
|
||||
return {
|
||||
upload: upload,
|
||||
buildDashboardState: buildDashboardState,
|
||||
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
|
||||
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
|
||||
collectUploadReferencesFromPayload: collectUploadReferencesFromPayload,
|
||||
syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange,
|
||||
syncExistingUploadsToPlayer: syncExistingUploadsToPlayer,
|
||||
broadcastDashboardState: broadcastDashboardState,
|
||||
installDashboardWebsocket: installDashboardWebsocket
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createWebBootstrap };
|
||||
Reference in New Issue
Block a user