UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

153 lines (130 loc) 4.42 kB
import { assert } from "../../../core/assert.js"; /** * Estimates the clock offset between this peer and a remote peer using the * NTP four-timestamp protocol. * * Each round trip provides four timestamps: * t0: local time when the request was sent * t1: remote time when the request was received * t2: remote time when the response was sent * t3: local time when the response was received * * From these: * `RTT = (t3 - t0) - (t2 - t1)` — excludes remote processing time * `offset = ((t1 - t0) + (t2 - t3)) / 2` — remote_time = local_time + offset * * Time units are application-defined; the module just does arithmetic. Callers * typically pass milliseconds. * * Reports the offset from the sample with lowest RTT (the standard SNTP estimator — error on offset is bounded by ±RTT/2, so the lowest-RTT sample is the tightest estimate) * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class TimeSync { #rtt_samples; #offset_samples; #sample_cursor; #best_rtt; #best_offset; /** * @param {{ window_size?: number }} [options] */ constructor({ window_size = 8 } = {}) { assert.isPositiveInteger(window_size, 'window_size'); /** * @readonly * @type {number} */ this.window_size = window_size; // initializing samples to Infinity prevents them from being picked for "best" and saves us having to keep an extra "length/size" count. /** * @type {Float64Array} * @private */ this.#rtt_samples = new Float64Array(window_size).fill(Infinity); /** * @type {Float64Array} * @private */ this.#offset_samples = new Float64Array(window_size).fill(Infinity); /** * * @type {number} * @private */ this.#sample_cursor = 0; /** * @type {number} * @private */ this.#best_rtt = 0; /** * @type {number} * @private */ this.#best_offset = 0; } /** * Record a complete four-timestamp sample. * * @param {number} t0 local time at send * @param {number} t1 remote time at receive * @param {number} t2 remote time at send * @param {number} t3 local time at receive */ record(t0, t1, t2, t3) { assert.isNumber(t0, 't0'); assert.isNumber(t1, 't1'); assert.isNumber(t2, 't2'); assert.isNumber(t3, 't3'); const rtt = (t3 - t0) - (t2 - t1); const offset = ((t1 - t0) + (t2 - t3)) / 2; const sample_index = this.#sample_cursor; // advance the cursor to the next sample this.#sample_cursor = (this.#sample_cursor + 1) % this.window_size; this.#rtt_samples[sample_index] = rtt; this.#offset_samples[sample_index] = offset; // as per SNTP, pick lowest RTT sample // see https://www.rfc-editor.org/rfc/rfc5905.html#section-10 let best_rtt = this.#rtt_samples[0]; let best_offset = this.#offset_samples[0]; const sample_count = this.window_size; for (let i = 1; i < sample_count; i++) { if (this.#rtt_samples[i] < best_rtt) { best_rtt = this.#rtt_samples[i]; best_offset = this.#offset_samples[i]; } } this.#best_rtt = best_rtt; this.#best_offset = best_offset; } /** * RTT of the sample whose offset is currently being reported — i.e. the * lowest RTT in the current window. This is the *best-case* round-trip, * not a typical one. Don't use it to size timeouts, jitter buffers, or * render delay; for that, measure observed jitter directly * (see `AdaptiveRenderDelay`). * * @returns {number} */ best_rtt() { return this.#best_rtt; } /** * Estimated `remote_time - local_time`. Add to a local timestamp to get the * estimated remote timestamp. * @returns {number} */ offset() { return this.#best_offset; } /** * Convert a local timestamp to the corresponding estimated remote timestamp. * @param {number} local_time * @returns {number} */ estimate_remote(local_time) { return local_time + this.#best_offset; } }