/** * The three-lane QoS scheduler — S5's outbound egress for one consumer socket. * * The channel carries three lanes in STRICT priority: control/facts > * interactive > bulk (invariant #5). The relay data plane is high-volume `bulk` * traffic; heartbeats, blackboard writes and relay control acks ride control / * interactive. The load-bearing guarantee: a bulk-output storm must NEVER * head-of-line-block a control or interactive frame. * * Two mechanisms enforce it: * * 1. **Strict lane priority.** {@link QosScheduler.flush} drains all control, * then all interactive, then bulk. Control/interactive are never gated, so a * queued heartbeat is emitted ahead of any bulk backlog. Cross-lane order is * DERIVED from S0's {@link compareFrameOrder} (asserted in the tests). * * 2. **Credit-based backpressure on the bulk lane.** A slow consumer grants * credit for how many bulk frames it can accept; the scheduler emits bulk * only while credit remains. With zero credit, bulk buffers (bounded — see * `bulkCapacity`) while control/interactive still flow freely. The buffered * bulk tail is safe to shed because the {@link ReplayRing} retains it for a * later resume-from-offset. */ import { compareFrameOrder, lanePriority } from "../protocol/index.ts"; import type { Frame } from "../protocol/index.ts"; export interface QosSchedulerOptions { /** Where drained frames are emitted (typically the connection's `send`). */ readonly sink: (frame: Frame) => void; /** Initial bulk credit. Default 0 — bulk stays buffered until credit is granted. */ readonly credit?: number; /** * Maximum buffered bulk frames before overflow sheds the oldest. Default 1024. * Shedding is safe: the replay ring retains evicted chunks for resume. Control * and interactive frames are never shed. */ readonly bulkCapacity?: number; } export declare class QosScheduler { #private; constructor(options: QosSchedulerOptions); /** Remaining bulk credit. */ get credit(): number; /** Frames buffered but not yet emitted (across all lanes). */ get pending(): number; /** Bulk frames buffered awaiting credit. */ get pendingBulk(): number; /** How many bulk frames have been shed on overflow over this scheduler's life. */ get shed(): number; /** * Buffer a frame on its lane and drain what is now eligible. Control and * interactive frames drain immediately (never credit-gated); a bulk frame * drains only if credit is available, otherwise it waits (or sheds the oldest * bulk frame if the bulk buffer is full). */ enqueue(frame: Frame): void; /** Grant `n` more bulk credits, then drain any bulk frames that credit now allows. */ grantCredit(n: number): void; /** * Drain eligible frames to the sink in strict lane priority: all control, then * all interactive, then bulk up to the available credit. Control/interactive * are never head-of-line-blocked by bulk backlog or exhausted credit. */ flush(): void; /** Discard every buffered frame (e.g. on subscriber teardown). */ clear(): void; } /** * Re-exported from S0 so callers reason about lane ordering from one source * rather than re-deriving priority. The scheduler's drain order (with unlimited * credit) is asserted equal to a stable sort by {@link compareFrameOrder}. */ export { compareFrameOrder, lanePriority };