/**
 * Estimates the snapshot-interpolation render delay (in frames) that the
 * receiver should hold the playhead behind the latest received frame, based
 * on observed inter-arrival jitter.
 *
 * Why adaptive: a fixed delay is a tradeoff between smoothness (large delay
 * absorbs more jitter) and responsiveness (small delay = less perceived lag).
 * If we pick a fixed delay sized for the worst case, we pay that cost even
 * when the network is calm. If we size it for the typical case, we stutter
 * during jitter spikes. Source / Counter-Strike use an adaptive scheme that
 * grows quickly when jitter is observed and decays slowly when the link is
 * stable. This module is a small reusable implementation of that idea.
 *
 * Algorithm (per-frame lateness, not inter-arrival time):
 *   1. Each `record_arrival(now_ms, frame_number)` computes
 *      `lateness = now_ms - frame_number * tick_period_ms`. The absolute value
 *      is meaningless (depends on time origin and one-way latency), but the
 *      *spread* across recent frames captures the jitter.
 *   2. The rolling window holds the last N lateness samples; the recommended
 *      target delay (ms) = `(max(window) - min(window)) * safety_multiplier`.
 *   3. Output ramps UP to a higher target instantly (so a jitter spike is
 *      absorbed at the next sample), and DOWN at most `decay_per_sample_ms`
 *      ms per arrival (so we don't flap when one calm packet follows a spike).
 *   4. The output is clamped to `[min_delay_frames, max_delay_frames]` and
 *      converted to an integer frame count.
 *
 * Why not inter-arrival time: in a tick-driven receiver (e.g. JS engine
 * polling a transport queue at 60 Hz), arrivals are quantized to tick
 * boundaries. Bursts of frames in one packet share the same arrival time;
 * the inter-arrival interval between consecutive applied frames is then
 * either 0 (within a burst — useless) or ~tick_period (next tick — also
 * useless). Massive network jitter shows up not as huge inter-arrival
 * intervals but as `frame_number` deltas wildly out of step with wall time.
 * Tracking lateness directly captures this.
 *
 * **`tick_period_ms` MUST match the sender's actual tick period.** Mismatch
 * is silent and catastrophic: if the sender ticks at 240 Hz but you pass
 * 1000/60 = 16.67 ms, the lateness math drifts ~12.5 ms per frame, the
 * spread saturates within ~30 frames, and the estimator pins to
 * `max_delay_frames` regardless of actual jitter. If your simulation runs
 * on `requestAnimationFrame`, do NOT use the display refresh as your tick
 * period — drive the simulation at a fixed timestep (accumulator pattern)
 * and pass that fixed period here.
 *
 * No allocation per call, no I/O, no engine knowledge.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class AdaptiveRenderDelay {
    /**
     * @param {{
     *   tick_period_ms: number,
     *   min_delay_frames?: number,
     *   max_delay_frames?: number,
     *   history_size?: number,
     *   safety_multiplier?: number,
     *   decay_per_sample_ms?: number,
     *   initial_delay_frames?: number,
     * }} options
     *
     * `safety_multiplier`: how much headroom to add above the observed jitter
     * spread. 2.0 (the default) follows the rule of thumb "render delay
     * should cover 2× the observed jitter spread" and reproduces visibly
     * smooth motion in practice.
     *
     * `decay_per_sample_ms`: how aggressively the recommendation drops once
     * conditions improve. Smaller = stickier (the delay stays high even after
     * a calm period), larger = snappier (the delay drops faster, possibly
     * stuttering if jitter recurs). Default 1 ms / sample is a calm decay
     * that takes ~1 s to drop by 60 ms at 60 Hz arrivals.
     */
    constructor({ tick_period_ms, min_delay_frames, max_delay_frames, history_size, safety_multiplier, decay_per_sample_ms, initial_delay_frames, }: {
        tick_period_ms: number;
        min_delay_frames?: number;
        max_delay_frames?: number;
        history_size?: number;
        safety_multiplier?: number;
        decay_per_sample_ms?: number;
        initial_delay_frames?: number;
    });
    /** @readonly @type {number} */
    readonly tick_period_ms: number;
    /** @readonly @type {number} */
    readonly min_delay_frames: number;
    /** @readonly @type {number} */
    readonly max_delay_frames: number;
    /** @readonly @type {number} */
    readonly safety_multiplier: number;
    /** @readonly @type {number} */
    readonly decay_per_sample_ms: number;
    /**
     * Record an arrival. Call this when a new frame from the sender has been
     * applied locally.
     *
     * @param {number} now_ms wall-clock timestamp (e.g. `performance.now()`)
     * @param {number} frame_number sender's monotonically-advancing frame index
     */
    record_arrival(now_ms: number, frame_number: number): void;
    /**
     * Recommended render delay in frames, integer, clamped to the configured
     * `[min_delay_frames, max_delay_frames]` window.
     *
     * @returns {number}
     */
    delay_frames(): number;
    /**
     * Recommended render delay in milliseconds (the unsmoothed-by-frame-rounding
     * value, useful for logging / HUD readouts).
     * @returns {number}
     */
    delay_ms(): number;
    /**
     * Most recent inter-arrival WALL TIME interval in ms. Returns -1 if fewer
     * than 2 arrivals have been observed. NOTE: this is NOT what the
     * estimator uses internally — it's just exposed for diagnostics / HUD.
     * @returns {number}
     */
    last_interval_ms(): number;
    /**
     * Drop all observed history. Useful after a reconnect or a level transition.
     * Leaves `current_delay_ms` as-is so the displayed delay doesn't snap; it
     * will adjust to new conditions over the next few samples.
     */
    reset(): void;
    #private;
}
//# sourceMappingURL=AdaptiveRenderDelay.d.ts.map