mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
slopfixing queue stuffs
This commit is contained in:
@@ -112,6 +112,7 @@ function driverAdded(roverId, socketId, options = {}) {
|
||||
const { force, pauseQueue } = normalizeDriverAddOptions(options);
|
||||
const queue = ensureQueue(roverId);
|
||||
const alreadyQueued = queue.queue.includes(socketId);
|
||||
const previousQueueLength = queue.queue.length;
|
||||
|
||||
if (pauseQueue) {
|
||||
applyAdminQueuePause(roverId, socketId, queue);
|
||||
@@ -141,6 +142,30 @@ function driverAdded(roverId, socketId, options = {}) {
|
||||
if (!queue.current || force) {
|
||||
queue.current = socketId;
|
||||
}
|
||||
|
||||
/*
|
||||
A normal join changes who will receive a future turn; it does not begin a
|
||||
new turn for the person who is already driving. Preserve both deadlines and
|
||||
the current driver's activity state while an already-rotating queue grows.
|
||||
|
||||
The one-to-two transition is intentionally different. Before the second
|
||||
driver arrives there is no reason to time the sole driver, so this is the
|
||||
moment the first real rotating turn begins. A forced grant also changes the
|
||||
active driver immediately and therefore starts that driver's fresh turn.
|
||||
*/
|
||||
const turnsAreRotating = getMode() === MODES.TURNS && queue.queue.length > 1;
|
||||
const beginsRotatingTurns = turnsAreRotating && previousQueueLength <= 1;
|
||||
if (turnsAreRotating && (force || beginsRotatingTurns)) {
|
||||
startCurrentTurn(roverId, queue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnsAreRotating) {
|
||||
setActiveDriver(roverId, queue.current);
|
||||
turnEvents.emit('queue', { roverId, reason: 'driver-joined' });
|
||||
return;
|
||||
}
|
||||
|
||||
syncState(roverId);
|
||||
}
|
||||
|
||||
@@ -165,13 +190,32 @@ function driverRemoved(roverId, socketId) {
|
||||
idleSkips.delete(roverId);
|
||||
}
|
||||
}
|
||||
idleDisarmed.delete(roverId);
|
||||
if (queue.current === socketId) {
|
||||
/*
|
||||
Removing the active driver is a real handoff. advanceTurn() owns clearing
|
||||
the old turn state and creating the next driver's deadlines, keeping this
|
||||
path identical for explicit release, disconnect, and stale-socket reap.
|
||||
*/
|
||||
stopRover(roverId);
|
||||
advanceTurn(roverId);
|
||||
} else {
|
||||
scheduleIdleTimer(roverId);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
A waiting driver leaving must not re-arm idle detection or extend the
|
||||
unrelated active driver's turn. If this removal ends rotation entirely,
|
||||
syncState() clears both deadlines immediately; otherwise only the public
|
||||
queue membership changes.
|
||||
*/
|
||||
if (getMode() !== MODES.TURNS || queue.queue.length <= 1) {
|
||||
syncState(roverId);
|
||||
return;
|
||||
}
|
||||
if (isQueuePaused(queue)) {
|
||||
turnEvents.emit('queue', { roverId, reason: 'driver-left-admin-pause' });
|
||||
return;
|
||||
}
|
||||
turnEvents.emit('queue', { roverId, reason: 'driver-left' });
|
||||
}
|
||||
|
||||
function cleanupRover(roverId) {
|
||||
@@ -242,11 +286,24 @@ function syncState(roverId) {
|
||||
if (!queue.current) {
|
||||
queue.current = queue.queue[0];
|
||||
}
|
||||
/*
|
||||
syncState() is used when entering turns mode and when a previously untimed
|
||||
queue becomes rotatable. Both cases begin a genuine turn rather than merely
|
||||
publishing a membership update.
|
||||
*/
|
||||
startCurrentTurn(roverId, queue);
|
||||
}
|
||||
|
||||
function startCurrentTurn(roverId, queue) {
|
||||
/*
|
||||
This is the sole normal entry point for a fresh timed turn. Keeping active
|
||||
ownership, the full-turn deadline, and the activity grace window together
|
||||
prevents callers from resetting only part of the lifecycle.
|
||||
*/
|
||||
setActiveDriver(roverId, queue.current);
|
||||
idleDisarmed.set(roverId, false);
|
||||
scheduleNextTurn(roverId);
|
||||
scheduleIdleTimer(roverId);
|
||||
turnEvents.emit('queue', { roverId });
|
||||
}
|
||||
|
||||
function scheduleNextTurn(roverId) {
|
||||
@@ -423,7 +480,12 @@ function reapStaleDrivers() {
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(reapStaleDrivers, STALE_REAPER_MS);
|
||||
/*
|
||||
Stale cleanup should run for the lifetime of the server, but it should not by
|
||||
itself keep short-lived tools or the Node test runner alive after their work
|
||||
has completed.
|
||||
*/
|
||||
setInterval(reapStaleDrivers, STALE_REAPER_MS).unref();
|
||||
|
||||
module.exports = {
|
||||
driverAdded,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Turn Service Tests
|
||||
// Purpose: Protects timed-turn state from unrelated queue membership changes.
|
||||
// Scope: Exercises public turn operations and inspects only their published queue model; no HTTP server is started.
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const { MODES, setMode } = require('../modeManager');
|
||||
const turnService = require('./index');
|
||||
|
||||
function enterTurnsMode() {
|
||||
/*
|
||||
Mode authorization is irrelevant to this state-machine test. The forced
|
||||
transition uses the same production mode event, ensuring existing queues
|
||||
are reconciled exactly as they would be after an administrator mode change.
|
||||
*/
|
||||
setMode(MODES.TURNS, null, { force: true });
|
||||
}
|
||||
|
||||
function resetRover(roverId) {
|
||||
turnService.cleanupRover(roverId);
|
||||
}
|
||||
|
||||
test('joining an active rotation preserves the current turn and completed activity grace', () => {
|
||||
const roverId = 'turn-test-join';
|
||||
enterTurnsMode();
|
||||
turnService.driverAdded(roverId, 'driver-a');
|
||||
turnService.driverAdded(roverId, 'driver-b');
|
||||
|
||||
const started = turnService.getTurnQueues()[roverId];
|
||||
assert.ok(started.deadline);
|
||||
assert.ok(started.idleDeadline);
|
||||
|
||||
turnService.recordActivity(roverId, 'driver-a');
|
||||
const active = turnService.getTurnQueues()[roverId];
|
||||
assert.equal(active.idleDeadline, null);
|
||||
|
||||
turnService.driverAdded(roverId, 'driver-c');
|
||||
const joined = turnService.getTurnQueues()[roverId];
|
||||
assert.equal(joined.current, 'driver-a');
|
||||
assert.equal(joined.deadline, active.deadline);
|
||||
assert.equal(joined.idleDeadline, null);
|
||||
|
||||
resetRover(roverId);
|
||||
});
|
||||
|
||||
test('a waiting driver leaving preserves both active deadlines', () => {
|
||||
const roverId = 'turn-test-waiting-leave';
|
||||
enterTurnsMode();
|
||||
turnService.driverAdded(roverId, 'driver-a');
|
||||
turnService.driverAdded(roverId, 'driver-b');
|
||||
turnService.driverAdded(roverId, 'driver-c');
|
||||
const before = turnService.getTurnQueues()[roverId];
|
||||
|
||||
turnService.driverRemoved(roverId, 'driver-c');
|
||||
const after = turnService.getTurnQueues()[roverId];
|
||||
assert.equal(after.current, 'driver-a');
|
||||
assert.equal(after.deadline, before.deadline);
|
||||
assert.equal(after.idleDeadline, before.idleDeadline);
|
||||
|
||||
resetRover(roverId);
|
||||
});
|
||||
|
||||
test('dropping to one driver clears turn and idle deadlines immediately', () => {
|
||||
const roverId = 'turn-test-single-driver';
|
||||
enterTurnsMode();
|
||||
turnService.driverAdded(roverId, 'driver-a');
|
||||
turnService.driverAdded(roverId, 'driver-b');
|
||||
assert.ok(turnService.getTurnQueues()[roverId].deadline);
|
||||
|
||||
turnService.driverRemoved(roverId, 'driver-b');
|
||||
const single = turnService.getTurnQueues()[roverId];
|
||||
assert.equal(single.current, 'driver-a');
|
||||
assert.equal(single.deadline, null);
|
||||
assert.equal(single.idleDeadline, null);
|
||||
|
||||
resetRover(roverId);
|
||||
});
|
||||
|
||||
test('turn deadlines are absent outside turns mode during queue grants', () => {
|
||||
const roverId = 'turn-test-admin-mode';
|
||||
setMode(MODES.ADMIN, null, { force: true });
|
||||
turnService.driverAdded(roverId, 'driver-a');
|
||||
turnService.driverAdded(roverId, 'driver-b', { force: true });
|
||||
|
||||
const queue = turnService.getTurnQueues()[roverId];
|
||||
/*
|
||||
In non-turn modes syncState keeps the first regular queued driver current.
|
||||
Explicit administrator takeover uses the separate pauseQueue path; this
|
||||
assertion is concerned only with ensuring neither grant creates timers.
|
||||
*/
|
||||
assert.equal(queue.current, 'driver-a');
|
||||
assert.equal(queue.deadline, null);
|
||||
assert.equal(queue.idleDeadline, null);
|
||||
|
||||
resetRover(roverId);
|
||||
});
|
||||
Reference in New Issue
Block a user