import { DatabaseSync } from 'node:sqlite' import { join } from 'node:path' /** * One `radiald run` per run state directory, enforced. * * Separate data directories are only a convention until something checks them, and two daemons on * one `stateDir` are two writers on `records.db`, `sync.db` and three ledgers — with no complaint * from either. This is the same cross-process advisory lock `FileSessionStore` already leans on: an * open `BEGIN IMMEDIATE` on a SQLite file, held for the process lifetime. It has the property a * pidfile does not — the OS releases it when the holder crashes, so a killed daemon does not leave a * stale lock its replacement has to be told to ignore. */ export class StateDirLock { readonly path: string #database: DatabaseSync | undefined private constructor(path: string, database: DatabaseSync) { this.path = path this.#database = database } /** Acquires the lock for `stateDir`, or throws the operator-facing "already in use" error. The * directory must already exist (`radiald run` creates it before locking). */ static acquire(stateDir: string): StateDirLock { const path = join(stateDir, 'run.lock.db') const database = new DatabaseSync(path) try { database.exec(` PRAGMA busy_timeout = 1000; CREATE TABLE IF NOT EXISTS run_lock (id INTEGER PRIMARY KEY) STRICT; BEGIN IMMEDIATE; `) } catch (error) { database.close() throw new Error( `${stateDir} is already in use by another radiald. Give this instance its own data ` + 'directory (--data-dir, or "dataDir" in radial.json), or stop the other daemon.' + (error instanceof Error && !/SQLITE_BUSY|database is locked/i.test(error.message) ? ` (${error.message})` : ''), ) } return new StateDirLock(path, database) } /** Releases it. Idempotent, so a `finally` may call it after an early failure. */ release(): void { const database = this.#database if (!database) return this.#database = undefined try { database.exec('COMMIT') } catch { // Best effort: closing the handle releases the lock either way, and shutdown must not hang. } finally { database.close() } } }