mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
109 lines
3.1 KiB
JavaScript
109 lines
3.1 KiB
JavaScript
// Global Objective Service
|
|
// Purpose: Defines the global objective 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 fs = require('fs');
|
|
const io = require('../../globals/io');
|
|
const logger = require('../../globals/logger').child('globalObjectiveService');
|
|
const { isAdmin } = require('../roleService');
|
|
const { publishEvent } = require('../eventBus');
|
|
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
|
|
|
|
const DATA_DIR = resolveDataDir();
|
|
const STORE_PATH = resolveDataPath('global-objective.json');
|
|
const LEGACY_STORE_PATH = resolveDataPath('community-goal.json');
|
|
const MAX_GOAL_LENGTH = 240;
|
|
|
|
let cache = null;
|
|
|
|
function loadStore() {
|
|
if (cache) return cache;
|
|
try {
|
|
const raw = fs.readFileSync(STORE_PATH, 'utf8');
|
|
cache = JSON.parse(raw);
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') {
|
|
logger.warn('Failed to load global objective', err.message);
|
|
} else {
|
|
try {
|
|
const legacyRaw = fs.readFileSync(LEGACY_STORE_PATH, 'utf8');
|
|
cache = JSON.parse(legacyRaw);
|
|
} catch (legacyErr) {
|
|
if (legacyErr.code !== 'ENOENT') {
|
|
logger.warn('Failed to load legacy global objective', legacyErr.message);
|
|
}
|
|
}
|
|
}
|
|
if (!cache) cache = null;
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
function saveStore(next) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
fs.writeFileSync(STORE_PATH, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
cache = next;
|
|
}
|
|
|
|
function normalizeText(input) {
|
|
if (typeof input !== 'string') return '';
|
|
return input.replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function getGlobalObjective() {
|
|
return loadStore();
|
|
}
|
|
|
|
function setGlobalObjective(text, meta = {}) {
|
|
const clean = normalizeText(text);
|
|
if (!clean) {
|
|
throw new Error('Goal text required');
|
|
}
|
|
if (clean.length > MAX_GOAL_LENGTH) {
|
|
throw new Error(`Goal too long (max ${MAX_GOAL_LENGTH} chars)`);
|
|
}
|
|
const payload = {
|
|
text: clean,
|
|
updatedAt: Date.now(),
|
|
updatedBy: meta.by || null,
|
|
};
|
|
saveStore(payload);
|
|
publishEvent({ source: 'globalObjective', type: 'globalObjective.updated', payload });
|
|
return payload;
|
|
}
|
|
|
|
function clearGlobalObjective(meta = {}) {
|
|
const payload = {
|
|
text: null,
|
|
updatedAt: Date.now(),
|
|
updatedBy: meta.by || null,
|
|
};
|
|
saveStore(payload);
|
|
publishEvent({ source: 'globalObjective', type: 'globalObjective.updated', payload });
|
|
return payload;
|
|
}
|
|
|
|
io.on('connection', (socket) => {
|
|
socket.on('globalObjective:set', ({ text } = {}, cb = () => {}) => {
|
|
if (!isAdmin(socket)) {
|
|
cb({ error: 'Not authorized' });
|
|
return;
|
|
}
|
|
try {
|
|
const result =
|
|
text == null || String(text).trim() === ''
|
|
? clearGlobalObjective({ by: socket?.data?.user?.username || socket?.id })
|
|
: setGlobalObjective(text, { by: socket?.data?.user?.username || socket?.id });
|
|
cb({ success: true, goal: result });
|
|
} catch (err) {
|
|
cb({ error: err.message });
|
|
}
|
|
});
|
|
});
|
|
|
|
module.exports = {
|
|
getGlobalObjective,
|
|
setGlobalObjective,
|
|
clearGlobalObjective,
|
|
MAX_GOAL_LENGTH,
|
|
};
|