/** * Claim work pools (SPEC §13.5 "claim", §13.6 predicate, §13.9 rows): competitive * at-most-one-winner acquisition from a durable pool, OWNER-MEDIATED end to end. * * The pool's owning endpoint holds the pool's single AckExplicit pull consumer * (`pool__`, provisioner-pre-created, exact filter); workers hold NO JetStream grant * on the pool and acquire, renew, and settle work exclusively through the owner's reserved * `lease` and `commit` commands on the ordinary `ep` rails. This is the only shape that * satisfies both claim invariants at once: the delivery's ack token never leaves the party * allowed to use it, and the attempt binding is OWNER-RECORDED at assignment (a worker-carried * "sequence + attempt" proves nothing about delivery; an owner assignment does). * * The stored pool message is WORK IDENTITY AND INPUT ONLY, never the authoritative lease: * broker redelivery re-delivers the same stored bytes, so a token in the payload cannot fence, * and the consumer's ack_wait is the broker's redelivery-to-owner timer only. * * THE LEASE RECORD IS THE SINGLE PER-ITEM LINEARIZATION POINT. The `lease` record * (`lease...`, §13.7) holds the owner-recorded assignment for the item's * CURRENT attempt AND its settlement state (`leased` | `settled`). A redelivery-advance, a * commit, and an expiry all contend on this ONE key's revision, so lease currency and terminal * settlement share a fence rather than reading across KV + EPF: the winner of the lease's CAS to * `settled` decides the disposition, and the per-item terminal fact * `epf..wrk..` is DERIVED from the settled lease (published * create-only, idempotently, by the committing worker's owner or by reconciliation). This closes * the double-effect windows a cross-store read-then-write left open (a stale attempt committing * after reassignment; an already-settled item being leased again). * * Every accepted item carries an absolute `workExpiry` (from its AcceptanceFact, §13.8). The * lease deadline is CLAMPED to it (`min(now + ttl, workExpiry)`), so no valid lease outlives the * horizon, and commit additionally refuses at `now >= workExpiry`: the item is dead once it * passes, leased or not. An endpoint worker's process epoch is freshly resolved and re-checked * at commit (§13.8), so a superseded process cannot settle a lease its predecessor held. */ import type { KV } from "@nats-io/kv"; import { type JetStreamClient, type JetStreamManager } from "@nats-io/jetstream"; import { type NatsConnection } from "@nats-io/transport-node"; import { type EpCaller } from "./endpoint-subjects.js"; import { type AcceptanceFact } from "./endpoint-journal.js"; /** A pool item's coordinates: the pool plus the item's ACCEPTANCE IDENTITY (§13.2: the accepted * submission's caller triple + request id — the four trailing subject tokens). */ export interface WorkItemRef { endpoint: string; pool: string; acceptance: EpCaller & { id: string; }; } /** A trusted, space-bonded work-pool context: the KV + JS + JSM are all DERIVED from one * binding-layer connection and one space by the constructor (never injected independently), so * a caller can never check a space-A lease against space-B facts. Every seam takes this * context. */ export interface WorkPoolContext { kv: KV; js: JetStreamClient; jsm: JetStreamManager; space: string; } /** Bond the resources to one space by CONSTRUCTION: the JetStream client, the manager, and the * records KV (the space's own bucket) all derive from the ONE connection passed in — four * already-separated resources are not accepted, so the advertised bond is real, not asserted. * The returned context is FROZEN (no later swap) and BRANDED: every seam accepts only a * context this constructor built, so a hand-assembled structural look-alike (the cross-space * mixup the bond exists to prevent) is rejected at the consuming boundary. */ export declare function workPoolContext(nc: NatsConnection, space: string): Promise; /** The brand assertion, exported for sibling modules composing over this context (the §13.6 * virtual admission/occupancy seams): every seam that accepts a WorkPoolContext enforces the * constructed bond, none trusts a structural look-alike. */ export declare function assertWorkPoolContext(ctx: WorkPoolContext): void; /** The item's stored subject (`epw......`). */ export declare function workItemSubject(space: string, ref: WorkItemRef): string; /** The item's terminal-fact subject (`epf..wrk..`, §13.2). */ export declare function workTerminalSubject(space: string, ref: WorkItemRef): string; /** The CANONICAL acceptance→item-bytes derivation (§13.6): the ONE deterministic projection of a * pool-routed {@link AcceptanceFact} into the EPW stored bytes — work identity + input ONLY * (`v`/`id`/`fingerprint`/`sourceSeq`/`workExpiry`/`caller`/`request`; never a lease, token, or * decision metadata), RFC-8785 canonical JSON so two independent derivations are BYTE-IDENTICAL. * Every first enqueue AND every reconciliation re-enqueue MUST derive through this function: * {@link enqueueWorkItem}'s idempotency is same-subject AND same-bytes, so a canonicalizer and a * drain repairing its crash-before-enqueue must agree byte-for-byte or the repair fails loud as * a mixup. Refuses a non-pool route or a missing work horizon (those never enqueue). */ export declare function workItemBytesOf(acceptance: AcceptanceFact): Uint8Array; /** Enqueue a pool item (the canonicalizer's seam, §13.6): CREATE-ONLY per acceptance-identity * subject, so acceptance→enqueue spanning two streams stays idempotent — a duplicate or * reconciliation re-enqueue of the same item loses its CAS harmlessly. The bytes are the * acceptance-derived work identity + input ONLY (never a lease/token; {@link workItemBytesOf} * is the canonical derivation). A CAS loss is only a * benign duplicate if the stored bytes are BYTE-IDENTICAL to the ones offered (same * acceptance-derived work): a differing prior body under the same identity is a canonicalizer * mixup and fails loud, never silently executes the wrong input. */ export declare function enqueueWorkItem(ctx: WorkPoolContext, itemRef: WorkItemRef, itemBytes: Uint8Array): Promise<{ enqueued: boolean; seq?: number; }>; /** The worker identity the OWNER binds at assignment: the broker-authenticated caller of the * reserved `lease` command (§13.5), DISCRIMINATED by kind. An `endpoint` worker (an endpoint * instance draining the pool) MUST carry its fenced process `epoch`, freshly re-checked at * commit; an `agent` worker has no epoch (its lifecycle UID is the whole currency). The kind is * structural so a missing epoch can never silently disable the fence. */ export type WorkWorker = { kind: "agent"; owner: string; actor: string; lifecycleUid: string; } | { kind: "endpoint"; owner: string; actor: string; lifecycleUid: string; epoch: number; }; /** The authoritative lease value at `lease....spec` (§13.7). The lease is * the item's per-item state machine: `leased` (a live assignment for the CURRENT attempt) or * `settled` (terminal — carries the disposition + committed outcome so the terminal fact is * derivable). */ export interface WorkLease { v: 1; state: "leased" | "settled"; /** The enqueued item's stream sequence — binds the lease to the exact stored execution. */ sourceSeq: number; /** The broker delivery count of the owner's fetch: the ONLY evidence of delivery. */ attempt: number; /** Present for `leased` and `settled:committed` (and kept when an existing lease is settled * expired/retired); ABSENT only for a NEVER-LEASED workerless settlement sentinel. */ worker?: WorkWorker; /** CAS-incremented once per attempt — the §13.8 monotonic fencing token. */ fencingToken: number; /** From the OWNER's own clock, CLAMPED to `workExpiry`; expiry revokes the claim. */ leaseDeadline: number; /** The item's absolute work horizon (§13.8), persisted so commit fences on it too. */ workExpiry: number; /** Present iff `state === "settled"`: how it settled and (for a commit) the cached outcome. */ disposition?: "committed" | "expired" | "retired"; outcome?: unknown; opId?: string; targetUid?: string; committedTs?: number; } /** Issue (or idempotently re-issue) the item's lease for the owner's CURRENT delivery — the * reserved `lease` command's handler seam, driven ONLY by the pool-owning endpoint after it * fetched the item off its own durable (§13.5). * * First-wins idempotent CAS per (item, attempt): * - no record → create `leased {attempt, worker, fencingToken: 1, leaseDeadline, workExpiry}`; * - the recorded attempt EQUALS this delivery → the SAME lease returns unchanged (no * reassignment within an attempt; the commit gate binds to the RECORDED worker); * - the recorded attempt is OLDER → redelivery advanced: revision-pinned update to the new * attempt with `fencingToken + 1`; * - the recorded attempt is NEWER → the caller's delivery is stale (`expired`). * Refusals before touching state: EXPIRED work (`now >= workExpiry`, settled by reconciliation, * never leased) and a SETTLED lease (`state === "settled"` — a committed item can never be * leased again, fenced on the SAME key, no cross-store read). `leaseDeadline` is CLAMPED to * `workExpiry` so no valid lease outlives the horizon. A DEL marker on the lease refuses. */ export declare function leaseWorkItem(ctx: WorkPoolContext, args: { ref: WorkItemRef; sourceSeq: number; attempt: number; worker: WorkWorker; /** The pool OWNER's own clock (never the worker's). */ now: number; leaseTtlMs: number; /** The item's absolute work expiry, read from its AcceptanceFact (§13.8). */ workExpiry: number; }): Promise; /** A pool item's cached terminal fact (`epf..wrk..`), DERIVED from the * settled lease — except `retired`, which the §13.1 retirement barrier's exact-pool cleaner * writes for an item whose acceptance targets the retiring lifecycle (never from a lease; the * owner's credentials are already revoked). Create-only CAS per item; the first terminal wins * forever. */ export type WorkTerminalFact = { v: 1; disposition: "committed"; pool: string; caller: EpCaller & { id: string; }; sourceSeq: number; attempt: number; fencingToken: number; worker: WorkWorker; outcome: unknown; ts: number; } | { v: 1; disposition: "expired"; pool: string; caller: EpCaller & { id: string; }; workExpiry: number; ts: number; } | { v: 1; disposition: "retired"; pool: string; caller: EpCaller & { id: string; }; /** The retirement operation that settled the item and the retiring target it re-bound the * item to through its acceptance decision (§13.9 cleaner row: the `epw` subject carries * no target, so the binding is recorded here). */ opId: string; targetUid: string; ts: number; }; /** Validate a terminal fact fully AND bind it to the subject it was read from (§13.4): a garbled * or mis-subjected fact never counts as authoritative settlement (which would suppress all * future leasing). Exported as the shared codec: the retirement cleaner (§13.1), which holds no * pool-owner context, validates the winners it reads through this same seam. */ export declare function parseWorkTerminalFact(raw: unknown, subject: string, ref: WorkItemRef): WorkTerminalFact; /** Read the item's cached terminal state (leader-served last-by-subject: the CAS-loser read * needs read-your-writes, §13.4). `undefined` when the item has no terminal yet. */ export declare function readWorkTerminal(ctx: WorkPoolContext, itemRef: WorkItemRef): Promise; /** Settle a claimed item — the reserved `commit` command's handler seam, driven ONLY by the * pool-owning endpoint on behalf of the broker-authenticated commit caller (§13.5). Gate order * against the OWNER-RECORDED lease and the owner's clock: execution binding (sourceSeq) → * token currency (attempt + fencingToken) → the caller IS the lease's bound worker → FRESH * epoch currency for an endpoint worker → `now < workExpiry` → `now < leaseDeadline`. Then the * FENCE: a revision-pinned CAS advances the lease `leased → settled{committed, outcome}` — the * SAME key a redelivery-advance contends on, so a stale attempt cannot slip a commit in after * reassignment. Only after winning that CAS is the terminal fact (derived from the settled * lease) published. A lost lease CAS means the lease advanced or settled concurrently: a * same-tuple settle is a DUPLICATE (return the cached terminal, which DOMINATES lease-expiry — * a true duplicate always sees its cached outcome, §13.5); anything else is `expired`/`conflict`. * * `resolveCurrentEpoch` freshly resolves an endpoint worker's CURRENT process epoch from trusted * authority (the lifecycle mapping) — a required seam for endpoint workers, absent for agents. */ export declare function commitWorkItem(ctx: WorkPoolContext, args: { ref: WorkItemRef; /** The broker-authenticated caller of the `commit` command — never a payload claim. */ caller: WorkWorker; /** The exact lease tuple the worker carries back (§13.5). */ lease: { sourceSeq: number; attempt: number; fencingToken: number; }; outcome: unknown; /** The pool OWNER's own clock. */ now: number; /** REQUIRED for an endpoint worker: fresh current-epoch resolver (null = retired/unknown * lifecycle). Absent/ignored for an agent worker. */ resolveCurrentEpoch?: (worker: WorkWorker) => Promise | number | null; /** OWNER-CONTROLLED budget on the resolver await (default 5000ms): a stuck lifecycle * authority is a bounded `unavailable` refusal, never a hung commit. */ epochResolveBudgetMs?: number; }): Promise<{ won: boolean; fact: WorkTerminalFact; }>; /** The §13.6 reconciliation verdict for one accepted, pool-routed item. */ export type WorkReconcileVerdict = { state: "settled"; fact: WorkTerminalFact; } | { state: "expired-settled"; fact: WorkTerminalFact; } | { state: "live"; } | { state: "re-enqueued"; seq: number; }; /** Decide and repair one item against the §13.6 predicate — the canonicalizer's reconciliation * seam (§13.9 row), for an ACCEPTED pool-routed item. The LEASE is consulted as the settlement * arbiter so reconciliation never contradicts a committing worker: * 1. a terminal `wrk` fact exists → SETTLED (the owner acks without effect); * 2. the lease is `settled` (a commit fenced it, maybe crashed before publishing) → publish * the derived terminal (idempotent) → SETTLED — recovery, never re-enqueued; * 3. `now >= workExpiry` → the item is DEAD, leased or not: fence it by CAS-settling the lease * `expired` (racing a live commit on the SAME key; a lost CAS re-reads the winner), then * publish the derived terminal → EXPIRED-SETTLED; with no lease record, a worker-less * `settled:expired` lease is CAS-CREATED on the same key first, so a racing FIRST lease * contends there instead of assigning dead work behind the expiry; * 4. a live pool entry exists (subject-confined LEADER-SERVED STREAM.MSG.GET, §13.6:1797-1799 — * a fencing read whose stale follower miss would re-arm settled work) → LIVE; * 5. else re-check the terminal (a commit may have landed since step 1), then re-enqueue the * SAME acceptance-derived bytes create-only — the ONLY re-enqueueable state. * Fail-closed preconditions: a DEL/PURGE marker on the lease REFUSES before any classification * (reconciling over a deletion could recreate authoritative state), and whenever a lease record * exists the caller-supplied `workExpiry` must EQUAL the persisted horizon (§13.8) — a mis-wired * reconciliation never expires a live lease against a foreign horizon. */ export declare function reconcileWorkItem(ctx: WorkPoolContext, args: { ref: WorkItemRef; /** The acceptance-derived stored bytes (work identity + input only). */ itemBytes: Uint8Array; /** From the item's AcceptanceFact — an absolute horizon a re-enqueue never resets. */ workExpiry: number; now: number; }): Promise; /** Settle a still-live pool item as `retired` for the §13.1 exact-pool cleaner. Unlike ordinary * expiry reconciliation, retirement is target-bound and may settle unexpired work, but it still * uses the lease key as the single arbiter: a racing commit, lease advance, or cleaner settlement * all contend on this revision before any terminal fact is published. */ export declare function retireWorkItem(ctx: WorkPoolContext, args: { ref: WorkItemRef; workExpiry: number; opId: string; targetUid: string; now: number; }): Promise<{ won: boolean; fact: WorkTerminalFact; }>; //# sourceMappingURL=endpoint-work.d.ts.map