@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
117 lines (104 loc) • 3.74 kB
JavaScript
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;
}
}