/** * @module drain-cycle * @category Internal * * Two layers of the drain pipeline: * * - {@link run_drain_cycle} — pure function for one round-trip of * claim → fetch → group → dispatch → ack/block. No orchestrator state. * Reusable for property tests and standalone benchmarks. * * - {@link DrainController} — stateful driver that owns the armed flag, * the concurrency lock, and the adaptive lag/lead ratio. Wraps * `run_drain_cycle` with the lifecycle decisions Act used to make inline. * * @internal */ import type { BatchHandler, BlockedLease, CloseTarget, Drain, DrainOptions, Fetch, Lease, Logger, ReactionPayload, Registry, SchemaRegister, Schemas } from "../types/index.js"; import type { CircuitBreaker } from "./circuit-breaker.js"; import type { DrainOps } from "./drain.js"; /** * Outcome of processing a single leased stream — produced by Act's `handle` * / `handle_batch` dispatchers, consumed by `run_drain_cycle` to drive ack/block. * * @internal */ export type HandleResult = Readonly<{ lease: Lease; handled: number; /** * Event id at which the ack would land — the last *successful* event * id, or `lease.at` when the batch had no work (empty payloads). Named * `acked_at` to pair symmetrically with {@link failed_at} and to keep * it visually distinct from `Lease.at` (the pre-cycle watermark — same * field name across types but a different semantic). */ acked_at: number; error?: string; block?: boolean; /** * Wall-clock timestamp (ms since epoch) at which the next attempt on * this stream may run. Populated by `_finalize` only on retry paths * where the reaction defined `options.backoff`. Undefined means "no * backoff configured" — drain re-attempts as soon as the lease expires. */ next_attempt_at?: number; /** * Wall-clock timestamp (ms since epoch) at which this stream should be * re-visited. Set by a handler that *defers* instead of acking or * failing: the triggering events stay pending (watermark not advanced), * `retry` is not bumped (a defer is not a failure), and the drain holds * the stream until `defer` elapses, then redelivers so the handler can * re-evaluate. This is the timing primitive autoclose rides (#1090); * unlike {@link next_attempt_at} (a retry-only backoff), a defer carries * no error and never blocks. When present, the result is excluded from * ack and block — it neither advances nor terminates the watermark. */ defer?: number; /** * Close request (#1090). Set when a handler throws `CloseSignal` to retire * its stream: the triggering event is acked (so the closing reaction isn't * seen as an in-flight consumer by the close-cycle safety guard) and the * drain hands this {@link CloseTarget} to the orchestrator's `on_close`, * which runs `run_close_cycle`. Carries the optional archiver from the * signal. Distinct from {@link defer} (hold for later) — a close advances * and retires. */ close?: CloseTarget; /** * Event id that threw, when a handler error occurred. Distinct from * {@link acked_at}: `failed_at = acked_at + 1` in dense streams, but * adapters with sparse ids give the trace the exact position. Always * set on the per-event error path; absent in batch mode (where no * single event id can be attributed to the failure). */ failed_at?: number; }>; /** * Per-event reaction dispatcher signature (matches `Act.handle`). * @internal */ export type Handle = (lease: Lease, payloads: ReactionPayload[]) => Promise; /** * Bulk reaction dispatcher signature (matches `Act.handle_batch`). * @internal */ export type HandleBatch = (lease: Lease, payloads: ReactionPayload[], batchHandler: BatchHandler) => Promise; /** * One drain cycle's results. Returned by {@link run_drain_cycle}; consumed by * `Act.drain()` to update lifecycle state, the lag/lead ratio, and emit the * `acked` / `blocked` lifecycle events. * * @internal */ export type DrainCycle = { readonly leased: Lease[]; readonly fetched: Fetch; readonly handled: HandleResult[]; readonly acked: Lease[]; readonly blocked: BlockedLease[]; /** Streams a handler asked to close this cycle (#1090) — handed to `on_close`. */ readonly closeable: CloseTarget[]; }; /** * Run one drain cycle: claim streams, fetch their events, dispatch * matching reactions, ack the successes, block the retries-exhausted. * * Returns `undefined` when nothing was claimed — caller can short-circuit * the rest of the drain pass. * * **Deferred streams** (backoff windows and explicit defers) are excluded * upstream by `claim`: their persisted `deferred_at` gates re-dispatch, so * they never reach this cycle until the schedule elapses. * * @internal */ export declare function run_drain_cycle>(ops: DrainOps, registry: Registry, batch_handlers: Map>, misrouted: Set, /** The store failed on the previous pass — see {@link budget_exhausted}. */ store_failing: boolean, handle: Handle, handle_batch: HandleBatch, lagging: number, leading: number, eventLimit: number, leaseMillis: number, /** * Emitted as soon as `block` confirms, BEFORE the `ack` that follows. * A block is terminal: every adapter gates `block` on `blocked = false` * and excludes a blocked stream from `claim`, so it never runs again for * that stream. If the emit waited until the end of the cycle, an `ack` * failure in between would lose the `blocked` event permanently (#1390). */ on_blocked: (blocked: BlockedLease[]) => void, lane?: string): Promise | undefined>; /** * Dependencies the {@link DrainController} needs from the orchestrator. * The lifecycle event sinks (`on_acked` / `on_blocked`) are callbacks so * this module doesn't reach back into Act's emitter. * * @internal */ export type DrainControllerDeps> = { readonly logger: Logger; readonly ops: DrainOps; readonly registry: Registry; readonly batch_handlers: Map>; readonly handle: Handle; readonly handle_batch: HandleBatch; readonly on_acked: (acked: Lease[]) => void; readonly on_blocked: (blocked: BlockedLease[]) => void; /** * Close requested by a reaction (#1090). The controller calls this with the * cycle's {@link CloseTarget}s after acks/blocks land; the orchestrator wires * it to its `run_close_cycle` machinery (same path as `app.close`). Awaited so * a slow close doesn't overlap the next cycle's claim on the controller. */ readonly on_close: (targets: CloseTarget[]) => Promise; /** * Shared, orchestrator-owned circuit breaker (ACT-984). Trips after * repeated store failures so the drain loop stops hammering a down * backend; closed/half-open let attempts through. It also surfaces each * failure (via its own `on_error`, wired by the orchestrator to the * `error` lifecycle event), so callers just `failed(now, error)`. */ readonly breaker: CircuitBreaker; /** * Scope runner (#1191). The per-lane worker (`start`) ticks outside * any caller frame, so its `drain()` must be re-wrapped in the Act's * `_scoped` bag or `store()`/`cache()` resolve to the singleton for a * scoped Act. The orchestrator always threads its `_scoped` (identity * for a non-scoped Act), so it's required. */ readonly run_scoped: (fn: () => Promise) => Promise; /** Lane this controller drains. Undefined = spans all lanes (legacy single-controller). */ readonly lane?: string; /** Per-lane defaults applied when caller doesn't override via DrainOptions. */ readonly defaults?: { readonly streamLimit?: number; readonly eventLimit?: number; readonly leaseMillis?: number; }; }; /** * Stateful driver around {@link run_drain_cycle}. Owns: * * - `_armed` — has any commit / reset / cold-start signaled work to do? * - `_locked` — concurrent-call guard (overlapping `drain()` calls return * an empty result instead of running twice) * - `_ratio` — adaptive lag-to-lead frontier split, updated per cycle * * The orchestrator owns commits, lifecycle emission, and `arm()` triggers * — the controller owns everything between those edges. * * @internal */ export declare class DrainController> { private _armed; private _locked; private _ratio; /** * Per-stream re-visit schedule (#1090): `stream → next visit` (ms since * epoch). Holds both retry backoff (`HandleResult.next_attempt_at`) and the * `defer` outcome; cleared on successful ack or terminal block. Lives in * process memory — per-worker pacing by design (see {@link BackoffOptions} * for the multi-worker trade-off). Its wake re-arms drain at the earliest * pending visit. */ private readonly _defer; /** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */ private _worker; /** * Misroutings this controller has reported (#1563). A resolver returning a * projection's target does so for every matching event; one line per event * would bury the signal it exists to raise. */ private readonly _misrouted; private _stopped; /** * Resolves when the cycle currently in flight finishes; `undefined` when * no cycle is running (#1442). `_locked` answers "is a cycle running?" for * the overlap guard; this answers "tell me when it is done" for a graceful * shutdown, which needs to await the handler rather than abandon it * mid-`await` with the stream still leased. Never rejects — `drain()` * contains its own errors — so awaiting it is always safe. */ private _inflight; private _inflight_done; private readonly _deps; constructor(deps: DrainControllerDeps); /** * Signal that a commit (or reset / cold-start) may have produced work. * Subsequent `drain()` calls will run the pipeline; once the pipeline * settles to no-progress, the controller disarms itself. */ arm(): void; /** * Re-seed a persisted defer schedule into the process-local timer at cold * start (#1221). The `_defer` map lives in worker memory and is empty * after a restart; a stream deferred to a future due-time (e.g. an idle * autoclose aggregate) is durable in the store's `deferred_at` but has * nothing in memory to re-arm the drain at the due-time. The orchestrator * reads the persisted `deferred_at` for this controller's lane and calls * this to park the stream + (re)schedule the shared wake — so the drain * re-arms at the due-time with no intervening commit. `schedule()` * collapses many seeds into one timer, so callers may seed in a loop and * let the earliest due-time win. */ seed_defer(stream: string, at: number): void; /** Read-only flag — true while a commit / reset is unprocessed. */ get armed(): boolean; /** * The cycle currently in flight, or `undefined` when idle (#1442). A * graceful shutdown awaits this so an in-flight handler reaches its `ack` * — which releases the stream's lease — instead of being abandoned with * the lease held until it expires. */ get inflight(): Promise | undefined; /** * This lane's configured lease budget, or `undefined` when the lane didn't * pin one. It is the operator's own statement of how long a handler may * legitimately hold a stream, which makes it the right basis for a * shutdown grace budget (#1442). */ get lease_millis(): number | undefined; /** Lane this controller drains (undefined = legacy single-lane span). */ get lane(): string | undefined; /** * Start a per-lane worker that drains at the lane's `cycleMs` * cadence (ACT-1103). When armed, the worker calls `drain()` on every * tick and re-schedules; when not armed, it still re-schedules at * `cycleMs` so a future `arm()` is picked up on the next tick. * * The setTimeout chain uses `unref()` so it doesn't keep the process * alive on its own. */ start(cycleMs: number): void; /** Stop the per-lane worker. Idempotent. */ stop(): void; /** Run one drain pass. Short-circuits when not armed or already running. */ drain(options?: DrainOptions): Promise>; } //# sourceMappingURL=drain-cycle.d.ts.map