/** * Shared runtime context and projections for the application command mailbox * (WFT-84). * * The public class and the maintenance pass both need the same bound storage * keys, resolved policy, clock, commit path, and attempt-controller registry. * Passing that context explicitly — rather than reaching into the class — keeps * `mailbox-maintenance.ts` a set of ordinary functions that tests * can drive directly. * * @module core/mailbox-internals */ import type { BatchOperation, ConditionalBatchCondition, Storage } from '../storage/interface.ts'; import { AttemptRegistry } from './application-primitive-attempt-registry.ts'; import type { ApplicationCommandReceipt, MailboxEventSink } from './mailbox-contract.ts'; import { type MailboxKeys } from './mailbox-storage.ts'; import type { ApplicationCommandRecord, ApplicationCommandTerminalRecord } from './mailbox-types.ts'; import type { ResolvedMailboxPolicy } from './mailbox-validation.ts'; import { WeftError } from './weft-error.ts'; /** * How many times a transition re-reads durable state and retries after losing a * compare-and-swap. * * The mailbox header is deliberately a per-mailbox hot key — admission and every * terminal transition both touch it, which is what makes backlog accounting * exact — so a busy mailbox does see contention. The ceiling exists so a * pathological loop surfaces as an error instead of spinning forever. */ export declare const MAX_MAILBOX_TRANSITION_ATTEMPTS = 25; /** * How many scan pages one maintenance pass may walk. * * The pass reaches the whole keyspace by paging, but stops after this many pages * so a very large mailbox cannot make one call run unboundedly. The next pass * starts again from the beginning and picks up whatever is still due. */ export declare const MAILBOX_MAINTENANCE_MAX_PAGES = 200; /** Everything a mailbox operation needs that is fixed at construction. */ export type MailboxRuntime = { readonly storage: Storage; readonly events: MailboxEventSink | undefined; readonly policy: ResolvedMailboxPolicy; readonly keys: MailboxKeys; readonly now: () => number; readonly generateId: () => string; /** * Abort controllers for attempts claimed in this process, keyed by attempt * token — shared across every handle onto the same mailbox, so a cancellation * raised through one handle reaches a claimant holding another. */ readonly attemptControllers: AttemptRegistry; /** * Record an attempt this handle now owns, or report that disposal already won. * * Registering ownership in the caller's `await` continuation would race * disposal: `dispose()` could run between the claim resolving and the token * being recorded, see nothing to abort, and leave a live claim from a disposed * mailbox. Doing both under one synchronous call closes that window. */ readonly adoptAttempt: (attemptToken: string) => (() => void) | null; /** * Where the previous maintenance pass stopped, when its page cap cut it short. * * Process-local rather than durable: it is an optimisation for walking a very * large keyspace across successive calls, and losing it on restart only means * the next pass starts from the beginning, which is always correct. */ readonly readMaintenanceCursor: () => string | undefined; readonly writeMaintenanceCursor: (cursor: string | undefined) => void; }; /** * Attempt controllers, shared per `(storage, namespace, resourceId)` within one * process. * * Two `Mailbox` handles onto the same durable mailbox are the same * mailbox. Giving each its own registry would make the documented in-process * cancellation channel silently fail whenever the claimant and the canceller * held different handles — the claimant's signal would never fire and it would * learn about cancellation only through renewal, which is supposed to be the * *cross-process* fallback. Keyed by `Storage` identity in a `WeakMap` so a * discarded backend takes its registries with it. */ /** The registry-scope tag for the mailbox, so it never shares a registry with another primitive. */ export declare const MAILBOX_PRIMITIVE = "mailbox"; /** * Thrown when a transition keeps losing its compare-and-swap. * * Surfacing this beats looping forever: it means durable contention on this * mailbox is real, and the caller — not a hidden retry loop — decides whether to * back off, shed load, or shard the resource. * * @example * ```ts * import { MailboxContentionError } from '@lostgradient/weft'; * * const error = new MailboxContentionError('admit', null); * console.log(error.code); // 'MailboxContentionError' * ``` */ export declare class MailboxContentionError extends WeftError<'MailboxContentionError'> { /** The mailbox operation that could not commit. */ readonly operation: string; /** The command the operation targeted, or `null` for mailbox-wide operations. */ readonly commandId: string | null; constructor(operation: string, commandId: string | null); } export declare function toApplicationCommandReceipt(record: ApplicationCommandRecord): ApplicationCommandReceipt; /** * The durable fleet event that describes a transition. * * A retry is distinguished from an initial admission even though both land in * `accepted`, so a consumer reading the feed can tell redelivery from first * delivery without diffing receipts. The payload is deliberately bounded: no * command payload, no failure details, nothing unbounded. */ export declare function describeCommandTransition(previous: ApplicationCommandRecord | null, next: ApplicationCommandRecord): { readonly kind: string; readonly payload: unknown; }; /** * Abort and forget the process-local controller for one attempt. * * Releasing a lease must never leave a live controller behind: the signal is * attempt-scoped, so a later attempt on the same command gets a fresh one. */ export declare function releaseAttemptController(runtime: MailboxRuntime, attemptToken: string, reason: string, commandId?: string): void; /** * Release every attempt this process holds for one command. * * For an observer that finds the command gone — retired by retention in * another process, whose maintenance cannot reach this registry — every local * attempt on it is over, whatever token it held. */ export declare function releaseAttemptsForCommand(runtime: MailboxRuntime, commandId: string, reason: string, currentToken?: string, observedAt?: number): void; /** * Commit one command transition together with the index maintenance and * backlog accounting it implies. * * Terminalizing a command decrements the mailbox's open count in the same * conditional batch, so `capacity()` can never drift from the records it * describes. Admission is the one transition that builds its own header * operation, because it also allocates the FIFO sequence. * * Returns `false` when a compare-and-swap was lost; the caller re-reads and * re-decides rather than retrying blindly with stale bytes. */ export declare function commitCommandTransition(runtime: MailboxRuntime, options: { readonly previous: ApplicationCommandRecord | null; readonly expectedBytes: Uint8Array | null; readonly next: ApplicationCommandRecord; readonly now: number; readonly extraConditions?: readonly ConditionalBatchCondition[] | undefined; readonly extraOperations?: readonly BatchOperation[] | undefined; }): Promise; /** Whether a record occupies one of the four terminal dispositions. */ export declare function isTerminalRecord(record: ApplicationCommandRecord): record is ApplicationCommandTerminalRecord;