# `mutex-contract` _(no submodules)_ ## Purpose Defines `PluginMutex` — the contract guaranteeing at most one lifecycle transition per plugin in flight at any time — together with a reference in-memory implementation (`InMemoryPluginMutex`) that the FSM factory uses by default and that the conformance harness exercises end-to-end. Concurrent `acquire(plugin)` calls queue FIFO and resolve in arrival order, matching the semantics required by design §4's "per-plugin mutex" paragraph. ## Features - Two-method interface: `acquire(plugin) → Promise` and `tryAcquire(plugin) → MutexRelease | null`. - Per-plugin scope: locks for distinct plugin names are independent, so `billing` and `inventory` can transition concurrently. - Idempotent `release()` — re-calling is safe. - FIFO queueing semantics so operators see deterministic transition order rather than scheduler-dependent races. - `assertAcquired(plugin, handle)` helper for callers that prefer the `LIFECYCLE.MUTEX_HELD` exception over the null-check pattern. ## Usage: Basic ```ts import { InMemoryPluginMutex } from "@suaveplan/api-lifecycle/mutex-contract"; const mutex = new InMemoryPluginMutex(); const handle = await mutex.acquire("billing"); try { // … perform the transition … } finally { handle.release(); } ``` ## Usage: Advanced Pluggable mutex backed by a distributed lock (sketch — the production driver in `api-host` will supply a real implementation): ```ts import type { MutexRelease, PluginMutex } from "@suaveplan/api-lifecycle/mutex-contract"; class RedisPluginMutex implements PluginMutex { public async acquire(plugin: string): Promise { const lock = await this.client.lock(`plugin:${plugin}`, { ttlMs: 60_000 }); return { release: () => lock.unlock() }; } public tryAcquire(plugin: string): MutexRelease | null { const lock = this.client.tryLock(`plugin:${plugin}`, { ttlMs: 60_000 }); return lock === null ? null : { release: () => lock.unlock() }; } private readonly client: { lock: (k: string, o: { ttlMs: number }) => Promise<{ unlock(): void }>; tryLock: (k: string, o: { ttlMs: number }) => { unlock(): void } | null }; } ``` ## API Reference - `interface PluginMutex` — `acquire`, `tryAcquire`. - `interface MutexRelease` — `release`. - `class InMemoryPluginMutex` — reference impl; concrete drop-in for the default factory and for conformance tests. - `assertAcquired(plugin, handle)` — throws `LIFECYCLE.MUTEX_HELD` on `null`, returns the handle otherwise. ## Implementation Notes The reference implementation tracks holders via a `Map` and maintains an array of waiter resolvers per plugin name. On release, if a waiter is queued, the new handle is created against the same plugin name so the next acquirer sees a fresh `released = false` flag — without that, a stale handle's `release()` could re-fire and drain another waiter. The `released` flag is closed-over rather than stored on the handle so the public shape of `MutexRelease` stays minimal.