UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

117 lines (104 loc) 3.74 kB
import { assert } from "../../../core/assert.js"; import { binarySearchHighIndex } from "../../../core/collection/array/binarySearchHighIndex.js"; /** * Comparator for {@link binarySearchHighIndex}: given a new frame number * and an existing queue entry, returns the standard `< 0 / 0 / > 0` ordering. * Hoisted to module scope so the binary-search helper doesn't allocate a * fresh closure on every push. * @param {number} frame * @param {{frame: number}} entry * @returns {number} */ function jitter_compare_frame(frame, entry) { return frame - entry.frame; } /** * Holds inbound payloads keyed by frame number, releasing them only after they * have aged enough that the consumer has high confidence ordering is settled. * * The delay (`delay_frames`) is the cost we pay to absorb arrival jitter. A * larger delay smooths over more jitter but adds visible latency; the canonical * choice is `~3 * snapshot_interval + small slack` (Glenn Fiedler). * * Generic over payload type — the buffer treats payloads as opaque blobs to * pass through. Common payload types: `Uint8Array` (raw packet), `BinaryBuffer`, * deserialized action records. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class JitterBuffer { #queue; /** * @param {{ delay_frames?: number }} [options] */ constructor({ delay_frames = 3 } = {}) { assert.isNonNegativeInteger(delay_frames, 'delay_frames'); /** @readonly @type {number} */ this.delay_frames = delay_frames; /** * Sorted by frame ascending. * @type {{frame: number, payload: any}[]} * @private */ this.#queue = []; } /** * Insert a payload tagged with its source frame number. Inserts in * frame-sorted order so {@link drain_until} can scan from the front. * * @param {number} frame * @param {*} payload */ push(frame, payload) { assert.isNonNegativeInteger(frame, 'frame'); // The whole point of this class is to absorb jittered arrivals, so // out-of-order pushes are the common case. `binarySearchHighIndex` // gives O(log n) insertion-point lookup and matches the engine's // canonical sorted-insert pattern (see SortedListProjection). const q = this.#queue; const i = binarySearchHighIndex(q, frame, jitter_compare_frame); q.splice(i, 0, { frame, payload }); } /** * Release every payload whose frame is `<= current_frame - delay_frames`. * Callback receives `(frame, payload)` in frame-ascending order. * * @param {number} current_frame * @param {function(number, *): void} callback * @returns {number} number of payloads released */ drain_until(current_frame, callback) { assert.isNonNegativeInteger(current_frame, 'current_frame'); assert.isFunction(callback, 'callback'); const threshold = current_frame - this.delay_frames; let released = 0; const q = this.#queue; while (q.length > 0 && q[0].frame <= threshold) { const entry = q.shift(); callback(entry.frame, entry.payload); released++; } return released; } /** * Discard everything in the buffer (e.g. on disconnect). */ clear() { this.#queue.length = 0; } /** * Number of payloads currently held. * @returns {number} */ size() { return this.#queue.length; } /** * Frame of the oldest payload, or `-1` if empty. * @returns {number} */ earliest_frame() { return this.#queue.length === 0 ? -1 : this.#queue[0].frame; } }