first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# `mutex-contract`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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>` 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<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)` — throws `LIFECYCLE.MUTEX_HELD` on `null`,
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.