fix: race-proof writer-lock acquisition + machine-readable conflict through init

- acquireWriterLock now CLAIMS with an atomic create-exclusive write (O_EXCL)
  inside a bounded retry loop: two processes racing an absent lock can never
  both succeed (the old read-then-tmp-rename flow let the loser keep running
  unlocked, silently). An EEXIST loser re-evaluates and either throws loudly
  with the winner's details or performs a verified stale-takeover (re-read
  before unlink so a lock that changed hands mid-deliberation is never
  clobbered). Exhausted contention fails loudly instead of degrading into a
  lockless open.
- BRAINY_WRITER_LOCKED passes through init() unwrapped: the error documents a
  machine-readable contract (err.code + err.lockInfo with the holder's
  pid/host/heartbeat), but init's blanket error wrapping stripped both,
  leaving consumers a message to regex against.
- Two contract tests added: stale-foreign takeover installs OUR lock via the
  atomic claim; the conflict error carries code + lockInfo at the public
  init() surface.
This commit is contained in:
David Snelling 2026-07-17 17:11:46 -07:00
parent e450e0eedf
commit 01a3b46ade
4 changed files with 188 additions and 49 deletions

View file

@ -1373,6 +1373,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
if (this._readyReject) {
this._readyReject(error instanceof Error ? error : new Error(String(error)))
}
// Machine-readable init failures pass through UNWRAPPED — the writer-lock
// conflict documents an err.code/err.lockInfo contract ("callers detect
// this case via err.code"), and wrapping in a fresh Error silently
// stripped both, leaving consumers only a message to regex against.
if (error instanceof Error && (error as Error & { code?: string }).code === 'BRAINY_WRITER_LOCKED') {
throw error
}
throw new Error(`Failed to initialize Brainy: ${error}`)
}
}