/** * @file HTTP surface for cross-lane named locks (`server/lib/named-lock.js`). * Every route is single-shot — this layer never blocks waiting for a lock to * free up. The CLI (`agenthub lock acquire`) owns the polling loop; AgentHub's * server-side primitives stay non-orchestrating, same as every other lane * route in this project. Lock names are one directory name each (named-lock.js * refuses a path), and in server mode a member's live in their own namespace. * @author Nguyễn Ngọc Trí Vĩ */ const { Router } = require("express"); const namedLock = require("../lib/named-lock"); const { sameOriginGuard } = require("./run"); const { memberId } = require("../lib/scope"); const router = Router(); // Server mode: a member's locks live in their own namespace (`u.`), // so one user's agents can neither see nor take another user's locks. const memberPrefix = (req) => { const uid = memberId(req); return uid == null ? "" : `u${uid}.`; }; const lockName = (req) => memberPrefix(req) + req.params.name; const badInput = (res, err) => res.status(400).json({ error: { code: err.code, message: err.message } }); router.get("/", (req, res) => { const prefix = memberPrefix(req); const locks = namedLock.listLocks(); res.json({ locks: prefix ? locks .filter((l) => l.name.startsWith(prefix)) .map((l) => ({ ...l, name: l.name.slice(prefix.length) })) : locks, }); }); router.post("/:name/acquire", sameOriginGuard, (req, res) => { const holder = typeof req.body?.holder === "string" ? req.body.holder.trim() : ""; if (!holder) { return res.status(400).json({ error: { code: "EBADHOLDER", message: "holder is required" } }); } let result; try { result = namedLock.tryAcquire(lockName(req), holder); } catch (err) { if (err.code === "EBADNAME" || err.code === "EBADHOLDER") return badInput(res, err); throw err; } if (result.acquired) return res.json(result); res.status(409).json(result); }); router.post("/:name/release", sameOriginGuard, (req, res) => { const holder = typeof req.body?.holder === "string" ? req.body.holder.trim() : ""; if (!holder) { return res.status(400).json({ error: { code: "EBADHOLDER", message: "holder is required" } }); } try { namedLock.release(lockName(req), holder); res.json({ ok: true }); } catch (err) { if (err.code === "EBADNAME" || err.code === "EBADHOLDER") return badInput(res, err); if (err.code === "ENOLOCK") { return res.status(404).json({ error: { code: err.code, message: err.message } }); } if (err.code === "ENOTHOLDER") { return res.status(409).json({ error: { code: err.code, message: err.message, currentHolder: err.currentHolder }, }); } res.status(500).json({ error: { code: err.code, message: err.message } }); } }); module.exports = router;