import type { NatsConnection } from "@nats-io/transport-node"; import { type EpCaller, type EpRoute } from "./endpoint-subjects.js"; import { EpEnvelopeError, type EndpointReply, type EndpointEvent, type EpCorrelation, type EpBindBlock } from "./endpoint-envelope.js"; import type { JetStreamManager } from "@nats-io/jetstream"; import type { CompiledContract } from "./schema-profile.js"; import { type FrozenInstance } from "./endpoint-service.js"; /** A verb's target: one input shape that builds BOTH halves of the §13.2/§13.3 targeted form — * the subject's authorization-mode token block and the body target block — so they can never * disagree. `self` carries no body target (the caller triple IS the target, §13.3). */ export type EpVerbTarget = { mode: "self"; } | { mode: "owner" | "any" | "child" | "ledger" | "handle"; owner: string; actor: string; lifecycleUid: string; mappingRevision?: number; }; /** What every verb needs to address one command: the compiled §13.7 contracts (digests derive * from `closureDigest`, exactly like the serve table), the caller triple the credential pins, * and an optional display name for the advisory `from.name`. */ export interface EpVerbOp { endpoint: string; command: string; contract: { input: CompiledContract; output: CompiledContract; }; caller: EpCaller; args?: Record; target?: EpVerbTarget; /** The incarnation this caller resolved against (§13.3). Carrying it makes a responder that is * not that incarnation refuse BEFORE running the command, which is the only place the refusal * can be a guard rather than a report. Omitted ⇒ any member of the class may serve the call. */ bind?: EpBindBlock; correlation?: EpCorrelation; /** Opaque signed authorization-context slot (§13.3); carried as-is. */ auth?: string; goalId?: string; /** * A caller-PINNED envelope id, replacing the per-publish nonce. A goal-accepting command binds * its goal under the envelope id, so a durable caller that must resubmit idempotently — a * workflow run resuming across a crash re-derives the same `requestId` and the far side serves * the recorded acceptance instead of accepting twice — pins the id it derived BEFORE submitting. * Validated as an id token; reply correlation still rides the per-publish subject nonce, so two * calls pinning one id cannot cross-deliver replies. */ id?: string; /** Advisory display name for `from.name`; `from.id` is DERIVED from the caller triple, so it * always equals the broker-authenticated sender principal (§13.3). */ name?: string; } /** What a liveness probe can establish about ONE frozen instance (§13.5). Only `gone` is an * AFFIRMATIVE fact — the broker itself reporting that the instance holds no subscription on its * own rail. `live` and `unknown` are handled identically by the gather: they license nothing and * the full deadline stands. There is deliberately NO verdict for "its presence entry expired": * a lapsed heartbeat is absence of evidence, and reading absence of evidence as death is how a * slow correct answer becomes a fast wrong one. */ export type EpInstanceLiveness = "gone" | "live" | "unknown"; /** * Ask the BROKER whether one instance still holds a subscription on its own `inst` rail (§13.2). * * A serving incarnation subscribes `ep.inst....>` for every command * it serves, so a request published there with a reply-to only the broker can reach either draws * the broker's no-responders 503 — an affirmative statement that this instance holds no * subscription — or draws nothing at all. * * NOTHING IS `unknown`, NEVER DEATH. A denied publish, a slow broker, a responder that receives the * request and declines to answer, and a perfectly healthy instance are indistinguishable from here, * and every one of them must keep the caller waiting. The failure direction is the whole safety * argument: because only a 503 says anything, a short probe budget can never turn a live instance * into a fast `missing` — it can only fail to speed up a dead one. * * It rides `describe` because §13.7 makes every endpoint serve it and it carries no effect, and it * rides as a CAST: the verdict comes from the transport, not from an answer, so nothing is read and * an instance whose describe is broken still reads as present. */ export declare function epProbeInstanceInterest(nc: NatsConnection, space: string, endpoint: string, instanceId: string, caller: EpCaller, opts: { deadlineMs: number; }): Promise; /** One attributed reply: the structural attribution comes from the reply SUBJECT (§13.2), never * from the body. */ export interface EpAttributedReply { reply: EndpointReply; responder: { endpoint: string; instanceId: string; epoch: number; }; } /** * Call one command and await its reply within `deadlineMs`: on the `one` (queue-group anycast) or * `inst` (stable incarnation) rail with `replyExpected: true`, subscribe the caller's own * nonce-scoped reply subject BEFORE publishing, and resolve the first attributed reply — BOUND to * the invoked identity (§13.2, the stale-reply rejection rule: "callers reject" stale-process replies), on BOTH rails: * - `inst` pins the addressed `(instanceId, epoch)` incarnation up front; a stale-epoch reply is * `expired` and a wrong-instance reply `internal`. * - `one` cannot pin an instance up front (the queue picks the responder), so the caller MUST supply * `currentEpoch(instanceId)`: after the reply lands, the answering incarnation's epoch is checked * against its current registry epoch, and a superseded-but-still-connected queue member's reply is * `expired`. The queue winner is NOT implicitly current — that is a check, not an assumption. * `currencyReference` says what that hook returns, for the refusal's wording and marker: the * documented `registry` read (default), or `bind` when a caller supplies its own resolve's epoch * instead (the describe-bound default in {@link invokeCommand}), where a responder ahead of it is * a successor and the caller's handle is the stale side. * * Application-level failure is NOT a throw: the resolved `reply` carries `ok: false` with the * responder's structured error (§13.3). This boundary throws only for its own refusals: invalid args * `bad-request`; an unparseable/mis-echoed/mis-attributed reply `internal` (a raw decode error never * escapes); a throwing `currentEpoch` hook `internal` and a garbled (non-integer/negative) currency * value `failed-precondition` (the read's own failure, never mislabeled staleness); a stale reply `expired`; * NO responder `unavailable` (SPEC 13.5, the broker no-responders answer — the broker's no-responders 503 lands on a reply-to that * sits on THIS caller's own rail, so a manual, fully-disposed probe distinguishes it from a slow * responder without leaving a lingering request); a failed reply subscription `unavailable`; a * broker publish violation `permission-denied` naming the refused subject (the connection-status * watch that keeps a missing GRANT from reading as an unanswered deadline); the elapsed budget * `deadline-exceeded`. Every subscription, timer, and status watch is released in the `finally`. */ export declare function epCall(nc: NatsConnection, space: string, route: { mode: "one"; } | { mode: "inst"; instanceId: string; epoch: number; }, op: EpVerbOp, opts: { deadlineMs: number; currentEpoch?: (instanceId: string) => Promise | number; currencyReference?: "bind" | "registry"; }): Promise; /** * Cast one command: the same request form with `replyExpected: false` — the responder never * replies, even on failure (§13.5 at-most-once), so this resolves once the request is flushed to * the broker. `deadlineMs` is optional and advisory for `one`/`inst` (the envelope requires it only * for calls and journal submissions, §13.3), but MANDATORY for `all`: §13.2's * `checkRequestSubjectAgreement` refuses an all-rail request without a deadline regardless of * `replyExpected`, and a cast has no reply on which that refusal could surface — so an all-cast * without a deadline would be silently dropped by every responder. Fail loud at the caller instead. */ export declare function epCast(nc: NatsConnection, space: string, route: EpRoute, op: EpVerbOp, opts?: { deadlineMs?: number; }): Promise; /** One attributed event: identity and epoch come from the SUBJECT (§13.2: forge-locked tokens; * a stale-epoch event is attributably stale — surfaced, never hidden). */ export interface EpAttributedEvent { endpoint: string; instanceId: string; epoch: number; topic: string[]; event: EndpointEvent; } export interface EpWatchHandle { stop(): Promise; } /** * Watch a granted `epe` subtree LIVE (the event half of §13.5 `watch`; §13.9: the read grant is the * caller's own `sub.allow` row, e.g. the per-goal progress subtree — delivery lands only on this * caller's own subscription). Every event is validated at this consuming boundary: an unparseable * subject or body is reported through `onError` (§13.3: fail loud, never a silent drop) and never * reaches `onEvent`. This is the LIVE tap ONLY — durable catch-up / filtered replay is the §13.9 * mediated read (see the module note above), and record watch is `watchRecord` (§13.4). */ export declare function epWatchEvents(nc: NatsConnection, space: string, filter: string, handlers: { onEvent: (ev: EpAttributedEvent) => void; onError: (err: EpEnvelopeError) => void; }): EpWatchHandle; /** The scatter gathers against a request-scoped FROZEN expected set (§13.5): the live instances of * the class, each `(instanceId, registrationRevision, epoch)` at send time. This is exactly * {@link import("./endpoint-service.js").freezeExpectedSet}'s output — pass it through so the freeze * identity (all THREE coordinates, not just instance+epoch) is what the gather classifies against. */ export type EpScatterSlot = FrozenInstance; /** Why a frozen slot's reply is churn (§13.5): `epoch` — it replied at a DIFFERENT process epoch than * frozen (a takeover restarted it); `registration` — its `svc….spec` registrationRevision advanced * past the frozen value (a re-registration re-declared its surface). Both mean the reply may be from * an incarnation that never saw this request, so it does NOT count toward completion. Registration * churn is NOT visible on the reply rail (the reply subject carries epoch, not registrationRevision, * and a re-registration does not advance the epoch), so it is observed only when a registration * reconcile runs (see {@link epScatter} `reconcileRegistration`). */ export type EpChurnReason = "epoch" | "registration"; /** The §13.5 scatter outcome. `complete` means EXPECTED-SLOT COVERAGE — every frozen slot produced * exactly one counted valid reply at its frozen `(epoch, registrationRevision)`, verified against the * registration reconcile — NOT that the gather was anomaly-free. `missing` and `invalid` force * `complete` false; a `registration`-churn drops a slot's counted reply (so that slot becomes * uncovered → false). But `duplicate`, `unexpected`, and an `epoch`-churn reply do NOT by themselves * force false: a slot that answered validly at its frozen epoch stays counted even if a stray * different-epoch reply from another incarnation also arrived. First valid reply per frozen * `(instanceId, epoch)` wins. */ export interface EpScatterResult { complete: boolean; /** instanceId → the first VALID attributed reply from that frozen slot at its frozen epoch. */ replies: Map; /** Frozen slots with NO reply of any kind by the DEADLINE (the classification point). A slot that * produced only churn/duplicate/invalid is reported there. A slot that produced ONLY a `late` reply * is BOTH `missing` (no on-time reply) and `late` (observational): classification linearizes at the * deadline, and the drain enriches, never moves it. */ missing: string[]; /** Replies from a DIFFERENT endpoint, or from an instance OUTSIDE the frozen set. Never count. */ unexpected: { instanceId: string; epoch: number; }[]; /** Frozen slots whose reply came from a superseded incarnation — a DIFFERENT epoch, or (via the * mandatory `reconcileRegistration`) an advanced registrationRevision. Does NOT count. */ churn: { instanceId: string; epoch: number; reason: EpChurnReason; }[]; /** Second-and-later replies from a frozen `(instanceId, epoch)` after its first classified one: * REPORTED, never silently dropped (§13.5); first reply wins, whatever it was classified. */ duplicate: { instanceId: string; epoch: number; }[]; /** Valid frozen-slot replies observed AFTER the deadline, during the optional bounded `lateDrainMs` * window: too late to count, reported not dropped. Empty unless `lateDrainMs` set. */ late: { instanceId: string; epoch: number; }[]; /** Frozen-slot replies that failed this consuming boundary (unparseable body, id mismatch, invalid * success payload, mis-attributed). NON-TERMINAL: an invalid frame does NOT consume the slot's one * terminal reply — the slot stays open to a later valid reply, bounded by the deadline. But any * recorded invalid keeps `complete` false (§13.3 fail-loud: an observed anomaly is not clean). * Not a §13.5-enumerated bucket; kept because fail-loud forbids counting an invalid reply as valid. */ invalid: { instanceId: string; epoch: number; message: string; }[]; } /** The reconcile hook's per-instance verdict. An instance STILL in the registry carries its current * `registrationRevision`; one the mediated §13.9 read observed as GONE is `{ registered: false }`. * This is an EXPLICIT value, distinct from an absent Map entry — an absent entry stays an incomplete * read (`failed-precondition`), so a buggy/partial hook can never masquerade as "everyone deregistered". * A mid-scatter deregistration is NOT registration-churn (a re-registration advances the revision and * invalidates the reply; a plain departure does not): a valid reply the instance already gave still * counts, and if it never replied its slot falls to `missing`. */ export type EpRegistrationState = { registered: true; registrationRevision: number; } | { registered: false; }; /** * Scatter one command to a FROZEN expected set (§13.5): publish once on the `all` rail, gather * attributed replies on the caller's nonce-scoped rail, and CLASSIFY against the freeze. An empty set * refuses (`failed-precondition`, never an empty success), and the observation channel is fail-loud: * a failed reply subscription is `unavailable`, never fabricated member silence. * * CLASSIFICATION LINEARIZES AT THE GATHER DEADLINE. The classification point T is `min(all frozen * slots answered-valid, deadline)`; `missing` is fixed at T (from the `respondedAtDeadline` snapshot). * The `lateDrainMs` window runs AFTER T on an ABSOLUTE clock — the rail is closed exactly `lateDrainMs` * after T regardless of how long the reconcile takes, so late classification cannot leak past the * requested horizon. It is OBSERVATIONAL only: it may add to `late`/`duplicate`, never move * `missing`/`churn`/`complete`. With `lateDrainMs` omitted the rail closes at T (no `late`). * * WHOLE-OPERATION BUDGET. The op is bounded: the gather to T comes FIRST, then a post-T phase in which * the reconcile and the drain run CONCURRENTLY (the drain is armed at T, before the reconcile await). * So worst-case wall-clock ≈ `deadlineMs` (gather) + `max(reconcileDeadlineMs, lateDrainMs)` (post-T), * NOT their sum. The reconcile is a bounded read taken SHORTLY AFTER T (not a zero-width at-T snapshot): * a true instant-of-T revision would need a watch/frontier, so a re-registration strictly concurrent * with the bounded read is an inherent, documented window (recorded for the §13.5 SPEC reconciliation). * * Two §13.5 signals are not on the reply rail, so the caller supplies them as HOOKS (keeping the verb * free of storage coupling; the §13.9 read grant stays with the caller, and a caller-read revision is * more trustworthy than a responder-stamped one): * - `reconcileRegistration` (REQUIRED): reads EVERY frozen slot's CURRENT state after the classification * point, returning a per-instance `EpRegistrationState` verdict. A slot still registered at a revision * ADVANCED past its frozen one is `churn` ("registration") and uncounted (a re-registration advances * registrationRevision WITHOUT advancing the epoch, so the reply rail cannot see it). A slot the read * observed as GONE (`{ registered: false }`) is an explicit mid-scatter deregistration: NOT churn, and * a valid reply it already gave still counts (a plain departure does not invalidate the reply the way * a re-registration would). Its result is COMPLETENESS-validated: a frozen id ABSENT from the returned * Map is an incomplete read (`failed-precondition`) — distinct from an explicit `{ registered: false }` * verdict — a non-integer/non-positive revision is a garbled read (`failed-precondition`), and a * revision BELOW the frozen one is a non-monotonic/buggy read (`failed-precondition`); otherwise a * partial hook would silently preserve the old full-triple over-claim. It is BOUNDED by * `reconcileDeadlineMs`: a never-settling read is `unavailable`, never a hung scatter (SPEC 13.5: * deadline mandatory); an unreadable registry is `failed-precondition`. Authoritative `complete` * requires it. * - `reconcileDeadlineMs` (optional, default `deadlineMs`): the explicit bound on that post-T read, * named so the single `deadlineMs` is not silently spent twice. * - `lateDrainMs` (optional): the absolute post-T horizon for `late` classification. Omitted → none. * - `probeLiveness` (optional): affirmative per-instance liveness, run concurrently with the gather. * The gather's only exits are "every frozen slot answered" and the deadline, so a slot that CANNOT * answer makes the first unreachable and the deadline is paid in full — every scatter in the space, * forever, because the registry has no expiry and a crashed instance never deregisters. This hook * supplies the missing fact. It moves the classification point T EARLIER and moves NOTHING else: * a slot the broker affirms is gone is still `missing`, still surfaced, still not `complete`. * ONLY the verdict `gone` licenses anything; `live`, `unknown`, a throwing hook, and any value * outside the closed set all license nothing and leave the full deadline standing. That asymmetry * is deliberate and is the safety argument: a broken or lying-quiet probe degrades to exactly * today's behavior, never to a fast wrong answer. */ export declare function epScatter(nc: NatsConnection, space: string, op: EpVerbOp, opts: { deadlineMs: number; expected: EpScatterSlot[]; reconcileRegistration: () => Promise>; reconcileDeadlineMs?: number; lateDrainMs?: number; probeLiveness?: (instanceId: string) => Promise; }): Promise; /** * Scatter one command to the LIVE class (§13.5), registry-wired end to end: freeze the expected * set from the service registry, publish once on the `all` rail, and reconcile registration * currency post-T — the full §13.5 sequence behind one call. `jsm` drives the LEADER-served * coordinate reads (the freeze's per-slot spec/status and the reconcile), `kv` the bounded-lag * instance enumeration; every freeze/reconcile refusal (`failed-precondition` on an empty or * unreadable registry, `internal` on malformed mediated-writer state) and every gather * classification passes through unchanged. The hooks stay public: a caller composing its own * freeze (a pinned set, a test harness) uses {@link epScatter} directly. */ export declare function epScatterService(nc: NatsConnection, jsm: JetStreamManager, space: string, op: EpVerbOp, opts: { deadlineMs: number; reconcileDeadlineMs?: number; lateDrainMs?: number; /** The §13.5 liveness hook, FORWARDED from the caller and never invented here. * * This function knows the frozen set only AFTER the freeze, so an auto-probe built in here * would publish on instance rails the caller may hold no grant for, and a refused publish is * invisible to it: the broker's violation lands on the CONNECTION, the probe just times out, * and the operation reports `unknown` — the exact "slow" reading of a permission bug this * whole change exists to stop. The caller is the only layer that knows which rails its * credential carries, so the caller supplies the closure (and, with it, its own budget and its * own violation reporting). Omitted ⇒ no probe, and the gather is bit-for-bit the pre-#468 * deadline behaviour. * * {@link epProbeInstanceInterest} is the ready-made implementation; a caller pins it to its * own grant set. */ probeLiveness?: (instanceId: string) => Promise; }): Promise; /** * Call one command with the registry-wired currency check (§13.2): on the `one` rail the queue * winner's epoch is verified against a LEADER-served read of its `svc….status` through * {@link serviceEpochReader}; the `inst` rail pins its incarnation up front and needs no read, so * the hook is not wired there. Every {@link epCall} refusal passes through unchanged (a superseded * winner is `expired`; an unregistered or never-converged responder is the read's own * `failed-precondition`, never mislabeled staleness). */ export declare function epCallService(nc: NatsConnection, jsm: JetStreamManager, space: string, route: { mode: "one"; } | { mode: "inst"; instanceId: string; epoch: number; }, op: EpVerbOp, opts: { deadlineMs: number; }): Promise; //# sourceMappingURL=endpoint-verbs.d.ts.map