import { type JetStreamClient, type JetStreamManager } from "@nats-io/jetstream"; import { type NatsConnection } from "@nats-io/transport-node"; /** * The successor's first act, and the only record the runtime layer writes that is not a step. * * `replayedTo` is the sequence the activation expected, so a reader of the journal alone can say * which prefix each driver actually saw — a takeover that replayed less than its predecessor wrote * is visible in the record rather than inferred from a gap. */ export interface RunJournalActivation { readonly v: 1; readonly kind: "activation"; readonly run: string; /** * This record's position in the run's journal, from 0. The CHAIN: a replay requires * `records[i].n === i`, which is the only check that sees a record removed from the MIDDLE — * counting cannot, and neither can any anchor at the front. Measured: deleting one interior * message left a replay of sequences [1,2,4] that passed every other check, so the run would have * re-performed an effect it had already done. */ readonly n: number; /** The driver principal taking the run over. */ readonly holder: string; /** * The work-pool lease's fencing token this takeover is authorized by — `WorkLease.fencingToken`, * a positive integer that only ever increases for a given work item. It is recorded so that the * NEXT takeover can refuse to activate under an older one: see {@link activateRun}. */ readonly fencingToken: number; /** The holder's process epoch: two takeovers by one principal are still two drivers. */ readonly epoch: number; readonly replayedTo: number; readonly at: number; } /** One step-journal entry, wrapped. The `entry` is the LANGUAGE's shape and core does not read it: * what a step recorded is the interpreter's business, and a wire layer that parsed it would have to * be revised every time the language learns an effect. */ export interface RunJournalStep { readonly v: 1; readonly kind: "step"; readonly run: string; /** This record's position in the run's journal, from 0. See {@link RunJournalActivation.n}. */ readonly n: number; readonly at: number; readonly entry: unknown; } export type RunJournalRecord = RunJournalActivation | RunJournalStep; /** A record as it was read back, with the stream sequence it landed at. */ export interface StoredRunJournalRecord { readonly seq: number; readonly record: RunJournalRecord; } /** * The server refused an append from a driver that had already activated: the run's subject is no * longer where this driver left it. * * Usually that means someone else activated, and that is what the name says. It is NOT the only * cause: purging a run's subject — the retirement mechanism the subject range exists for — also * moves the head out from under a live appender, and measured, it produced exactly this refusal * with no successor in existence. So `isSuperseded` is "the stream refused my expectation", and a * driver that needs to know WHICH replays the run: a journal that reads back empty was retired, one * that carries a later activation was taken over. * * Not retryable either way, and not an effect failure. It says nothing about the effect the entry * described — whatever it did, it did — only that this process may no longer speak for the run. */ export declare class RunSuperseded extends Error { readonly run: string; readonly subject: string; readonly expectedSeq: number; readonly cause?: unknown | undefined; constructor(run: string, subject: string, expectedSeq: number, cause?: unknown | undefined); } /** * An append ended without a PubAck for a reason that is not a refusal. * * A timeout, a dropped connection, a serialization failure: the entry may be on disk, may not be, * and the appender cannot tell. Either way its head is no longer known to match the subject's, so * every later append would carry an expectation it invented. That is the read-then-publish window * the barrier exists to close, so this is terminal in exactly the way {@link RunSuperseded} is — * distinct only because nobody else has necessarily taken the run, and the same driver may recover * it by replaying and activating again, which is what re-reads the true head. */ export declare class RunJournalStalled extends Error { readonly run: string; readonly subject: string; readonly expectedSeq: number; readonly cause: unknown; constructor(run: string, subject: string, expectedSeq: number, cause: unknown); } /** * An ACTIVATION lost its CAS: the prefix this driver replayed is already stale. * * Deliberately not {@link RunSuperseded}. Nothing orders a takeover ahead of the outgoing driver's * last packet, so losing here is the ordinary case of "it appended while I was reading", and the * successor has performed no work to abandon. It re-replays and activates again while it still holds * the lease. Only an append AFTER a won activation means superseded. */ export declare class ActivationRaced extends Error { readonly run: string; readonly subject: string; readonly expectedSeq: number; readonly attempts: number; readonly cause?: unknown | undefined; constructor(run: string, subject: string, expectedSeq: number, attempts: number, cause?: unknown | undefined); } /** * The replay did not deliver the whole prefix. * * A short replay is the same failure as an evicted journal prefix: the run re-performs effects it * has already performed, under a driver that believes it is resuming correctly. A pull iterator * ends cleanly on a dropped connection, so "it stopped early" and "there was no more" look alike on * the wire and have to be distinguished by counting. */ export declare class RunJournalReplayIncomplete extends Error { readonly run: string; readonly expected: number; readonly delivered: number; constructor(run: string, expected: number, delivered: number); } /** * Two drivers replayed the same run at once, and this one inherited the other's consumer. * * Retryable at the takeover level — the loser waits and replays again — and deliberately not an * incomplete-replay error, because nothing is missing from the JOURNAL: the run is simply being * contended, which is what a takeover is. */ export declare class RunJournalReplayRaced extends Error { readonly run: string; readonly durable: string; readonly alreadyDelivered: number; constructor(run: string, durable: string, alreadyDelivered: number); } /** * The replayed prefix does not start at the run's beginning. * * Its counts were consistent and its last sequence may well be the subject's true head — that is * exactly why this check exists separately from {@link RunJournalReplayIncomplete}. */ /** * A takeover was attempted under a lease token older than the one the run is already held under. * * Terminal for this driver: it does not hold the lease, and the fix is not to try again but to stop * driving. The activation barrier cannot catch this on its own — a stale holder that replays learns * the head like anyone else — so the ordering is enforced against the journal's own record of who * activated, which the replay has just read. */ export declare class StaleLeaseToken extends Error { readonly run: string; readonly offered: number; readonly held: number; readonly holder: string; constructor(run: string, offered: number, held: number, holder: string); } /** * A takeover said the run exists and its journal is empty. * * Terminal. A run whose journal has been purged is RETIRED, not new, and the difference is * unrecoverable from the stream alone — which is why the caller states which it expected. */ export declare class RunNotResumable extends Error { readonly run: string; readonly subject: string; constructor(run: string, subject: string); } /** A takeover said the run is new and its journal already has records. Terminal: a run id is used * once, and re-running one under its own id is a fork, which takes a new id. */ export declare class RunAlreadyStarted extends Error { readonly run: string; readonly records: number; constructor(run: string, records: number); } /** A takeover carried a current fencing token but not the identity that holds it. Terminal. */ export declare class ActivationNotAuthorized extends Error { readonly run: string; readonly why: string; constructor(run: string, why: string); } export declare class RunJournalPrefixTruncated extends Error { readonly run: string; readonly seq: number; readonly expectedN: number; readonly foundN: number; constructor(run: string, seq: number, expectedN: number, foundN: number); } /** * The freshness test itself, separate so it can be put to a consumer that really was inherited. * * A consumer that has delivered anything, or is holding an ack, has fed another driver part of this * run; what is left on it is a tail, however consistent its counts look. */ export declare function assertReplayConsumerFresh(run: string, durable: string, info: { delivered: { consumer_seq: number; }; num_ack_pending: number; }): void; /** * Measured on the repo's broker floor: `name: "ConsumerNotFoundError"`, `code: 10014`. * * Exported so the one error the replay is allowed to swallow can be checked against a real one. */ export declare function isConsumerNotFound(e: unknown): boolean; /** Parse one stored record. The envelope is validated; the step's `entry` is passed through. */ export declare function parseRunJournalRecord(raw: unknown, subject: string): RunJournalRecord; export interface RunJournalReplay { readonly records: readonly StoredRunJournalRecord[]; /** The run subject's last sequence, and therefore the expectation an activation must carry. 0 * when the run has never been appended to, which is the create-only expectation. */ readonly lastSeq: number; } /** * Read the run's whole journal, in append order, from the beginning. * * The replay durable is DELETED and recreated rather than reused. A durable remembers how far it * delivered, and resume is not a cursor: every takeover needs the prefix FROM THE TOP, so a durable * that survived the previous driver would hand its successor the empty tail and let it resume a run * as if nothing had happened. Deleting is also why a rival's interference is bounded — it can only * disturb a pre-activation replay, and a disturbed replay either fails its count check or loses its * activation CAS, both of which end in "replay again". * * Both halves of that sentence are enforced here rather than assumed of the caller: one replay at a * time per durable in this process (see `exclusively`), and a durable of that name left behind by an * earlier replay is removed before this one reads (see below). */ export declare function replayRunJournal(js: JetStreamClient, jsm: JetStreamManager, space: string, runId: string, takeoverId: string): Promise; /** A takeover id: what a driver asks its credential to be minted for. Callers that mint their own * rows use this; one that is handed a credential uses the id that credential names. */ export declare function newTakeoverId(): string; /** * The one authorized appender for a run, for as long as nobody else takes it. * * Obtained only from {@link activateRun}: an appender that has not activated does not know the * subject's sequence, and one that guessed would be the read-then-publish window again. The head is * private and advances only from a PubAck — a caller that could pass an expectation in could pass a * stale one, and every branch of a concurrency scope would be passing its own. */ export declare class RunJournalAppender { private readonly js; readonly run: string; readonly subject: string; /** The prefix this driver replayed, which is the journal the interpreter resumes from. */ readonly replayed: readonly StoredRunJournalRecord[]; private seq; /** Set once the stream refuses an append. Terminal, and checked before the wire is touched. */ private dead; /** The serial pump. Every append waits for the one before it, so one expectation is in flight. */ private chain; /** The next journal ordinal. Allocated under the serial pump, so it cannot be raced, and only * advanced by a PubAck — a stalled append leaves the ordinal where it was, and the appender is * finished anyway. */ private nextN; private constructor(); /** @internal — {@link activateRun} is the only way in. */ static of(js: JetStreamClient, run: string, subject: string, replayed: readonly StoredRunJournalRecord[], seq: number): RunJournalAppender; /** The last sequence this appender has seen acknowledged on the run's subject. */ get lastSeq(): number; /** True once the stream has REFUSED an append: someone else holds the run. */ get isSuperseded(): boolean; /** * True once this appender has stopped writing, for either reason. Drivers test this; only the * recovery path cares which of the two it was. */ get isFinished(): boolean; /** * The highest journal ordinal this appender has written or replayed. * * The run record's tail anchor is written from this: it is the one number that says how far the * journal got, and it lives here because `nextN` advances only on a PubAck — so it is a fact about * what the broker accepted, never about what this process attempted. */ get journalHigh(): number; /** The step entries of the replayed prefix, in order, with the activations dropped. */ steps(): readonly unknown[]; /** * Append one step entry. Serialized against every other append on this run. * * Callers do not pass a sequence and could not usefully hold one: two concurrent branches of a * `parallel` would both hold the same head, and the second publish would be refused by a server * doing exactly what it was asked. Here they queue instead, and the head moves only when the * broker says it did. */ append(entry: unknown, at: number): Promise; private publishOne; } export interface RunTakeover { readonly space: string; readonly runId: string; readonly holder: string; /** The work-pool lease's fencing token. A takeover under an OLDER one is refused. */ readonly fencingToken: number; readonly epoch: number; /** * Whether this activation STARTS the run or RESUMES one that already exists. * * An empty journal is ambiguous on its own and the stream cannot resolve it: a run that was never * started and a run that was retired by subject purge — which is the retirement mechanism the * subject range exists for — read back identically as zero records. Measured: a purged run * activated again as if new. Only the caller knows which it meant, because the run record is what * says the run exists, so the caller states it and this refuses the mismatch either way. */ readonly expect: "new" | "existing"; /** * The id this attempt's replay consumer is named for, and the id its CREDENTIAL was minted for. * * A consumer name is one subject token, so a per-takeover name cannot be covered by a grant * pattern — `*` is a whole-token wildcard in NATS and `wfj__*` is a literal that matches * nothing. The uniqueness therefore has to be known when the rows are minted, which is when the * lease is handed out, so the caller brings it here rather than the replay inventing one. */ readonly takeoverId: string; readonly at: number; } export interface ActivateOptions { /** How many replay+activate rounds to try before giving up. Each round re-reads the prefix. */ readonly attempts?: number; /** * Called between rounds, and the place a driver re-checks that it still holds the lease. * * A losing activation means the journal moved, which is also the shape a takeover BY SOMEONE ELSE * has: retrying forever would be a driver that lost the lease politely re-reading until the real * holder pauses. Throwing from here stops the loop with that reason. */ readonly beforeRetry?: (attempt: number) => Promise | void; /** * Called with the replayed prefix, immediately before the activation is published. * * This is the LAST point at which anything can be checked against a prefix that is still current, * so it is where a driver re-reads its lease: the gap between "I replayed" and "I claimed" is the * only window in the takeover, and every millisecond of work moved out of it is a window narrowed. * Throwing from here abandons the takeover without claiming the subject. */ readonly onReplayed?: (replay: RunJournalReplay) => Promise | void; } export declare function activateRun(js: JetStreamClient, jsm: JetStreamManager, takeover: RunTakeover, opts?: ActivateOptions): Promise; /** Convenience for a caller holding only a connection. */ export declare function runJournalContext(nc: NatsConnection): Promise<{ js: JetStreamClient; jsm: JetStreamManager; }>; //# sourceMappingURL=run-journal.d.ts.map