/** * Structural contract for anything USED as a clock — the loose type Predict * accepts, so bare test fakes may omit the optional members. What a * {@link Room} EXPOSES is the stricter {@link RoomClock}, which additionally * guarantees {@link renderNow}. The default implementation is * {@link RoomClockImpl} (test mocks, alternative RTT estimators, NTP-style * probe-driven clocks can replace it — see {@link Room.clock}). * * PURE TIME + LATENCY: the clock no longer tracks input acks. The input * round-trip (what you sent / what the server processed) lives on the * {@link InputHandle}; the Room feeds the clock a pre-computed RTT sample. */ export interface RoomClockLike { /** Local monotonic clock (ms) — the un-offset base {@link serverNow} is built on. * For SELF-IMPOSED relative gates (a cooldown / fire-rate you started locally): * `now() - lastAction >= COOLDOWN_MS`. A relative measure cancels the clock offset, * so this needs no clock sync and dodges the offset-EMA jitter. Contrast * {@link serverNow}, which you want for server-STAMPED absolute deadlines * (invuln / respawn / buy-phase windows you compare an absolute instant against). */ now(): number; /** Estimated server clock (ms since room start — see {@link RoomClockImpl.serverNow}). */ serverNow(): number; /** Server clock like {@link serverNow}, but on a SLEW-LIMITED **render** * timeline: a clock advanced at 1 ms/ms and servoed gently toward * `serverNow()` (τ ≈ 250 ms). It strips the per-patch offset-EMA wobble * `serverNow()` carries, so DRAWING / dead-reckoning remote entities off * this avoids the `v·Δclock` jitter the raw estimate shows at speed (the * dominant remote-entity stutter). Optional — a clock with no offset noise * (or that doesn't care) may omit it, and Predict falls back to * {@link serverNow}. Do NOT use it for hit stamps / server-stamped * deadlines: those must match the server's rewind target, which the render * timeline deliberately lags during an offset correction. */ renderNow?(): number; /** Last RTT sample (ms). */ rtt(): number; /** EMA-smoothed RTT (ms). Preferred for forward-prediction. */ smoothedRtt(): number; /** Connection *jitter* (ms): RFC 3550-style interarrival jitter — an EMA of how far * each patch's arrival interval strays from the advertised cadence. A connection- * quality signal (~0 on a steady link), independent of the latency level. A custom * clock with no patch cadence can return `0`. `0` until the cadence is known and two patches land. */ jitter(): number; /** Server-encode time (ms since room start) of the MOST RECENT patch — the * raw `sNow` of the last TIMED sample, NOT offset-reconstructed. The state * you currently hold represents the server at this instant, so * `serverNow() − lastServerTime()` is the snapshot's age — the exact * forward horizon for dead-reckoning a remote entity to "now". `0` until * the first sample. */ lastServerTime(): number; /** Server snapshot cadence (`patchRate`, ms) — the interval at which the * server broadcasts state, advertised in the input handshake. On the * jitter-free server-time axis a MOVING field gets a sample every patch, so * interpolation uses this to tell a delta-encoded IDLE gap (collapse it) * from the normal cadence. `0` when unknown (no input room / not advertised). */ patchInterval?(): number; /** Feed the server's snapshot cadence (`patchRate`, ms), decoded once from * the input handshake — the producer side of {@link patchInterval}, and a * Room→clock feed like {@link sample}. Optional: a custom clock that doesn't * drive interpolation idle-gap detection need not implement it. */ setPatchInterval?(milliseconds: number): void; /** Feed a decoded TIMED sample: `sNow` (ms since room start) updates the * clock offset; `rttSample` (ms round-trip from the input ack, or `<0` if * none this packet) updates the RTT estimate. */ sample(sNow: number, rttSample: number): void; } /** * The clock contract `Room.clock` guarantees: {@link RoomClockLike} with * {@link RoomClockLike.renderNow | renderNow} always present, so render code * calls `room.clock.renderNow()` with no optional chaining and no * `serverNow()` fallback. Both built-in clocks ({@link NULL_CLOCK}, * {@link RoomClockImpl}) satisfy it; a custom replacement with no slew state * of its own aliases the estimate: `renderNow() { return this.serverNow(); }`. */ export interface RoomClock extends RoomClockLike { renderNow(): number; } /** * Stub clock returned by {@link Room.clock} until the JOIN_ROOM handshake * reveals whether the room declared input. * * - `serverNow()` falls back to the client's own `performance.now()` — a * monotonic, non-offset-corrected timestamp. Good enough for any consumer * that just wants "a monotonic ms reading." * - `rtt()` / `smoothedRtt()` return `0` (no samples available). * - `sample()` is a no-op. * * Shared, frozen singleton — costs nothing to keep around for rooms that * never call `defineInput()`. The Room replaces it with a real * {@link RoomClockImpl} during handshake when input is declared. */ export declare const NULL_CLOCK: RoomClock; /** * Per-room clock-sync + RTT estimator, fed by the {@link ProtocolModifier.TIMED} * prefix the server prepends to state messages when the room declared input * via `defineInput()`. * * Two independent estimates are tracked: * * - **Clock offset** (`serverNow()`): the delta between server `performance.now()` * and the client's. Seeded by the first sample (offset-only or RTT-valid) * then EMA-smoothed. Used so client-side comparisons against * server-stamped deadlines (`invulnUntil`, `hitTime`, etc.) line up. * * - **Round-trip time** (`rtt()` / `smoothedRtt()`): computed by correlating * the server-echoed `lastInputSeq` with the client's own send-time table. * Seeded *only* by the first RTT-valid sample (separate from the offset * seed) — otherwise the first valid sample would EMA-blend from 0 and * strand the smoothed value at ~10% of reality, after which the outlier * guard would reject every subsequent real sample. * * Doesn't know about transports, schemas, input, or Room internals — the Room * calls {@link sample} when a TIMED prefix arrives, passing a pre-computed RTT * sample (the input round-trip is tracked by the InputHandle). Pure math. */ export declare class RoomClockImpl implements RoomClock { /** Default exponential-smoothing weight for offset + RTT EMA. */ private static readonly EMA_ALPHA; /** Default slew time-constant (ms) for {@link renderNow}. ~250 ms: offset * corrections smear over a few frames instead of popping, while the render * timeline still tracks `serverNow()` closely in steady state. */ private static readonly RENDER_TAU; /** Gap (ms) past which {@link renderNow} SNAPS to `serverNow()` instead of * slewing. Slewing a large gap (join warmup while the offset EMA is still * converging, a route-change offset jump, a tab-resume stall) would render * a visible standing lag; only wobble-scale gaps get smoothed. */ private static readonly RENDER_SNAP; /** * RTT samples greater than `outlierFactor × smoothedRtt` are rejected. * Catches tab-resume spikes once the smoothed value has converged; the * separate `_rttHasSample` seed prevents this from clamping early * legitimate samples to a stranded baseline. */ private static readonly RTT_OUTLIER_X; /** RFC 3550 jitter EMA gain (1/16) — slower than the RTT/offset EMA so the * reported jitter is a steady readout rather than a per-patch flicker. */ private static readonly JITTER_GAIN; /** Arrival gaps beyond `JITTER_STALL_X ×` the cadence (a tab-resume stall), and * sub-cadence bursts (mult 0), are skipped so they don't spike the jitter EMA. */ private static readonly JITTER_STALL_X; /** * Clock-offset jitter gate. An offset sample is only folded into the EMA when * its RTT is within `RTT_GATE_FACTOR ×` the windowed-minimum RTT — i.e. the * packet traversed near-empty queues, so its `rtt/2` one-way estimate (and * thus the offset) is least corrupted by jitter. Higher-RTT samples carry * proportionally more jitter and are dropped (the offset just holds). This is * the NTP/QUIC pattern: EMA-smooth the value you report, but filter the input * by the low-delay floor — purely a VARIANCE reduction on `serverNow()`, which * is what steadies the stamped reckonTime. The held offset's small bias is * harmless (it cancels: predict + rewind share the stamped instant). */ private static readonly RTT_GATE_FACTOR; /** Sliding window (ms) over which the RTT floor (gate reference) is tracked. * Long enough to hold a good low-jitter sample; on a route change the stale * floor expires within this horizon and the gate re-opens. */ private static readonly RTT_GATE_WINDOW; /** Post-reset warmup: the first `RTT_GATE_WARMUP` RTT-valid samples BYPASS the * gate (pure EMA), so the offset converges at baseline speed after a reset / * reconnect. The gate drops samples, which otherwise stretches convergence — * worst at high RTT, where it showed as an inflated offset.std until settled. * ~3× the EMA time-constant (1/α = 10) ⇒ converged before the gate engages for * steady-state variance reduction. `0` disables the warmup (gate from sample 1). */ private static readonly RTT_GATE_WARMUP; private _clockOffset; private _clockHasSample; private _offsetCount; private _rttFloorT; private _rttFloorV; private _rtt; private _smoothedRtt; private _rttHasSample; private _jitter; private _lastRecvTime; private _lastServerTime; private _patchInterval; private _renderTau; private _renderSn; private _renderSnAt; /** Estimated server clock: **milliseconds since room start** (the server's * `clock.elapsedTime`, reconstructed via the wire `sNow` + local offset). * NOT raw `performance.now()` — a portable integer-ms timeline the server's * own time-keyed logic shares, so client-side reckon stays in phase. * Returns the local clock until the first sample lands. */ serverNow(): number; /** Slew-limited render timeline — see {@link RoomClockLike.renderNow}. * Free-runs at 1 ms/ms and servos toward {@link serverNow} with the * time-constant set by {@link setRenderTau} (default {@link RENDER_TAU}); * `τ ≤ 0` disables the slew and returns `serverNow()` verbatim. Idempotent * within a frame: it advances only on the first call each frame (guarded on * the local clock), so reading it once per tracked entity doesn't over-step * it. */ renderNow(): number; /** Set the {@link renderNow} slew time-constant (ms). `≤ 0` disables slewing * (renderNow == serverNow). Larger = smoother, but offset corrections lag * longer. */ setRenderTau(milliseconds: number): void; /** Local monotonic clock (ms): the client's own reading WITHOUT the server * offset — the base {@link serverNow} adds the offset to. Use it for * self-imposed relative cooldowns (`now() - lastAction >= COOLDOWN_MS`): they * need only a steady rate, not clock sync, so this avoids the offset-EMA * jitter `serverNow()` carries. @see RoomClockLike.now */ now(): number; /** Most recent RTT sample (ms). `0` until the first RTT-valid sample lands. */ rtt(): number; /** EMA-smoothed RTT (ms). `0` until the first RTT-valid sample lands. Prefer this for forward-prediction. */ smoothedRtt(): number; /** Connection jitter (ms): RFC 3550-style interarrival jitter — see * {@link RoomClockLike.jitter}. `0` until the cadence is known and two patches land. */ jitter(): number; /** Server-encode time (raw `sNow`) of the most recent patch. Pair with * {@link serverNow} for the snapshot age (`serverNow() − lastServerTime()`). * `0` until the first sample. */ lastServerTime(): number; /** Server snapshot cadence (`patchRate`, ms); `0` until the handshake * advertises it. @see RoomClockLike.patchInterval */ patchInterval(): number; /** Set the server snapshot cadence (ms), from the input handshake's * advertised `patchRate`. Non-positive values clear it back to `0`. */ setPatchInterval(milliseconds: number): void; /** * Feed a decoded TIMED sample. The input round-trip lives on the * {@link InputHandle} now — the Room hands us a pre-computed RTT sample. * * @param sNow Server clock (ms since room start, `clock.elapsedTime`) * → clock offset. * @param rttSample Round-trip time (ms) for the input ack this packet * carried, or `< 0` if none (no matching send / no input * yet). Filtered + EMA-smoothed here. */ sample(sNow: number, rttSample: number): void; /** * Push an RTT sample into the sliding-window-minimum deque and return the * current windowed-min RTT (the jitter-free floor the offset gate references). * Standard monotonic-deque algorithm — O(1) amortized, the deque holds only * descending "record-low" candidates (typically 1–few entries). */ private pushRttFloor; /** Reset all state. Useful on reconnect when the room rebuilds context. */ reset(): void; }