mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 03:02:17 -05:00
more queue related convergence for the future.
This commit is contained in:
@@ -10,7 +10,8 @@ const { loadConfig, getFeatureFlags, registerConfigurationHandler } = require('.
|
||||
const { getConfiguredSocials } = require('../sessionService/configuration');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const roverManager = require('../roverManager');
|
||||
const { getTurnQueues } = require('../turnService');
|
||||
const { getTurnQueues, getActiveDrivers } = require('../turnService');
|
||||
const { buildRoverTurn } = require('../turnService/display');
|
||||
const { getRoomCameras, getRoomCameraState } = require('../roomCameraService');
|
||||
const { getRoverSnapshotState } = require('../roverSnapshotService');
|
||||
const { getRole } = require('../roleService');
|
||||
@@ -157,22 +158,6 @@ function publicRoverIdSet(roster = []) {
|
||||
return new Set(roster.map((rover) => String(rover?.id || '')).filter(Boolean));
|
||||
}
|
||||
|
||||
function filterPublicTurnQueues(turnQueues = {}, publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
const next = {};
|
||||
/*
|
||||
RoverQueuesPanel creates fallback rows for queue ids that are not present in
|
||||
the roster, so the public payload must filter queues with the exact same
|
||||
privacy boundary as the roster. Otherwise a closed-private rover can leak as
|
||||
an orphan queue row after a private access grant assigns someone to it.
|
||||
*/
|
||||
Object.entries(turnQueues || {}).forEach(([roverId, info]) => {
|
||||
if (!visible.has(String(roverId))) return;
|
||||
next[roverId] = info;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function filterPublicUsers(users = [], publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
/*
|
||||
@@ -203,6 +188,8 @@ function buildLocalInfo() {
|
||||
const publicIds = publicRoverIdSet(publicRoster);
|
||||
const roster = publicRoster.map((rover) => (lockdown ? rover : addRoverSnapshotLinks(rover)));
|
||||
const users = filterPublicUsers(Array.from(io.sockets.sockets.values()).map(buildUserEntry), publicIds);
|
||||
const turnQueues = getTurnQueues();
|
||||
const activeDrivers = getActiveDrivers();
|
||||
const roomCameras = lockdown || !features.roomCameras ? [] : getRoomCameras().map(buildRoomCameraInfo);
|
||||
return {
|
||||
instance: {
|
||||
@@ -212,8 +199,12 @@ function buildLocalInfo() {
|
||||
features,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
roster,
|
||||
turnQueues: filterPublicTurnQueues(getTurnQueues(), publicIds),
|
||||
// Only public roster entries reach the builder; queue metadata follows the
|
||||
// same visibility boundary instead of creating separate orphan queue rows.
|
||||
roster: roster.map((rover) => ({
|
||||
...rover,
|
||||
turn: buildRoverTurn({ rover, mode, socketId: null, turnInfo: turnQueues[rover.id], activeDriverId: activeDrivers[rover.id], users }),
|
||||
})),
|
||||
users,
|
||||
roomCameras,
|
||||
socials: features.socials ? getConfiguredSocials(config) : [],
|
||||
@@ -349,7 +340,6 @@ function normalizeRemotePayload(entry, payload) {
|
||||
features: instance.features && typeof instance.features === 'object' ? instance.features : {},
|
||||
},
|
||||
roster: Array.isArray(payload?.roster) ? payload.roster : [],
|
||||
turnQueues: payload?.turnQueues && typeof payload.turnQueues === 'object' ? payload.turnQueues : {},
|
||||
users: Array.isArray(payload?.users) ? payload.users : [],
|
||||
roomCameras: Array.isArray(payload?.roomCameras) ? payload.roomCameras : [],
|
||||
socials: Array.isArray(payload?.socials) ? payload.socials : [],
|
||||
@@ -419,7 +409,6 @@ function markOffline(entry, error) {
|
||||
publicUrl: previous.instance?.publicUrl || entry.url,
|
||||
},
|
||||
roster: previous.roster || [],
|
||||
turnQueues: previous.turnQueues || {},
|
||||
users: previous.users || [],
|
||||
roomCameras: previous.roomCameras || [],
|
||||
socials: previous.socials || [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// PTZ policy and session facade. Camera IO and participation each own their lifecycle.
|
||||
const { getNickname: getNicknameForQueue } = require('../nicknameService');
|
||||
const { describeQueue } = require('../turnService/queueDisplay');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
@@ -118,8 +119,10 @@ function describeParticipation(socket) {
|
||||
},
|
||||
viewMode: socket && canRequestLiveVideo(socket) ? 'live' : 'snapshot',
|
||||
turn: {
|
||||
...describeQueue(queue, currentId),
|
||||
userLabels: Object.fromEntries(queue.map((id) => [id, getSocketLabel(id)])),
|
||||
...describeQueue(queue, currentId, queue.map((id) => {
|
||||
const participant = io.sockets.sockets.get(id);
|
||||
return { socketId: id, nickname: getNicknameForQueue(participant), role: getRole(participant) };
|
||||
})),
|
||||
target: {
|
||||
id: PTZ_CAMERA_ID,
|
||||
name: cameraConfig.name || 'PTZ Camera',
|
||||
@@ -127,7 +130,7 @@ function describeParticipation(socket) {
|
||||
fallback: null,
|
||||
},
|
||||
enabled: entered && (isOperator || position > 0) && participation.state.queue.length > 0,
|
||||
isActive: isOperator,
|
||||
ownsControl: isOperator,
|
||||
turnsAhead: isOperator ? 0 : position || null,
|
||||
queueLength: participation.state.queue.length + (participation.state.operatorSocketId ? 1 : 0),
|
||||
deadline: participation.state.deadline,
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
const labels = {
|
||||
...require('../turnService/labels'),
|
||||
waiting: 'Someone else has control',
|
||||
handoff: 'It’s your turn!',
|
||||
active: 'You control the camera',
|
||||
idleWarning: 'Start driving or your turn will be skipped',
|
||||
activeTimer: 'Your turn',
|
||||
waitingTimer: 'Waiting',
|
||||
untilTurn: 'until your turn',
|
||||
timeRemaining: 'left',
|
||||
showTimer: 'Show turn timer',
|
||||
hideTimer: 'Hide turn timer',
|
||||
showName: 'Show camera name',
|
||||
hideName: 'Hide camera name',
|
||||
};
|
||||
|
||||
@@ -68,13 +68,13 @@ function getDefaultWebSources(assignment = {}, socket = null) {
|
||||
replay`, follows the camera currently controlled by that socket just as it
|
||||
follows an assigned rover below.
|
||||
|
||||
turn.isActive is deliberately stricter than PTZ access or queue membership:
|
||||
turn.ownsControl is deliberately stricter than PTZ access or queue membership:
|
||||
spectators and users waiting for a camera turn must not silently replay a
|
||||
camera they are not currently operating. Keeping this rule here also makes
|
||||
every web replay entry point share the same default instead of teaching the
|
||||
chat-command adapter about PTZ-specific state.
|
||||
*/
|
||||
if (ptzCameraService.getPublicState(socket).turn.isActive) {
|
||||
if (ptzCameraService.getPublicState(socket).turn.ownsControl) {
|
||||
const source = ptzCameraService.getReplaySource();
|
||||
if (!source) return [];
|
||||
return [{ type: source.type, id: String(source.id), label: source.label || source.id }];
|
||||
|
||||
@@ -10,7 +10,7 @@ const { getMode, modeEvents } = require('../modeManager');
|
||||
const roverManager = require('../roverManager');
|
||||
const { managerEvents } = roverManager;
|
||||
const assignmentService = require('../assignmentService');
|
||||
const { getActiveDrivers, getTurnQueues, turnEvents } = require('../turnService');
|
||||
const { getActiveDrivers, getTurnQueues, canDrive, turnEvents } = require('../turnService');
|
||||
const { buildRoverTurn } = require('../turnService/display');
|
||||
const { getRoomCameras, roomCameraEvents } = require('../roomCameraService');
|
||||
const {
|
||||
@@ -197,6 +197,7 @@ function buildSession(socket) {
|
||||
features,
|
||||
roster: roster.map((rover) => ({
|
||||
...rover,
|
||||
permissions: { canControl: getRole(socket) !== 'spectator' && canDrive(rover.id, socket) },
|
||||
turn: buildRoverTurn({
|
||||
rover,
|
||||
mode: getMode(),
|
||||
@@ -212,8 +213,6 @@ function buildSession(socket) {
|
||||
roverId: assignmentRoverId,
|
||||
status: assignmentRoverId ? assignment.status : assignment.status === 'waiting' ? 'waiting' : null,
|
||||
},
|
||||
activeDrivers,
|
||||
turnQueues,
|
||||
roomCameras: getRoomCameras(),
|
||||
ptzCamera: getPtzCameraState(socket),
|
||||
homeAssistant: getHomeAssistantState(),
|
||||
|
||||
@@ -2,16 +2,9 @@ const { describeQueue } = require('./queueDisplay');
|
||||
const { TURN_DURATION_MS, IDLE_TIMEOUT_MS } = require('./constants');
|
||||
|
||||
const labels = {
|
||||
...require('./labels'),
|
||||
waiting: 'Someone else is driving',
|
||||
handoff: 'It’s your turn!',
|
||||
active: 'You’re driving',
|
||||
idleWarning: 'Start driving or your turn will be skipped',
|
||||
activeTimer: 'Your turn',
|
||||
waitingTimer: 'Waiting',
|
||||
untilTurn: 'until your turn',
|
||||
timeRemaining: 'left',
|
||||
showTimer: 'Show turn timer',
|
||||
hideTimer: 'Hide turn timer',
|
||||
showName: 'Show rover name',
|
||||
hideName: 'Hide rover name',
|
||||
};
|
||||
@@ -25,14 +18,14 @@ function buildRoverTurn({ rover, mode, socketId, turnInfo, activeDriverId, users
|
||||
const userIndex = socketId ? queue.indexOf(socketId) : -1;
|
||||
const enabled = mode === 'turns' && queue.length > 1 && currentIndex >= 0 && userIndex >= 0;
|
||||
const turnsAhead = enabled ? (userIndex - currentIndex + queue.length) % queue.length : null;
|
||||
const display = describeQueue(queue, currentDriverId, users);
|
||||
return {
|
||||
target: { id: rover.id, name: rover.name, color: rover.color, fallback: rover.id },
|
||||
...describeQueue(queue, currentDriverId),
|
||||
userLabels: Object.fromEntries(queue.map((id) => [id,
|
||||
users.find((user) => user.socketId === id)?.nickname || id,
|
||||
])),
|
||||
...display,
|
||||
// Null distinguishes a rover that has never had a queue from an empty queue.
|
||||
queue: turnInfo ? display.queue : null,
|
||||
enabled,
|
||||
isActive: turnsAhead === 0,
|
||||
ownsControl: Boolean(socketId && currentDriverId === socketId),
|
||||
turnsAhead,
|
||||
queueLength: queue.length,
|
||||
deadline: turnInfo?.deadline || null,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// turn Service
|
||||
// Purpose: Defines the turn Service module and the helpers/state used by this service unit.
|
||||
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
|
||||
const { describeQueue } = require('./queueDisplay');
|
||||
const io = require('../../globals/io');
|
||||
const { sendAlert } = require('../alertService');
|
||||
const { MODES, getMode, modeEvents } = require('../modeManager');
|
||||
@@ -432,7 +431,7 @@ function getTurnQueues() {
|
||||
driverQueues.forEach((queue, roverId) => {
|
||||
payload[roverId] = {
|
||||
mode,
|
||||
...describeQueue(Array.from(queue.queue), queue.current),
|
||||
queue: Array.from(queue.queue),
|
||||
current: queue.current,
|
||||
deadline: turnDeadlines.get(roverId) || null,
|
||||
idleDeadline: idleDeadlines.get(roverId) || null,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
handoff: 'It’s your turn!',
|
||||
idleWarning: 'Start driving or your turn will be skipped',
|
||||
activeTimer: 'Your turn',
|
||||
waitingTimer: 'Waiting',
|
||||
untilTurn: 'until your turn',
|
||||
timeRemaining: 'left',
|
||||
showTimer: 'Show turn timer',
|
||||
hideTimer: 'Hide turn timer',
|
||||
};
|
||||
@@ -1,10 +1,20 @@
|
||||
// Shared display ordering only; each service still owns its queue policy.
|
||||
function describeQueue(queue, currentId) {
|
||||
const currentIndex = currentId ? queue.indexOf(currentId) : -1;
|
||||
const nextId = queue.length > 1
|
||||
? queue[(currentIndex + 1) % queue.length]
|
||||
: null;
|
||||
return { queue, currentId, nextId };
|
||||
// Queue entries are complete display records. No browser-side user lookup or
|
||||
// naming fallback is needed, including when the queue comes from another server.
|
||||
function describeQueue(socketIds, currentId, users) {
|
||||
const byId = new Map(users.map((user) => [user.socketId, user]));
|
||||
const currentIndex = currentId ? socketIds.indexOf(currentId) : -1;
|
||||
return {
|
||||
queue: socketIds.map((socketId) => {
|
||||
const user = byId.get(socketId);
|
||||
return {
|
||||
socketId,
|
||||
name: user?.nickname || user?.name || socketId.slice(0, 6) || 'unknown',
|
||||
role: user?.role || null,
|
||||
};
|
||||
}),
|
||||
currentId,
|
||||
nextId: socketIds.length > 1 ? socketIds[(currentIndex + 1) % socketIds.length] : null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { describeQueue };
|
||||
|
||||
Reference in New Issue
Block a user