UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

132 lines (118 loc) 4.8 kB
import { assert } from "../../../../core/assert.js"; import { Transport } from "../Transport.js"; /** * Transport adapter that wraps a browser `RTCDataChannel` configured for * unreliable, unordered delivery — the only sane low-latency channel on the web. * * The data channel must be created with `{ ordered: false, maxRetransmits: 0 }` * and reach `readyState === "open"` before this adapter is used. Channel setup * (negotiating the `RTCPeerConnection`, ICE, signalling) is the application's * responsibility; this adapter only handles byte transport. * * Duck-typed against `RTCDataChannel` so the same adapter works with mocks in * tests. The expected interface is: * - `binaryType` (writable; we set it to `'arraybuffer'`) * - `addEventListener(type, handler)` for `'message'`, `'close'`, `'error'` * - `send(ArrayBuffer)` * - `close()` * - `readyState` (read in some debug paths) * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class WebRTCDataChannelTransport extends Transport { #stats; #on_message; #on_close; #on_error; #disconnect_fired; /** * @param {{ data_channel: RTCDataChannel }} options */ constructor({ data_channel }) { super(); assert.ok(data_channel && typeof data_channel.send === 'function', 'data_channel must implement send()'); /** @type {RTCDataChannel} */ this.data_channel = data_channel; this.data_channel.binaryType = 'arraybuffer'; /** @private */ this.#stats = { bytes_in: 0, bytes_out: 0, packets_in: 0, packets_out: 0 }; // RTCDataChannel may fire 'close' and 'error' separately for the same // failure (browsers vary). Latch on the first to avoid double-dispatch. /** @private */ this.#disconnect_fired = false; /** @private */ this.#on_message = (event) => { // event.data is an ArrayBuffer because we set binaryType above. const ab = event.data; const bytes = new Uint8Array(ab); this.#stats.bytes_in += bytes.length; this.#stats.packets_in += 1; this.onReceive.send2(bytes, bytes.length); }; /** @private */ this.#on_close = () => { if (this.#disconnect_fired) return; this.#disconnect_fired = true; this.onDisconnect.send1('channel_closed'); }; /** @private */ this.#on_error = (event) => { if (this.#disconnect_fired) return; this.#disconnect_fired = true; const reason = event && event.error && event.error.message ? event.error.message : 'channel_error'; this.onDisconnect.send1(reason); }; this.data_channel.addEventListener('message', this.#on_message); this.data_channel.addEventListener('close', this.#on_close); this.data_channel.addEventListener('error', this.#on_error); } /** * @param {Uint8Array} bytes * @param {number} length */ send(bytes, length) { assert.isNonNegativeInteger(length, 'length'); // RTCDataChannel.send wants the exact bytes; if the source view extends past `length`, // we need to slice. The slice copies, which is unavoidable for over-MTU correctness. if (length === 0) return; const payload = (length === bytes.byteLength) ? bytes : bytes.subarray(0, length); // .send accepts ArrayBufferView; pass the typed array directly. this.data_channel.send(payload); this.#stats.bytes_out += length; this.#stats.packets_out += 1; } /** * Close the underlying channel. After this, sends are no-ops at the channel level. * * Locally-initiated disconnect: we suppress the channel's own `close` * event from firing `onDisconnect` again — callers who explicitly * disconnect don't expect a notification echo. */ disconnect() { this.#disconnect_fired = true; this.data_channel.removeEventListener('message', this.#on_message); this.data_channel.removeEventListener('close', this.#on_close); this.data_channel.removeEventListener('error', this.#on_error); try { this.data_channel.close(); } catch (e) { // Channel may already be closed; ignore. } } /** * @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, }; } }