3.1 KiB
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<MutexRelease>andtryAcquire(plugin) → MutexRelease | null. - Per-plugin scope: locks for distinct plugin names are independent, so
billingandinventorycan 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 theLIFECYCLE.MUTEX_HELDexception over the null-check pattern.
Usage: Basic
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):
import type { MutexRelease, PluginMutex } from "@suaveplan/api-lifecycle/mutex-contract";
class RedisPluginMutex implements PluginMutex {
public async acquire(plugin: string): Promise<MutexRelease> {
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)— throwsLIFECYCLE.MUTEX_HELDonnull, returns the handle otherwise.
Implementation Notes
The reference implementation tracks holders via a Map<string, "held"> 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.