import type { Event } from '#compiled/@workflow/world/index.js';
/**
 * Delay before firing the deferred unconsumed-event check after the promise
 * queue has drained. Must be long enough for cross-VM microtask chains to
 * propagate (resolve in host → workflow code in VM → subscribe call back
 * in host). Any subscribe() arriving during this window cancels the check.
 */
export declare const DEFERRED_CHECK_DELAY_MS = 100;
/**
 * Floor for the deferred-check delay, so a too-low override can't manufacture
 * spurious divergence (each false positive burns a divergence-recovery retry
 * and can escalate to a terminal `CorruptedEventLogError`).
 *
 * Exported so tests needing the shortest legal delay can ask for it instead of
 * hardcoding a number this floor would silently clamp up.
 */
export declare const MIN_DEFERRED_CHECK_DELAY_MS = 10;
export declare enum EventConsumerResult {
    /**
     * Callback consumed the event, but should not be removed from the callbacks list
     */
    Consumed = 0,
    /**
     * Callback did not consume the event, so it should be passed to the next callback
     */
    NotConsumed = 1,
    /**
     * Callback consumed the event, and should be removed from the callbacks list
     */
    Finished = 2
}
type EventConsumerCallback = (event: Event | null) => EventConsumerResult;
export interface EventsConsumerOptions {
    /**
     * Callback invoked after an event has been consumed. Consumers such as the
     * deterministic workflow clock must not observe events that are merely
     * inspected while waiting for user code to subscribe to the next operation.
     */
    onConsumedEvent?: (event: Event) => void;
    /**
     * Callback invoked when a non-null event cannot be consumed by any registered
     * callback, indicating an orphaned or invalid event in the event log. The
     * check is deferred until after the promise queue has drained, ensuring that
     * any pending async work (e.g., deserialization/decryption) completes and
     * downstream subscribe() calls have a chance to cancel the check first.
     */
    onUnconsumedEvent: (event: Event) => void;
    /**
     * Callback invoked when an event is skipped because it repeats an event
     * class the walk already consumed for the same entity. `firstEventType` is
     * the type that recorded the class, which is the one the workflow observed.
     * Diagnostics only: skipping is a normal outcome, not an error, though a
     * `firstEventType` differing from `event.eventType` says the two writers
     * decided the entity's outcome differently, which is worth more than an
     * info log.
     */
    onDuplicateEvent?: (event: Event, firstEventType: Event['eventType']) => void;
    /**
     * Returns the current promise queue. The unconsumed event check is chained
     * onto this queue so it only fires after all pending async work (e.g.,
     * deserialization) has completed. This prevents false positives when async
     * deserialization delays the resolve() that triggers the next subscribe().
     */
    getPromiseQueue: () => Promise<void>;
    /**
     * Whether no data delivery is in flight (`isDeliveryIdle` in private.ts).
     * The unconsumed-event check waits for this before it fires: a delivery in
     * flight means the workflow VM is mid-reaction, and an event it has not
     * claimed yet is an event it has not reached yet.
     *
     * Required rather than defaulting to always-idle: always-idle is exactly the
     * pre-gate behavior, so a defaulted option would let a construction site opt
     * a whole replay path back out without saying so. Tests that drive a consumer
     * with no orchestrator context pass `() => true` to keep the pre-existing
     * timing, and say so at the call site.
     */
    isDeliveryIdle: () => boolean;
}
export declare class EventsConsumer {
    eventIndex: number;
    readonly events: Event[];
    readonly callbacks: EventConsumerCallback[];
    /**
     * Events the ordered walk stepped over because nobody claimed them and their
     * type carries no ordering claim. Each keeps the index it held in the log:
     * consumers read {@link eventIndex} at consumption time to order their
     * delivery against the rest of the log, and a late delivery must still make
     * the claim its position gave it.
     *
     * Held in log order, drained in log order, and drained before every offer so
     * a consumer registered after the walk passed the event still receives it.
     */
    private readonly parked;
    /**
     * Correlation ids of the {@link ONE_SHOT_EVENT_TYPES} events consumed so
     * far, so a second resolution for one of them is recognized as unclaimable
     * rather than parked for a consumer that cannot exist.
     */
    private readonly resolved;
    /**
     * `<class>:<correlationId>` for every event class the walk has already
     * consumed, mapped to the event type that recorded it. The type is kept so a
     * repeat that decided the same class *differently* (a `step_failed` behind a
     * `step_completed`) can be reported as more than a re-commit. See
     * {@link EventsConsumer.firstEventTypeOfClass}.
     */
    private readonly seenEventClasses;
    private onConsumedEvent?;
    private onUnconsumedEvent;
    private onDuplicateEvent?;
    private getPromiseQueue;
    private isDeliveryIdle;
    private pendingUnconsumedCheck;
    private pendingUnconsumedTimeout;
    private unconsumedCheckVersion;
    constructor(events: Event[], options: EventsConsumerOptions);
    /**
     * The oldest event the walk stepped over that no consumer has claimed yet,
     * if any. Parking is a bet that a consumer will be registered later, so at
     * any point where no consumer ever will be again (the replay finishing is
     * the definitive one), this answers which event the bet lost on.
     */
    get strandedEvent(): Event | undefined;
    /**
     * What the walk is still holding, or `undefined` when it holds nothing.
     *
     * Read at every point a replay stops, including the suspensions that are not
     * settling points, so the held state reaches telemetry. A replay cannot tell
     * a delivery awaiting a later consumer from one no consumer will ever
     * register, so it reports rather than decides: the same `eventId` reported on
     * suspension after suspension of one run is the shape that says the bet
     * parking made is not going to pay off, and that shape is only visible across
     * replays.
     */
    get parkedSummary(): {
        count: number;
        eventId: string;
        eventType: Event['eventType'];
    } | undefined;
    append(events: Event[]): void;
    /**
     * Registers a callback function to be called after an event has been consumed
     * by a different callback. The callback can return:
     *  - `EventConsumerResult.Consumed` the event is considered consumed and will not be passed to any other callback, but the callback will remain in the callbacks list
     *  - `EventConsumerResult.NotConsumed` the event is passed to the next callback
     *  - `EventConsumerResult.Finished` the event is considered consumed and the callback is removed from the callbacks list
     *
     * @param fn - The callback function to register.
     */
    subscribe(fn: EventConsumerCallback): void;
    private notifyConsumedEvent;
    private consume;
    /**
     * Offer `currentEvent` to each registered callback in turn. Returns true
     * when a callback consumed it. Does not move {@link eventIndex}: the ordered
     * walk and the parked drain advance differently, so each does its own.
     */
    private offer;
    /**
     * Offer everything parked, oldest first, until a pass claims nothing.
     *
     * Each offer runs with {@link eventIndex} moved back to the position the
     * parked event held in the log, because that is the position its consumer
     * will register a delivery barrier under. Restoring the walk pointer
     * afterwards is what keeps the two pointers from interfering.
     */
    private drainParked;
    /**
     * Release anything parked whose class a consumption has since recorded for
     * the same entity, on the same terms as {@link skipDuplicateEvent}.
     *
     * The ordered walk decides a straggler at the event, but only for one of the
     * two orders the copies can arrive in. When neither copy has a consumer yet,
     * both park — the walk steps over the first and re-enters immediately, so
     * the second is offered in the same tick, with no class recorded because
     * nothing has been *consumed*. The drain then claims the first and the
     * second is left held by a consumer list that will never grow the callback
     * it needs, which the workflow function returning reports through
     * {@link strandedEvent} as a replay divergence.
     *
     * Run before each offer pass rather than once, because the consumption that
     * records the class happens inside the drain itself.
     *
     * Not specific to `attr_set`: `wait_completed` reaches the same state, and
     * {@link ONE_SHOT_EVENT_TYPES} only covers it in the order where the
     * consumption came first.
     */
    private dropParkedDuplicates;
    /**
     * Step the ordered walk over an event nobody claimed, holding on to it for a
     * later consumer. Returns false when the event's type makes its position a
     * decision record, which is the one case where nobody claiming it means the
     * replay diverged.
     */
    private park;
    /**
     * The key `event`'s class is tracked under, or `undefined` for the events
     * that belong to no class and are therefore never skipped: the types with no
     * entry at all (`hook_received`, `hook_conflict`, `run_created`), and a
     * classed type carrying no entity to track it under.
     *
     * {@link classifyEntityEvent} owns that rule, because the observability UI
     * decides the same question about the same log and the two must not drift.
     */
    private eventClassKey;
    /**
     * Remembers that `event`'s class is now decided for its entity, if the type
     * belongs to a class. First writer wins: the recorded type is the one the
     * workflow observed, and a later repeat is measured against it.
     */
    private recordEventClass;
    /**
     * The type that already decided `event`'s class for the same entity, or
     * `undefined` when nothing has: a second `step_created` for one step, a
     * second terminal outcome, a second `step_started` after the step's result is
     * already in the log.
     *
     * Such an event is committed but inert. Concurrent replays write into one
     * log without a currency guard, so a replay working from a prefix that
     * predates another replay's write can commit its own copy of work the log
     * already records. That copy cannot change what the workflow observed: the
     * outcome was decided by the first event of the class and every later replay
     * reads that same event at the same log position, so ignoring the straggler
     * is deterministic across replays.
     *
     * Classes are tracked separately, so passing one does not suppress another.
     * A step whose result is in the log still reaches its `step_created` and
     * `step_started` consumers if it has yet to see those classes.
     *
     * Consulted only after every registered callback has declined the event, so
     * it can never take an event a consumer wanted. A retry's `step_started` is
     * claimed by the step's live consumer and counts as an attempt exactly as
     * before, and a second `step_created` reaching a step that has not finished
     * is likewise consumed rather than skipped; only the copies nobody claims are
     * skipped.
     *
     * Unlike the divergence report, this does *not* wait out the deferred window
     * first, and it does not need to. The window buys time for a consumer that
     * has yet to register, and no such consumer can want this event: the class
     * was recorded by a consumption in this same replay, which means the entity's
     * consumer was registered and took an event of this class, and correlation
     * ids are minted from a monotonic ULID per body position, so nothing later in
     * the body registers a second consumer under this id. Waiting would cost
     * `getDeferredCheckDelayMs()` per straggler per replay for information that
     * cannot arrive: 0.75% of production runs carry at least one straggler, and
     * the p99 among those carries 155.
     *
     * The invariant to preserve if hook identity ever becomes caller-supplied
     * (an idempotency key rather than a minted id): two `createHook` calls in one
     * body could then share a correlation id, and the second consumer's
     * `hook_created` would be a repeat of a class this replay already recorded.
     * That would make skipping wrong for `hook_created`, and is the reason the
     * class map lives next to the event types rather than being inferred.
     */
    private firstEventTypeOfClass;
    /**
     * Steps the walk over a sealed-log `noop` (specVersion >= 7): the World's
     * backend wrote it to occupy a slot whose writer allocated the position and
     * died, so the log's density arithmetic holds. It is invisible to the
     * workflow: no consumer is offered it, no event class is recorded, and the
     * deterministic clock does not advance, exactly as with
     * {@link skipDuplicateEvent}, so a log that happens to contain one produces the same
     * timestamps as a log that does not. (Its `createdAt` is the seal time,
     * which can even postdate later slots' events; letting it touch the clock
     * would leak the sealer's wall clock into replay.)
     */
    private skipSealedNoop;
    /** Steps the walk over a repeat of an already-consumed class. */
    private skipDuplicateEvent;
    private handleEndOfLog;
    private scheduleUnconsumedCheck;
    /**
     * Decide what a still-unconsumed event is, now that the promise queue has
     * drained and the delivery gate says the VM is not mid-reaction.
     *
     * `mayPark` is false only for the end-of-log recheck of an event {@link park}
     * already holds. Nothing in the first branch applies to one of those: it is
     * not the event at the cursor, so the identity guard is not meaningful, and
     * it is parked already.
     *
     * A duplicate class never arrives here. {@link consume} steps over one in the
     * pass that offered it, before this check is ever scheduled, and a class
     * recorded while the check was in flight can only have been recorded by a
     * consumption inside {@link consume}, whose next pass re-offers this event
     * and steps over it there, leaving the identity guard above to drop the
     * in-flight check. One that parks before any consumption records its class
     * is released later by {@link dropParkedDuplicates} instead.
     */
    private resolveUnconsumedEvent;
    /**
     * Run `fn` once no data delivery is in flight, polling the way
     * `scheduleWhenIdle` does: let the promise queue drain, re-check a timer
     * tick later, repeat.
     *
     * Without this the check is a bet that every delivery the walk is running
     * ahead of lands inside a fixed window. Consumption is synchronous while the
     * resolution it triggers is not: a step result hydrates in the host, resolves
     * from a detached continuation behind `awaitEarlierDeliveries`, and only then
     * does VM code run far enough to subscribe the next consumer. Replaying a
     * batch of N parallel step results leaves N-1 of them on that detached path
     * with the queue already drained, so the walk sits on the ordered event the
     * VM is about to draw and the window is the only thing standing between a
     * healthy run and `ReplayDivergenceError`.
     *
     * Shortening the window shows that mechanism directly: on identical event logs
     * the local race repro corrupts 34 of 42 runs at a 10ms window and 0 of 114 at
     * the 100ms default. That measures how the bet loses, not that the default
     * loses it, and no measurement of a delivery outrunning 100ms exists either
     * way. So read this as retiring the bet rather than as repairing an observed
     * failure of that number: the delay is a user-settable env override, which
     * leaves the old behavior one configuration away from losing on any backend.
     *
     * Termination is `hasParkedCommittedDelivery`'s: it counts only deliveries
     * that resolve on their own, so nothing here can gate its own retirement. A
     * genuinely orphaned event has no delivery to wait on and reaches `fn` on the
     * first poll.
     *
     * What the gate gives up: for the ordered events that still reach
     * `onUnconsumedEvent` rather than {@link park}, this stops being the thing
     * that catches a diverged log while a delivery is in flight. The suspension
     * and this check now wake from the same `isDeliveryIdle` edge, and
     * `scheduleWhenIdle` fires on the first timer tick after idle while this waits
     * a further `getDeferredCheckDelayMs()`. So a run with a pending `sleep()`
     * suspends first, and `onWorkflowError` drops the divergence arriving second
     * (its `'suspended'` branch demotes to `'replay'` and surfaces nothing),
     * leaving a later `resume()` to decline into a cold replay. Pre-gate the
     * suspension already won that race whenever the delivery landed inside the
     * fixed window, so what changed is that the outcome stopped depending on
     * timing. Nothing should treat this check as the mechanism that reports
     * divergence on a log the run is still delivering into.
     */
    private whenDeliveryIdle;
}
export {};
//# sourceMappingURL=events-consumer.d.ts.map