Files
MultiRoombaRover/server/src/services/sessionService.js
T
2026-01-23 02:16:37 -05:00

204 lines
5.9 KiB
JavaScript

const io = require('../globals/io');
const logger = require('../globals/logger').child('sessionService');
const { getRole, roleEvents } = require('./roleService');
const { getMode, modeEvents } = require('./modeManager');
const roverManager = require('./roverManager');
const { managerEvents } = roverManager;
const assignmentService = require('./assignmentService');
const { getActiveDrivers, getTurnQueues, turnEvents } = require('./turnService');
const { getRoomCameras, roomCameraEvents } = require('./roomCameraService');
const { getState: getHomeAssistantState, homeAssistantEvents } = require('./homeAssistantService');
const { getNickname, nicknameEvents } = require('./nicknameService');
const { getReplayState, replayEvents } = require('./replayService');
const { getReplaySources } = require('./replaySourceService');
const { getHealthSnapshot } = require('./healthService');
const { loadConfig } = require('../helpers/configLoader');
const { getCommunityGoal } = require('./communityGoalService');
const { subscribe } = require('./eventBus');
const discordInvite = loadConfig().discord?.invite || null;
const kofiLink = loadConfig().kofi?.link || null;
logger.info('Discord invite loaded:', discordInvite ? 'present' : 'not configured');
logger.info('Ko-fi link loaded:', kofiLink ? 'present' : 'not configured');
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
let lastActivitySync = 0;
let pendingActivitySync = null;
function buildUserEntry(socket) {
if (!socket) return null;
const role = getRole(socket);
const assignment = assignmentService.describeAssignment(socket.id);
const primaryRover = roverManager.getPrimaryRoverForSocket(socket.id);
return {
socketId: socket.id,
nickname: getNickname(socket) || null,
role,
roverId: primaryRover || assignment?.roverId || null,
};
}
function buildSession(socket) {
const users = Array.from(io.sockets.sockets.values())
.map((sock) => buildUserEntry(sock))
.filter(Boolean);
return {
socketId: socket?.id || null,
role: getRole(socket),
mode: getMode(),
roster: roverManager.getRoster(),
assignment: assignmentService.describeAssignment(socket?.id || ''),
activeDrivers: getActiveDrivers(),
turnQueues: getTurnQueues(),
roomCameras: getRoomCameras(),
homeAssistant: getHomeAssistantState(),
replay: getReplayState(),
replaySources: getReplaySources(),
health: getHealthSnapshot(),
communityGoal: getCommunityGoal(),
users,
discord: {
invite: discordInvite,
},
kofi: {
link: kofiLink,
},
};
}
function syncSocket(socket) {
if (!socket) return;
const payload = buildSession(socket);
logger.info('Syncing session', socket.id, payload.role, payload.assignment);
socket.emit('session:sync', payload);
}
function syncAll() {
logger.info('Broadcasting session sync to all sockets');
io.sockets.sockets.forEach((socket) => syncSocket(socket));
}
io.on('connection', (socket) => {
logger.info('New socket connected', socket.id);
syncSocket(socket);
});
roleEvents.on('change', ({ socket }) => {
if (!socket) return;
logger.info('Role changed; syncing session', socket.id);
syncSocket(socket);
});
assignmentService.assignmentEvents.on('update', (socketId) => {
const socket = io.sockets.sockets.get(socketId);
if (socket) {
logger.info('Assignment update; syncing session', socketId);
syncSocket(socket);
}
});
modeEvents.on('change', () => {
logger.info('Mode change detected; syncing all clients');
syncAll();
});
managerEvents.on('rover', () => {
logger.info('Rover roster change; syncing all clients');
syncAll();
});
managerEvents.on('lock', ({ roverId, locked }) => {
logger.info('Rover lock change', roverId, locked);
syncAll();
});
managerEvents.on('driver', ({ socketId }) => {
if (!socketId) return;
const socket = io.sockets.sockets.get(socketId);
if (socket) {
logger.info('Driver assignment change; syncing session', socketId);
syncSocket(socket);
}
});
turnEvents.on('activeDriver', () => {
logger.info('Active driver change; syncing all clients');
syncAll();
});
turnEvents.on('queue', (event = {}) => {
const { reason } = event;
if (reason === 'activity') {
const now = Date.now();
const elapsed = now - lastActivitySync;
if (elapsed >= ACTIVITY_SYNC_COOLDOWN_MS) {
lastActivitySync = now;
logger.info('Turn activity; syncing all clients (immediate)');
syncAll();
return;
}
if (!pendingActivitySync) {
const delay = ACTIVITY_SYNC_COOLDOWN_MS - elapsed;
pendingActivitySync = setTimeout(() => {
lastActivitySync = Date.now();
pendingActivitySync = null;
logger.info('Turn activity; syncing all clients (delayed)');
syncAll();
}, delay);
}
return;
}
if (pendingActivitySync) {
clearTimeout(pendingActivitySync);
pendingActivitySync = null;
}
logger.info('Turn queue change; syncing all clients');
syncAll();
});
roomCameraEvents.on('update', () => {
logger.info('Room camera change detected; syncing all clients');
syncAll();
});
homeAssistantEvents.on('update', () => {
logger.info('Home Assistant state change; syncing all clients');
syncAll();
});
homeAssistantEvents.on('status', () => {
logger.info('Home Assistant status change; syncing all clients');
syncAll();
});
replayEvents.on('update', () => {
logger.info('Replay cooldown updated; syncing all clients');
syncAll();
});
nicknameEvents.on('change', ({ socketId }) => {
const socket = socketId ? io.sockets.sockets.get(socketId) : null;
if (socket) {
logger.info('Nickname change; syncing session', socketId);
syncSocket(socket);
} else {
syncAll();
}
});
subscribe('communityGoal.updated', () => {
logger.info('Community goal updated; syncing all clients');
syncAll();
});
// sync all sockets 20 seconds
setInterval(() => {
logger.info('Periodic session sync for all clients');
syncAll();
}, 20000);
module.exports = {
buildSession,
syncSocket,
syncAll,
};