mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
queue fixes and admin force improvements
This commit is contained in:
@@ -51,7 +51,16 @@ function createRoverLifecycle(deps) {
|
|||||||
}
|
}
|
||||||
socketToRovers.get(socket.id).add(roverId);
|
socketToRovers.get(socket.id).add(roverId);
|
||||||
socket.join(record.room);
|
socket.join(record.room);
|
||||||
turnService.driverAdded(roverId, socket.id, (force && isAdmin(socket)) || forceTurn);
|
const adminForceControl = force && isAdmin(socket);
|
||||||
|
turnService.driverAdded(roverId, socket.id, {
|
||||||
|
/*
|
||||||
|
`forceTurn` is used by private-rover approval to grant a user the next
|
||||||
|
active slot, but it should not freeze the public turn queue. Only an
|
||||||
|
explicit admin force-control request is a takeover that pauses rotation.
|
||||||
|
*/
|
||||||
|
force: adminForceControl || forceTurn,
|
||||||
|
pauseQueue: adminForceControl,
|
||||||
|
});
|
||||||
socket.emit('controlGranted', { roverId });
|
socket.emit('controlGranted', { roverId });
|
||||||
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' });
|
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' });
|
||||||
sendAlert({
|
sendAlert({
|
||||||
|
|||||||
@@ -26,11 +26,118 @@ const {
|
|||||||
} = require('./state');
|
} = require('./state');
|
||||||
const { stopRover, removeDriverCompletely } = require('./actions');
|
const { stopRover, removeDriverCompletely } = require('./actions');
|
||||||
|
|
||||||
function driverAdded(roverId, socketId, force) {
|
function normalizeDriverAddOptions(options = {}) {
|
||||||
|
if (typeof options === 'boolean') {
|
||||||
|
return { force: options, pauseQueue: false };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
force: Boolean(options?.force),
|
||||||
|
pauseQueue: Boolean(options?.pauseQueue),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTurnTimer(roverId, queue) {
|
||||||
|
/*
|
||||||
|
Timer cleanup is centralized because normal turn end, non-turn modes, admin
|
||||||
|
pauses, and empty queues all need the same invariant: no stale timeout should
|
||||||
|
be left behind that can rotate the queue later.
|
||||||
|
*/
|
||||||
|
if (queue) clearTimeout(queue.timer);
|
||||||
|
turnDeadlines.delete(roverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearIdleTimer(roverId) {
|
||||||
|
/*
|
||||||
|
The idle timer is separate from the turn timer because idle skips are about
|
||||||
|
user activity inside a turn. Admin takeover pauses both systems, while normal
|
||||||
|
activity only disarms the idle timer for the current turn.
|
||||||
|
*/
|
||||||
|
clearTimeout(idleTimers.get(roverId));
|
||||||
|
idleDeadlines.delete(roverId);
|
||||||
|
idleTimers.delete(roverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQueueTimers(roverId, queue) {
|
||||||
|
clearTurnTimer(roverId, queue);
|
||||||
|
clearIdleTimer(roverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQueuePaused(queue) {
|
||||||
|
return Boolean(
|
||||||
|
queue &&
|
||||||
|
queue.pausedByAdminSocketId &&
|
||||||
|
queue.current === queue.pausedByAdminSocketId &&
|
||||||
|
queue.queue.includes(queue.pausedByAdminSocketId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInvalidAdminPause(queue) {
|
||||||
|
/*
|
||||||
|
A pause only means something while that admin socket is still in the queue.
|
||||||
|
Disconnect cleanup and release paths remove sockets first, so this guard lets
|
||||||
|
the rest of the turn code safely resume without needing special stale-state
|
||||||
|
branches everywhere.
|
||||||
|
*/
|
||||||
|
if (!queue?.pausedByAdminSocketId) return;
|
||||||
|
if (queue.queue.includes(queue.pausedByAdminSocketId)) return;
|
||||||
|
queue.pausedByAdminSocketId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncPausedQueue(roverId, queue) {
|
||||||
|
/*
|
||||||
|
During admin takeover, the queue is deliberately frozen: the admin stays at
|
||||||
|
the front and active, existing users keep their order behind the admin, and
|
||||||
|
no turn or idle timeout is allowed to remove the admin from control.
|
||||||
|
*/
|
||||||
|
setActiveDriver(roverId, queue.pausedByAdminSocketId);
|
||||||
|
clearQueueTimers(roverId, queue);
|
||||||
|
idleDisarmed.delete(roverId);
|
||||||
|
turnEvents.emit('queue', { roverId, reason: 'admin-pause' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAdminQueuePause(roverId, socketId, queue) {
|
||||||
|
/*
|
||||||
|
Force-control should be a takeover, not a destructive queue reset. Moving the
|
||||||
|
admin to index 0 makes the visible queue match control ownership, while the
|
||||||
|
filter preserves every other driver's relative order for when the admin
|
||||||
|
releases the rover.
|
||||||
|
*/
|
||||||
|
queue.queue = [socketId, ...queue.queue.filter((id) => id !== socketId)];
|
||||||
|
queue.current = socketId;
|
||||||
|
queue.pausedByAdminSocketId = socketId;
|
||||||
|
syncPausedQueue(roverId, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function driverAdded(roverId, socketId, options = {}) {
|
||||||
|
const { force, pauseQueue } = normalizeDriverAddOptions(options);
|
||||||
const queue = ensureQueue(roverId);
|
const queue = ensureQueue(roverId);
|
||||||
if (!queue.queue.includes(socketId)) {
|
const alreadyQueued = queue.queue.includes(socketId);
|
||||||
|
|
||||||
|
if (pauseQueue) {
|
||||||
|
applyAdminQueuePause(roverId, socketId, queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyQueued) {
|
||||||
queue.queue.push(socketId);
|
queue.queue.push(socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Re-requesting the same rover must not be treated as a fresh queue mutation.
|
||||||
|
The old behavior always called syncState(), which always rebuilt the turn
|
||||||
|
timeout in multi-driver turns mode and let the current driver extend a turn
|
||||||
|
forever by clicking their rover again.
|
||||||
|
*/
|
||||||
|
if (alreadyQueued && queue.current && !force) {
|
||||||
|
clearInvalidAdminPause(queue);
|
||||||
|
if (isQueuePaused(queue)) {
|
||||||
|
syncPausedQueue(roverId, queue);
|
||||||
|
} else {
|
||||||
|
turnEvents.emit('queue', { roverId, reason: 'duplicate-driver-request' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!queue.current || force) {
|
if (!queue.current || force) {
|
||||||
queue.current = socketId;
|
queue.current = socketId;
|
||||||
}
|
}
|
||||||
@@ -41,6 +148,16 @@ function driverRemoved(roverId, socketId) {
|
|||||||
const queue = driverQueues.get(roverId);
|
const queue = driverQueues.get(roverId);
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
queue.queue = queue.queue.filter((id) => id !== socketId);
|
queue.queue = queue.queue.filter((id) => id !== socketId);
|
||||||
|
if (queue.pausedByAdminSocketId === socketId) {
|
||||||
|
/*
|
||||||
|
Releasing or disconnecting the pausing admin is the handoff point back to
|
||||||
|
normal turns. The users were never removed, so advanceTurn() can resume at
|
||||||
|
the first preserved user after the admin has been filtered out above.
|
||||||
|
*/
|
||||||
|
queue.pausedByAdminSocketId = null;
|
||||||
|
} else {
|
||||||
|
clearInvalidAdminPause(queue);
|
||||||
|
}
|
||||||
const skips = idleSkips.get(roverId);
|
const skips = idleSkips.get(roverId);
|
||||||
if (skips) {
|
if (skips) {
|
||||||
skips.delete(socketId);
|
skips.delete(socketId);
|
||||||
@@ -62,12 +179,10 @@ function cleanupRover(roverId) {
|
|||||||
if (queue) {
|
if (queue) {
|
||||||
clearTimeout(queue.timer);
|
clearTimeout(queue.timer);
|
||||||
}
|
}
|
||||||
clearTimeout(idleTimers.get(roverId));
|
clearIdleTimer(roverId);
|
||||||
driverQueues.delete(roverId);
|
driverQueues.delete(roverId);
|
||||||
activeDrivers.delete(roverId);
|
activeDrivers.delete(roverId);
|
||||||
turnDeadlines.delete(roverId);
|
turnDeadlines.delete(roverId);
|
||||||
idleDeadlines.delete(roverId);
|
|
||||||
idleTimers.delete(roverId);
|
|
||||||
idleSkips.delete(roverId);
|
idleSkips.delete(roverId);
|
||||||
idleDisarmed.delete(roverId);
|
idleDisarmed.delete(roverId);
|
||||||
}
|
}
|
||||||
@@ -91,13 +206,15 @@ function isQueuedDriver(roverId, socketId) {
|
|||||||
function syncState(roverId) {
|
function syncState(roverId) {
|
||||||
const mode = getMode();
|
const mode = getMode();
|
||||||
const queue = ensureQueue(roverId);
|
const queue = ensureQueue(roverId);
|
||||||
|
clearInvalidAdminPause(queue);
|
||||||
|
if (isQueuePaused(queue)) {
|
||||||
|
syncPausedQueue(roverId, queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mode !== MODES.TURNS || queue.queue.length <= 1) {
|
if (mode !== MODES.TURNS || queue.queue.length <= 1) {
|
||||||
queue.current = queue.queue[0] || null;
|
queue.current = queue.queue[0] || null;
|
||||||
setActiveDriver(roverId, queue.current);
|
setActiveDriver(roverId, queue.current);
|
||||||
clearTimeout(queue.timer);
|
clearQueueTimers(roverId, queue);
|
||||||
turnDeadlines.delete(roverId);
|
|
||||||
clearTimeout(idleTimers.get(roverId));
|
|
||||||
idleDeadlines.delete(roverId);
|
|
||||||
idleDisarmed.delete(roverId);
|
idleDisarmed.delete(roverId);
|
||||||
turnEvents.emit('queue', { roverId });
|
turnEvents.emit('queue', { roverId });
|
||||||
return;
|
return;
|
||||||
@@ -115,6 +232,10 @@ function syncState(roverId) {
|
|||||||
function scheduleNextTurn(roverId) {
|
function scheduleNextTurn(roverId) {
|
||||||
const queue = driverQueues.get(roverId);
|
const queue = driverQueues.get(roverId);
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
|
if (isQueuePaused(queue)) {
|
||||||
|
syncPausedQueue(roverId, queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
clearTimeout(queue.timer);
|
clearTimeout(queue.timer);
|
||||||
const deadline = Date.now() + TURN_DURATION_MS;
|
const deadline = Date.now() + TURN_DURATION_MS;
|
||||||
turnDeadlines.set(roverId, deadline);
|
turnDeadlines.set(roverId, deadline);
|
||||||
@@ -124,10 +245,10 @@ function scheduleNextTurn(roverId) {
|
|||||||
|
|
||||||
function scheduleIdleTimer(roverId) {
|
function scheduleIdleTimer(roverId) {
|
||||||
const queue = driverQueues.get(roverId);
|
const queue = driverQueues.get(roverId);
|
||||||
clearTimeout(idleTimers.get(roverId));
|
clearIdleTimer(roverId);
|
||||||
idleDeadlines.delete(roverId);
|
|
||||||
if (
|
if (
|
||||||
!queue ||
|
!queue ||
|
||||||
|
isQueuePaused(queue) ||
|
||||||
getMode() !== MODES.TURNS ||
|
getMode() !== MODES.TURNS ||
|
||||||
queue.queue.length <= 1 ||
|
queue.queue.length <= 1 ||
|
||||||
!queue.current ||
|
!queue.current ||
|
||||||
@@ -188,14 +309,16 @@ function handleIdleTimeout(roverId, expectedDriver) {
|
|||||||
function advanceTurn(roverId) {
|
function advanceTurn(roverId) {
|
||||||
const queue = driverQueues.get(roverId);
|
const queue = driverQueues.get(roverId);
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
|
clearInvalidAdminPause(queue);
|
||||||
|
if (isQueuePaused(queue)) {
|
||||||
|
syncPausedQueue(roverId, queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (queue.queue.length === 0) {
|
if (queue.queue.length === 0) {
|
||||||
stopRover(roverId);
|
stopRover(roverId);
|
||||||
clearTimeout(queue.timer);
|
clearTurnTimer(roverId, queue);
|
||||||
setActiveDriver(roverId, null);
|
setActiveDriver(roverId, null);
|
||||||
turnDeadlines.delete(roverId);
|
clearIdleTimer(roverId);
|
||||||
idleDeadlines.delete(roverId);
|
|
||||||
clearTimeout(idleTimers.get(roverId));
|
|
||||||
idleTimers.delete(roverId);
|
|
||||||
idleDisarmed.delete(roverId);
|
idleDisarmed.delete(roverId);
|
||||||
turnEvents.emit('queue', { roverId });
|
turnEvents.emit('queue', { roverId });
|
||||||
return;
|
return;
|
||||||
@@ -204,11 +327,7 @@ function advanceTurn(roverId) {
|
|||||||
if (mode !== MODES.TURNS || queue.queue.length <= 1) {
|
if (mode !== MODES.TURNS || queue.queue.length <= 1) {
|
||||||
queue.current = queue.queue[0] || null;
|
queue.current = queue.queue[0] || null;
|
||||||
setActiveDriver(roverId, queue.current);
|
setActiveDriver(roverId, queue.current);
|
||||||
clearTimeout(queue.timer);
|
clearQueueTimers(roverId, queue);
|
||||||
turnDeadlines.delete(roverId);
|
|
||||||
idleDeadlines.delete(roverId);
|
|
||||||
clearTimeout(idleTimers.get(roverId));
|
|
||||||
idleTimers.delete(roverId);
|
|
||||||
idleDisarmed.delete(roverId);
|
idleDisarmed.delete(roverId);
|
||||||
turnEvents.emit('queue', { roverId });
|
turnEvents.emit('queue', { roverId });
|
||||||
return;
|
return;
|
||||||
@@ -234,6 +353,7 @@ function getTurnQueues() {
|
|||||||
current: queue.current,
|
current: queue.current,
|
||||||
deadline: turnDeadlines.get(roverId) || null,
|
deadline: turnDeadlines.get(roverId) || null,
|
||||||
idleDeadline: idleDeadlines.get(roverId) || null,
|
idleDeadline: idleDeadlines.get(roverId) || null,
|
||||||
|
pausedByAdminSocketId: queue.pausedByAdminSocketId || null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ const idleDisarmed = new Map();
|
|||||||
|
|
||||||
function ensureQueue(roverId) {
|
function ensureQueue(roverId) {
|
||||||
if (!driverQueues.has(roverId)) {
|
if (!driverQueues.has(roverId)) {
|
||||||
driverQueues.set(roverId, { queue: [], current: null, timer: null });
|
driverQueues.set(roverId, {
|
||||||
|
queue: [],
|
||||||
|
current: null,
|
||||||
|
timer: null,
|
||||||
|
/*
|
||||||
|
Admin force-control is intentionally modeled as a queue pause instead of
|
||||||
|
a queue replacement. That keeps every regular driver's relative place
|
||||||
|
intact while making the admin takeover immune to normal turn rotation.
|
||||||
|
*/
|
||||||
|
pausedByAdminSocketId: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return driverQueues.get(roverId);
|
return driverQueues.get(roverId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user