UNPKG

socraticode

Version:

SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.

91 lines • 3.91 kB
// SPDX-License-Identifier: AGPL-3.0-only // Copyright (C) 2026 Giancarlo Erra - Altaire Limited import { acquireIdentityLock, holdsIdentityLock, isProjectIdentityLocked, releaseIdentityLock } from "./lock.js"; import { logger } from "./logger.js"; // Held from the final inventory validation through deletion, so no writer can start // underneath a delete. Lock files are per host; another host sharing Qdrant is out of reach. const BARRIER_OPERATION = "prune"; /** Locks a writer would take for the identity; held so none can start. */ export const WRITER_OPERATIONS = ["index", "watch", "graph", "context"]; const heldBarriers = new Set(); export class ReclamationBarrierError extends Error { projectId; constructor(projectId, detail) { super(`Project ${projectId} is being reclaimed: ${detail}`); this.projectId = projectId; this.name = "ReclamationBarrierError"; } } /** Whether this process is reclaiming the identity right now. */ export function isReclamationBarrierHeld(projectId) { return heldBarriers.has(projectId); } /** Throws ReclamationBarrierError while the identity is being reclaimed here or on this host, and LockInspectionError when that cannot be told. */ export async function assertNoReclamationBarrier(projectId) { if (heldBarriers.has(projectId)) { throw new ReclamationBarrierError(projectId, "reclamation is in progress in this process"); } if (await isProjectIdentityLocked(projectId, BARRIER_OPERATION)) { throw new ReclamationBarrierError(projectId, "another process holds its reclamation barrier"); } } /** Hold a writer lock by identity for the duration of a write; false when another process holds it or it cannot be taken. */ export async function withWriterLock(projectId, operation, write) { if (holdsIdentityLock(projectId, operation)) return null; if (!(await acquireIdentityLock(projectId, operation))) return null; try { return await write(); } finally { await releaseIdentityLock(projectId, operation); } } /** Take the barrier for an identity, or null when a writer holds any lock it needs; partial acquisition is undone. */ export async function acquireReclamationBarrier(projectId) { if (heldBarriers.has(projectId)) return null; const operations = [BARRIER_OPERATION, ...WRITER_OPERATIONS]; // Flagged before any lock is taken, so a writer in this process is refused throughout the acquisition. heldBarriers.add(projectId); let compromised = false; const acquired = []; const undo = async () => { heldBarriers.delete(projectId); for (const operation of [...acquired].reverse()) { await releaseIdentityLock(projectId, operation); } }; for (const operation of operations) { let ok = false; try { // Non-reentrant: a lock this process already holds belongs to a live writer, never to the barrier. ok = await acquireIdentityLock(projectId, operation, (err) => { compromised = true; logger.warn("Reclamation barrier lock compromised", { projectId, operation, error: err.message }); }, projectId, false); } catch (err) { logger.warn("Reclamation barrier lock acquisition threw", { projectId, operation, error: err instanceof Error ? err.message : String(err), }); } if (!ok) { await undo(); return null; } acquired.push(operation); } return { isCompromised: () => compromised, release: undo, }; } /** Test seam: forget every barrier this process holds without touching lock files. */ export function resetReclamationBarriers() { heldBarriers.clear(); } //# sourceMappingURL=reclamation-barrier.js.map