UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

195 lines (177 loc) 6.23 kB
import { assert } from "../../../core/assert.js"; import { Transport } from "./Transport.js"; /** * In-process Transport pair for tests and tooling. * * Two `LoopbackTransport` instances are paired via {@link bind_pair}. Calls to * `send` on one place a copy of the bytes into the peer's inbound queue, where * they sit until {@link deliver_all} is called. This gives tests precise control * over packet timing — no hidden setTimeout, no real network. * * Wire-condition simulation is offered as deterministic helpers * ({@link drop_next}, {@link reorder}) rather than randomized parameters; tests * are easier to write and reproduce when they pick which packet to drop, not * "5% of packets". Random conditions can be layered on top by the test if needed. * * Each `send` copies the bytes (because the caller may reuse the source buffer * immediately). Pool-based optimization is reasonable later but not necessary * for correctness. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class LoopbackTransport extends Transport { #peer; #in_queue; #stats; constructor() { super(); // `deliver_all()` empties the queue front-to-back, so packets // arrive in send order, and the queue never drops them. this.reliable = true; this.ordered = true; /** * Peer transport; null until {@link bind_pair} is called. * @type {LoopbackTransport|null} * @private */ this.#peer = null; /** * Inbound queue of pending packets (copies of the bytes the peer sent). * Entries are Uint8Array — already trimmed to packet length. * @type {Uint8Array[]} * @private */ this.#in_queue = []; /** * Stats. * @private */ this.#stats = { bytes_in: 0, bytes_out: 0, packets_in: 0, packets_out: 0, }; } /** * Bind two LoopbackTransports together so they deliver to each other. * * @param {LoopbackTransport} a * @param {LoopbackTransport} b */ static bind_pair(a, b) { assert.ok(a instanceof LoopbackTransport, 'a must be a LoopbackTransport'); assert.ok(b instanceof LoopbackTransport, 'b must be a LoopbackTransport'); a.#peer = b; b.#peer = a; } /** * Send a packet. Bytes are copied into the peer's inbound queue immediately; * delivery to the peer's `onReceive` happens when {@link deliver_all} runs * on the peer. * * @param {Uint8Array} bytes * @param {number} length */ send(bytes, length) { assert.isNonNegativeInteger(length, 'length'); if (this.#peer === null) { // Fire-and-forget when unbound; useful for echoing tests that ignore output. this.#stats.bytes_out += length; this.#stats.packets_out += 1; return; } const copy = new Uint8Array(length); copy.set(bytes.subarray(0, length)); this.#peer.#in_queue.push(copy); this.#stats.bytes_out += length; this.#stats.packets_out += 1; } /** * Deliver every packet in the inbound queue to {@link onReceive}, in order. * Updates inbound stats. Returns the number of packets delivered. * * @returns {number} */ deliver_all() { let delivered = 0; while (this.#in_queue.length > 0) { const packet = this.#in_queue.shift(); this.#stats.bytes_in += packet.length; this.#stats.packets_in += 1; this.onReceive.send2(packet, packet.length); delivered++; } return delivered; } /** * Drop the next `n` queued inbound packets (simulates packet loss). * If fewer than `n` are queued, drops what's available. * * @param {number} n * @returns {number} actual number dropped */ drop_next(n) { assert.isNonNegativeInteger(n, 'n'); const actual = Math.min(n, this.#in_queue.length); this.#in_queue.splice(0, actual); return actual; } /** * Swap two queued inbound packets by index (simulates reorder). * * @param {number} i * @param {number} j */ reorder(i, j) { assert.isNonNegativeInteger(i, 'i'); assert.isNonNegativeInteger(j, 'j'); if (i >= this.#in_queue.length || j >= this.#in_queue.length) { throw new Error(`LoopbackTransport.reorder: index out of range (queue has ${this.#in_queue.length} packets)`); } const tmp = this.#in_queue[i]; this.#in_queue[i] = this.#in_queue[j]; this.#in_queue[j] = tmp; } /** * Number of packets currently waiting to be delivered. * @returns {number} */ queued_count() { return this.#in_queue.length; } /** * Clear the queue and unbind. Subsequent sends become no-ops. The peer is * also unbound so its `send()` calls don't keep accumulating into our * (now-undrained) inbound queue. The peer's own inbound queue is left * alone — the caller can still `deliver_all()` on the peer if needed. */ disconnect() { this.#in_queue.length = 0; if (this.#peer !== null) { const p = this.#peer; this.#peer = null; if (p.#peer === this) { p.#peer = null; // Notify the peer's session-level disconnect listeners // so test harnesses can drive reconnect flows // deterministically through `disconnect()`. p.onDisconnect.send1('peer_disconnected'); } } this.onDisconnect.send1('local_disconnect'); } /** * Read-only stats snapshot. * @returns {{bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}} */ getStats() { return { bytes_in: this.#stats.bytes_in, bytes_out: this.#stats.bytes_out, packets_in: this.#stats.packets_in, packets_out: this.#stats.packets_out, }; } }