/** * Durable concurrency primitives — a counting semaphore and a mutex built on * top of the compare-and-swap state slots exposed by `ctx.state.*` and * `engine.state.*`. * * Workflows frequently need to serialize access to a shared resource: only one * workflow may charge a customer at a time, at most three may hit a * rate-limited API concurrently, and so on. The building block already exists — * {@link AtomicState} gives you a single CAS-backed state slot — but reusing it * correctly (with FIFO fairness and a lease that frees the lock if a holder * crashes) is fiddly. {@link DurableSemaphore} and {@link DurableMutex} package * that algorithm so you do not have to hand-roll it. * * The primitives store a single {@link LockRecord} in one CAS slot. Each * `acquire`/`release` is a CAS transaction over that record, so every mutation * is durable, replay-safe, and recovered automatically after a crash. The * primitive never reads the wall clock itself: callers pass a deterministic * `now` (captured durably, e.g. via a clock activity) so the lease arithmetic * replays identically. A holder whose lease has expired is reclaimed by the * next contender, which is what prevents a crashed holder from deadlocking the * lock forever. * * @module core/concurrency */ import { type AcquireAttempt, type LockRecord } from './concurrency-lock-record.ts'; export { initialLockRecord, reduceAcquire, reduceRelease, reduceRenew, } from './concurrency-lock-record.ts'; export type { AcquireAttempt, LockHolder, LockRecord } from './concurrency-lock-record.ts'; /** * Minimal CAS state-slot surface shared by the durable `ctx.state.*` handles * (whose methods are workflow operations) and the admin `engine.state.*` * handles (whose methods are promises). `RUpdate` is the result of `update` * and `RGet` the result of `get`; both are a `Promise` for {@link AtomicState} * and a workflow-operation generator for `ctx.state.*`. They are decoupled * because `get` may resolve to `T | undefined` while `update` resolves to `T`. * Both {@link AtomicState} and the durable `ctx.state.*` handles satisfy this * structurally, so the same primitive drives both flavours. * * @example * ```ts * import { AtomicState, type CasSlot, type LockRecord } from '@lostgradient/weft'; * import { MemoryStorage } from '@lostgradient/weft/storage/memory'; * * // AtomicState satisfies CasSlot structurally (its methods return promises). * const slot: CasSlot, Promise> = * new AtomicState(new MemoryStorage(), 'state:workflow-scope:default:lock'); * void slot; * ``` */ export interface CasSlot { get(): RGet; update(updater: (current: T | undefined) => T): RUpdate; } /** * Options for {@link DurableSemaphore} and {@link DurableMutex}. * * @example * ```ts * import { DurableSemaphore, type DurableSemaphoreOptions } from '@lostgradient/weft'; * * const options: DurableSemaphoreOptions = { permits: 3, leaseMs: 60_000 }; * const semaphore = new DurableSemaphore(options); * void semaphore; * ``` */ export interface DurableSemaphoreOptions { /** * Number of permits. At most this many holders may hold the lock at once. * Defaults to `1` (a mutex). */ permits?: number; /** * Default lease duration in milliseconds applied to an acquired permit when * an explicit `leaseMs` is not supplied to `tryAcquire`. A permit whose lease * expires may be reclaimed by another contender, which is what frees the lock * when a holder crashes without releasing. Defaults to `30_000`. */ leaseMs?: number; } /** * A durable counting semaphore: at most `permits` holders may hold the lock at * once. Built entirely on a single compare-and-swap state slot, so it works * inside workflows (via `ctx.state.*`) and from admin code (via * `engine.state.*`). * * The semaphore is intentionally non-blocking at the slot level: `tryAcquire` * performs one CAS transaction and reports whether the permit was granted. The * caller decides how to wait between attempts — inside a workflow you * `yield* ctx.sleep(...)` between retries so the wait is durable and * replay-safe. * * Fairness is FIFO: a contender enqueues itself and only acquires once it * reaches the head of the waiter queue and a permit is free. Each granted * permit carries a lease; an expired lease is reclaimed by the next contender, * so a crashed holder cannot deadlock the lock forever. * * @example * ```ts * import { DurableSemaphore } from '@lostgradient/weft'; * import { AtomicState } from '@lostgradient/weft'; * import { MemoryStorage } from '@lostgradient/weft/storage/memory'; * * const storage = new MemoryStorage(); * const slot = new AtomicState( * storage, * 'state:workflow-scope:default:rate-limit:lock', * { initial: { holders: [], waiters: [] } }, * ); * const semaphore = new DurableSemaphore({ permits: 3, leaseMs: 60_000 }); * const attempt = await semaphore.tryAcquire(slot, { holderId: 'worker-1', now: Date.now() }); * if (attempt.acquired) { * try { * // ...use the shared resource... * } finally { * await semaphore.release(slot, { holderId: 'worker-1', now: Date.now() }); * } * } * ``` */ export declare class DurableSemaphore { readonly permits: number; readonly leaseMs: number; constructor(options?: DurableSemaphoreOptions); /** * Attempt to acquire a permit with a single CAS transaction. Returns whether * the permit was granted and the caller's FIFO queue position when it was * not. The caller is registered in the waiter queue on a failed attempt so a * subsequent retry preserves FIFO order. * * `RUpdate` is the slot's `update` return type — a `Promise` for * {@link AtomicState} or a workflow-operation generator for `ctx.state.*`. */ tryAcquire(slot: CasSlot, options: { holderId: string; now: number; leaseMs?: number; }): AcquireWithSlot; /** * Release a held permit with a single CAS transaction. Idempotent: releasing * a permit the caller does not hold simply removes any stale waiter entry. */ release(slot: CasSlot, options: { holderId: string; now: number; }): RUpdate; /** * Extend the lease on a held permit. Long-running holders renew before their * lease expires so a contender does not reclaim a still-active permit. * Resolves/returns `false` when the caller is not currently a holder. */ renew(slot: CasSlot, options: { holderId: string; now: number; leaseMs?: number; }): RenewWithSlot; /** * Read the current record without mutating it. `RGet` is the slot's `get` * return type — a `Promise` for {@link AtomicState} * or a workflow-operation generator for `ctx.state.*`. */ inspect(slot: CasSlot): RGet; } /** * A durable mutual-exclusion lock: a {@link DurableSemaphore} with exactly one * permit, so at most one holder at a time. Acquire it with `tryAcquire` and * `yield* ctx.sleep(...)` between retries to wait durably for release. * * @example * ```ts * import { DurableMutex } from '@lostgradient/weft'; * import { workflow, type WorkflowContext, type LockRecord } from '@lostgradient/weft'; * * const transfer = workflow({ name: 'transfer' }).execute(async function* (ctx: WorkflowContext) { * const slot = ctx.state.workflow('account-42:lock', { * initial: { holders: [], waiters: [] }, * }); * const mutex = new DurableMutex({ leaseMs: 60_000 }); * const now = yield* ctx.run(() => Date.now()); * yield* mutex.tryAcquire(slot, { holderId: ctx.workflowId, now }); * try { * // ...critical section... * } finally { * const releaseNow = yield* ctx.run(() => Date.now()); * yield* mutex.release(slot, { holderId: ctx.workflowId, now: releaseNow }); * } * }); * void transfer; * ``` */ export declare class DurableMutex extends DurableSemaphore { constructor(options?: Omit); } /** * Result of {@link DurableSemaphore.tryAcquire}, mirroring the slot's flavour: * a `Promise` when the slot's `update` returns a promise (an * {@link AtomicState}), or a workflow-operation generator otherwise. * * @example * ```ts * import type { AcquireAttempt, AcquireWithSlot } from '@lostgradient/weft'; * * type PromiseResult = AcquireWithSlot>; // Promise * const result: PromiseResult = Promise.resolve({ acquired: true, position: -1 }); * void result; * ``` */ export type AcquireWithSlot = R extends Promise ? Promise : Generator; /** * Result of {@link DurableSemaphore.renew}, mirroring the slot's flavour: a * `Promise` for an {@link AtomicState} slot, or a workflow-operation * generator that yields to a `boolean` for a `ctx.state.*` slot. * * @example * ```ts * import type { RenewWithSlot } from '@lostgradient/weft'; * * type PromiseResult = RenewWithSlot>; // Promise * const result: PromiseResult = Promise.resolve(true); * void result; * ``` */ export type RenewWithSlot = R extends Promise ? Promise : Generator;