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
JavaScript
// 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