/** * @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 (`ccam lock acquire`) owns the polling loop; CCAM's * server-side primitives stay non-orchestrating, same as every other lane * route in this project. * @author Nguyễn Ngọc Trí Vĩ */ const { Router } = require("express"); const namedLock = require("../lib/named-lock"); const { sameOriginGuard } = require("./run"); const router = Router(); router.get("/", (_req, res) => { res.json({ locks: namedLock.listLocks() }); }); 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" } }); } const result = namedLock.tryAcquire(req.params.name, holder); 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(req.params.name, holder); res.json({ ok: true }); } catch (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;