const WebSocket = require('ws');
const chokidar = require('chokidar');
const fs = require('fs/promises');
const path = require('path');
const { existsSync } = require('fs');
/ --- Configuration ---
const WS_URL = 'ws://localhost:1777';
const SYNC_DIR = path.resolve(__dirname, './gamelib/mesh/');
const WS_PREFIX = 'mesh/';
/ --- State & Locks ---
let ws;
let isConnected = false;
/ A map to track files currently being written by the WebSocket.
/ Key: Absolute file path, Value: Expiration timestamp (ms)
const writeLocks = new Map();
/ How long to ignore local Chokidar events after the WS writes a file.
/ 1500ms is generally safe to absorb OS-level file buffering events.
const LOCK_DURATION_MS = 1500;
async function init() {
// Ensure the sync directory exists before watching
if (!existsSync(SYNC_DIR)) {
await fs.mkdir(SYNC_DIR, { recursive: true });
console.log(`[System] Created directory: ${SYNC_DIR}`);
}
startFileWatcher();
connectWebSocket();
}
function startFileWatcher() {
const watcher = chokidar.watch(SYNC_DIR, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
}
});
watcher.on('all', async (event, filePath) => {
if (!isConnected) return;
const lockExpiry = writeLocks.get(filePath);
if (lockExpiry) {
if (Date.now() < lockExpiry) {
console.log(`[Watcher] Ignored echo event for:
${path.basename(filePath)}`);
return;
} else {
writeLocks.delete(filePath); // Clean up expired lock
}
}
const relativePath = path.relative(SYNC_DIR, filePath).replace(/\\/g,
'/');
const dmzPath = `${WS_PREFIX}${relativePath}`;
try {
if (event === 'add' || event === 'change') {
const fileData = await fs.readFile(filePath, { encoding:
'base64' });
const payload = {
type: 'VFS_WRITE',
path: dmzPath,
data: fileData,
timestamp: Date.now()
};
ws.send(JSON.stringify(payload));
console.log(`[FS -> WS] Broadcasted ${event}: ${dmzPath}`);
}
else if (event === 'unlink') {
// Handle deletions gracefully
const payload = {
type: 'VFS_DELETE',
path: dmzPath,
timestamp: Date.now()
};
ws.send(JSON.stringify(payload));
console.log(`[FS -> WS] Broadcasted delete: ${dmzPath}`);
}
} catch (err) {
console.error(`[Watcher Error] Failed to process ${filePath}:`,
err.message);
}
});
}
function connectWebSocket() {
console.log(`[Network] Connecting to ${WS_URL}...`);
ws = new WebSocket(WS_URL);
ws.on('open', () => {
isConnected = true;
console.log('[Network] Connected to DMZ server.');
});
ws.on('message', async (message) => {
try {
const msg = JSON.parse(message);
if (msg.type === 'VFS_WRITE' && msg.path && msg.data) {
await handleIncomingWrite(msg);
} else if (msg.type === 'VFS_DELETE' && msg.path) {
await handleIncomingDelete(msg);
}
} catch (err) {
console.error('[Network Error] Failed to parse/handle message:',
err.message);
}
});
ws.on('close', () => {
isConnected = false;
console.log('[Network] Disconnected. Reconnecting in 3 seconds...');
setTimeout(connectWebSocket, 3000);
});
ws.on('error', (err) => {
console.error('[Network Error]', err.message);
ws.close();
});
}
async function handleIncomingWrite(msg) {
const relativeTarget = msg.path.replacenew RegExp(`^${WS_PREFIX}`), '');
const absoluteTarget = path.join(SYNC_DIR, relativeTarget);
if (!absoluteTarget.startsWith(SYNC_DIR)) {
console.warn(`[Security] Blocked out-of-bounds write attempt:
${msg.path}`);
return;
}
writeLocks.set(absoluteTarget, Date.now() + LOCK_DURATION_MS);
await fs.mkdir(path.dirname(absoluteTarget), { recursive: true });
const buffer = Buffer.from(msg.data, 'base64');
await fs.writeFile(absoluteTarget, buffer);
console.log(`[WS -> FS] Wrote incoming file: ${relativeTarget}`);
}
async function handleIncomingDelete(msg) {
const relativeTarget = msg.path.replace(new RegExp(`^${WS_PREFIX}`), '');
const absoluteTarget = path.join(SYNC_DIR, relativeTarget);
if (!absoluteTarget.startsWith(SYNC_DIR)) return;
// Set lock to prevent echoing the deletion back
writeLocks.set(absoluteTarget, Date.now() + LOCK_DURATION_MS);
try { if (existsSync(absoluteTarget)) { await fs.unlink(absoluteTarget);
console.log(`[WS -> FS] Deleted incoming file: ${relativeTarget}`); }}
catch (err) { console.error(`[FS Error] Could not delete ${relativeTarget}:
`, err.message); }
}
/ Start the daemon
init();