export type CoverSchedulerStatus = "starting" | "active" | "degraded" | "suspended" | "stopped"; export type CoverJobKind = "real" | "control"; export type CoverCellSource = CoverJobKind | "cancel" | "dummy"; type MaybePromise = T | PromiseLike; export interface CoverClock { now(): number; setTimeout(callback: () => void, delayMs: number): unknown; clearTimeout(handle: unknown): void; } /** * The phase is an absolute clock offset. Epoch zero starts at phaseOffsetMs; * negative epoch/cycle indices are valid when a test or monotonic clock starts * before that offset. */ export interface CoverSchedule { phaseOffsetMs: number; coverCadenceMs: number; coverLanes: number; coverFramesPerCell: number; coverDurationEpochs: number; } export interface CoverSlot { readonly deadlineMs: number; readonly cycleIndex: number; readonly cycleStartMs: number; readonly cycleEndMs: number; readonly epochIndex: number; readonly epochInCycle: number; readonly laneIndex: number; readonly frameIndex: number; readonly laneSlotIndex: number; } export interface CoverJobSlot extends CoverSlot { readonly jobId: string; readonly jobKind: CoverJobKind; /** Index of the next not-yet-accepted job cell. */ readonly jobCellIndex: number; } /** * A producer is deliberately lazy: ratchet state and encrypted cell bytes can * be prepared at the scheduled slot instead of buffering a complete cycle. * Returning null substitutes a dummy for this slot without consuming a job * cell. declaredCellCount enforces the authenticated F x D capacity class. */ export interface CoverJob { readonly id: string; readonly kind: CoverJobKind; readonly declaredCellCount: number; readonly priority?: number; nextCell(slot: CoverJobSlot): MaybePromise; /** No further real cells after a selective producer skips its have-set. */ exhausted?(): boolean; cancelCell?(slot: CoverJobSlot): MaybePromise; } export interface CoverJobSummary { readonly id: string; readonly kind: CoverJobKind; readonly declaredCellCount: number; } export interface CoverLaneOpenContext { readonly cycleIndex: number; readonly cycleStartMs: number; readonly cycleEndMs: number; readonly laneIndex: number; readonly job: CoverJobSummary | null; } export interface CoverSendRequest { readonly cell: Uint8Array; readonly source: CoverCellSource; readonly slot: CoverSlot; readonly jobId?: string; readonly jobCellIndex?: number; } export interface CoverLaneCloseContext extends CoverLaneOpenContext { readonly reason: "boundary" | "late-cleanup"; } export interface CoverLane { /** * Return false only when the transport rejected the cell for backpressure. * A thrown/rejected send is treated as a transport failure. */ send(request: CoverSendRequest): MaybePromise; close(context: CoverLaneCloseContext): void; } export type CoverJobOutcome = "completed" | "cancelled-before-admission" | "cancelled" | "failed"; export interface CoverJobResult { readonly jobId: string; readonly outcome: CoverJobOutcome; readonly reason?: "capacity-exhausted" | "stopped" | "key-epoch-changed"; } export interface CoverJobInterruption { readonly jobId: string; readonly reason: "backpressure" | "cell-production-failed" | "lane-open-failed" | "missed-deadline" | "send-failed" | "suspended"; } export interface CoverStatusChange { readonly previous: CoverSchedulerStatus; readonly status: CoverSchedulerStatus; readonly reason?: string; } export interface CoverSchedulerOptions { readonly schedule: CoverSchedule; readonly clock: CoverClock; readonly laneFactory: (context: CoverLaneOpenContext) => CoverLane; readonly makeDummy: (slot: CoverSlot) => MaybePromise; /** * Defaults to zero. A runtime may opt into a small, explicit timer tolerance; * deadlines and metadata remain the absolute unshifted values. */ readonly maxTimerDriftMs?: number; readonly onStatusChange?: (change: CoverStatusChange) => void; readonly onJobResult?: (result: CoverJobResult) => void; readonly onJobInterrupted?: (interruption: CoverJobInterruption) => void; } export type CoverCancelResult = "not-found" | "cancelled-before-admission" | "cancel-pending"; /** * What one enqueued job has actually achieved so far. * * A scheduled sender has NO receiver-side progress signal to poll: scheduled * mode can receive batched chunk receipts, but older peers emit only terminal * completion. Local progress remains useful during admission and receipt lag. * This is therefore the only evidence a send is alive, and it is what * separates "still working" from "stuck" — a distinction a wall-clock bound * cannot make, because a job admitted only at a cycle open waits behind every * other job in the room. */ export interface CoverJobProgress { readonly state: "queued" | "admitted" | "terminal"; /** Index in the admission order, or -1 once admitted (nothing to wait for). */ readonly queuePosition: number; /** Cells this job has put on the wire and had accepted. */ readonly sentCells: number; } export declare class CoverScheduler { readonly schedule: Readonly; private readonly clock; private readonly laneFactory; private readonly makeDummy; private readonly maxTimerDriftMs; private readonly onStatusChange?; private readonly onJobResult?; private readonly onJobInterrupted?; private readonly cycleDurationMs; private readonly slotsPerEpoch; private readonly slotsPerLane; private status; private running; private sequence; private generation; private timer; private hasTimer; private cycle?; private readonly queued; private readonly jobs; constructor(options: CoverSchedulerOptions); getStatus(): CoverSchedulerStatus; getQueuedJobIds(): readonly string[]; /** Queued and admitted application jobs both hold old-epoch message keys. */ hasPendingRealJobs(): boolean; /** Retire old-key producers while keeping this cycle's fixed dummy tail. */ invalidateRealJobsForRekey(): void; /** * Live progress for one job, or undefined once the scheduler no longer holds * it (settled: completed, cancelled or failed — `settle` drops it from the * map). Read-only: polling it never perturbs the fixed schedule. */ getJobProgress(jobId: string): CoverJobProgress | undefined; enqueue(job: CoverJob): void; cancel(jobId: string): CoverCancelResult; /** * Mark a producer complete without adding a terminal frame. Receipt/control * traffic is queued as its own control job; this lane becomes dummy tail. */ complete(jobId: string): boolean; start(): boolean; suspend(reason?: string): boolean; resume(): boolean; /** * Stop is graceful for an already-open cover cycle: sends cease immediately, * but channel close remains pinned to the authenticated cycle boundary. */ stop(): boolean; private assertClockNow; private nextCycleBoundary; private cycleIndexAtBoundary; private sortQueue; private removeQueued; private dequeue; private requeue; private settle; private setStatus; private safeNotify; private disarm; private arm; private armOpenBoundary; private armCycleBoundary; private armSlot; /** * A timer that fired past its own drift tolerance. * * The cycle it belonged to is over: it must be closed and it must NOT be * claimed. What must not happen is parking. Before C5 a late boundary called * setStatus("suspended", "timer-drift") and armed nothing while `running` * stayed true, and "suspended" is left only by resume(), whose callers are * the public method (no src/ caller), visibilitychange -> visible, and * online. A visible, online tab that stalled 500 ms over a boundary * therefore parked scheduled cover permanently: both handshakes stayed * authenticated and the transport stayed connected while every send in both * directions failed at its delivery budget. Degrading instead re-arms, which * is what coverRuntime's own drift comment already promised. * * A scheduler that is already suspended or stopped keeps its state: those * are deliberate, and each has an event or a caller that will leave it. Only * their dead cycle is swept, exactly as before. */ private handleLateTimer; private openCycle; private slotAt; private jobSlot; private requestForSlot; private cacheUnsent; private acceptSent; private sendSlot; /** * Degrade honestly and keep the schedule alive: the cycle closes at its own * authenticated boundary when there still is one, otherwise the gap is swept * and the next strictly-future boundary is armed. This is the only recovery * path a late or failed boundary is allowed to take (C5). * * `statusReason` is the free-form CoverStatusChange reason, which is not the * same vocabulary as the CoverJobInterruption reason handed to requeued jobs; * it defaults to it where they do coincide. */ private degrade; private interruptCycle; private finalizeCycleJobs; private closeCurrentCycle; private handleCycleBoundary; } export declare const coverSlotSpacingMs: (schedule: CoverSchedule) => number; export declare const validateCoverSchedule: (schedule: CoverSchedule) => void; export declare const createCoverScheduler: (options: CoverSchedulerOptions) => CoverScheduler; export {};