Files
skills/docs/api-stack/api/api-lifecycle/src/mutex-contract/mutex-contract.md
2026-05-29 08:20:56 +01:00

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> 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

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) — 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.