// lock.js — soft lock so the second PC gets warned instead of silently // overwriting the other PC's work. This is advisory, not a hard database lock: // sql.js rewrites the whole .db file on every save, so if both PCs really did // write at the same time, the last save wins. The heartbeat file makes that // situation visible before it happens instead of after. "use strict"; const fs = require("fs"); const path = require("path"); const os = require("os"); const HEARTBEAT_MS = 10_000; const STALE_AFTER_MS = 30_000; let lockPath = null; let heartbeatTimer = null; let myToken = null; function readLock(dataFolder) { const p = path.join(dataFolder, ".fellakte.lock"); if (!fs.existsSync(p)) return null; try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; } } /** * Checks whether another PC currently looks active. * Returns { active:boolean, info } — info is the other host's lock contents when active. */ function checkOther(dataFolder) { const info = readLock(dataFolder); if (!info) return { active: false, info: null }; const age = Date.now() - info.updatedAt; const isOther = info.host !== os.hostname() || info.pid !== process.pid; return { active: isOther && age < STALE_AFTER_MS, info }; } function acquire(dataFolder) { lockPath = path.join(dataFolder, ".fellakte.lock"); myToken = { host: os.hostname(), pid: process.pid, updatedAt: Date.now() }; fs.writeFileSync(lockPath, JSON.stringify(myToken)); heartbeatTimer = setInterval(() => { myToken.updatedAt = Date.now(); try { fs.writeFileSync(lockPath, JSON.stringify(myToken)); } catch { // shared drive briefly unreachable — next tick will retry } }, HEARTBEAT_MS); } function release() { if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer = null; if (lockPath && fs.existsSync(lockPath)) { try { const current = readLock(path.dirname(lockPath)); if (current && current.host === os.hostname() && current.pid === process.pid) { fs.unlinkSync(lockPath); } } catch { // best effort — a stale lock will simply age out after STALE_AFTER_MS } } } module.exports = { checkOther, acquire, release };