This commit is contained in:
legop3
2026-09-27 18:41:24 -04:00
parent dbcb7f6a7a
commit 73a78eef20
11 changed files with 1024 additions and 2 deletions
+7 -2
View File
@@ -11,6 +11,7 @@ const logger = require('../../globals/logger').child('commandService');
const { isHeadlightBlocked } = require('../../rewards/definitions/darkness');
const homeAssistantService = require('../homeAssistantService');
const overcurrentProtectionService = require('../overcurrentProtectionService');
const { subscribeCommandRecording, observeCommand, recordCommandRequest } = require('./recording');
// Command observations are intentionally separate from the global event bus.
// Drive and motor commands can run at control-loop frequency, and publishing
@@ -96,6 +97,7 @@ function issueCommand(roverId, payload) {
const normalizedPayload = normalizeOutboundCommandPayload(payload);
const message = { ...normalizedPayload, id };
record.ws.send(JSON.stringify(message));
observeCommand({ phase: 'dispatched', roverId, commandId: id, command: normalizedPayload });
pendingCommands.set(id, { roverId, ts: Date.now(), type: normalizedPayload.type });
logger.info('Issued command', roverId, normalizedPayload.type, id);
return id;
@@ -130,6 +132,7 @@ function handleAck(msg) {
const pending = pendingCommands.get(msg.id);
if (!pending) return;
pendingCommands.delete(msg.id);
observeCommand({ phase: 'acknowledged', roverId: pending.roverId, commandId: msg.id, acknowledgement: msg });
logger.info('Command acknowledged', pending.roverId, pending.type, msg.status);
io.emit('commandAck', {
roverId: pending.roverId,
@@ -255,6 +258,7 @@ module.exports = {
getRecentDriveActivity,
setDriveCooldown,
commandEvents,
subscribeCommandRecording,
};
io.on('connection', (socket) => {
@@ -392,8 +396,9 @@ io.on('connection', (socket) => {
}
}
socket.on('command', handleCommand);
socket.on('command:issue', handleCommand);
const recordedCommand = (request, reply) => recordCommandRequest(socket, request, reply, handleCommand);
socket.on('command', recordedCommand);
socket.on('command:issue', recordedCommand);
socket.on('command:updateAllRovers', (_payload = {}, cb) => {
const reply = typeof cb === 'function' ? cb : () => {};
@@ -0,0 +1,55 @@
// Optional detailed command observation, separate from payload-free analytics.
// Subscribers must only enqueue work; recorder failures cannot fail a command.
const { AsyncLocalStorage } = require('node:async_hooks');
const { randomUUID } = require('node:crypto');
const observers = new Set();
const requests = new AsyncLocalStorage();
function subscribeCommandRecording(observer) {
observers.add(observer);
return () => observers.delete(observer);
}
function observeCommand(event) {
if (!observers.size) return;
const scope = requests.getStore();
const context = scope?.active ? scope.context : null;
for (const observer of observers) {
try {
// Snapshot before safety code or a caller mutates a nested payload. Each
// subscriber receives its own copy and cannot change command delivery.
observer(structuredClone({
ts: Date.now(),
monotonicNs: process.hrtime.bigint().toString(),
source: context ? 'client' : 'server',
...context,
...event,
}));
} catch {
// Recording is deliberately best-effort at this boundary. The owning
// recorder reports its storage health independently of driver replies.
}
}
}
function recordCommandRequest(socket, request, reply, execute) {
if (!observers.size) return execute(request, reply);
const context = { requestId: randomUUID(), socketId: socket.id, userId: socket.data?.userId || null };
const scope = { active: true, context };
return requests.run(scope, () => {
try {
observeCommand({ phase: 'requested', roverId: request?.roverId, command: request });
return execute(request, (result) => {
observeCommand({ phase: 'result', roverId: request?.roverId, result });
if (typeof reply === 'function') reply(result);
});
} finally {
// The command handler is synchronous. A safety timer created inside it
// inherits async context but its later commands are server actions.
scope.active = false;
}
});
}
module.exports = { subscribeCommandRecording, observeCommand, recordCommandRequest };