// Primary RTCDataChannel manager. // // - Messages queued while the channel is not yet open. // - If the channel closes while the RTCPeerConnection is still 'connected', // it is recreated automatically (brief-disconnect recovery). // - Both sides can safely call initiate() — the one that creates the channel // triggers onnegotiationneeded; the other side receives ondatachannel. // Use { negotiated: true, id: 0 } if you want to skip the SDP dance. import MWSEEventTarget from '../EventTarget.js'; export default class DataChannel extends MWSEEventTarget { constructor(pc, label = 'mwse') { super(); this._pc = pc; this._label = label; this._ch = null; this._queue = []; this._open = false; // Accept channels offered by the remote side. pc.on('datachannel', ch => { if (ch.label === this._label) this._attach(ch); }); } // Create the channel on this side (makes us the "offering" side for the // channel SDP line; triggers onnegotiationneeded). initiate() { if (this._ch?.readyState === 'open' || this._ch?.readyState === 'connecting') return; const ch = this._pc.createDataChannel(this._label, { ordered: true }); this._attach(ch); return ch; } // Send any JSON-serialisable value or an ArrayBuffer. send(data) { const wire = (typeof data === 'string' || data instanceof ArrayBuffer) ? data : JSON.stringify(data); if (this._open) { this._ch.send(wire); } else { this._queue.push(wire); } } get open() { return this._open; } get state() { return this._ch?.readyState ?? 'closed'; } close() { this._ch?.close(); this._ch = null; this._open = false; } // ---- Internal ------------------------------------------------------- _attach(ch) { this._ch = ch; ch.binaryType = 'arraybuffer'; ch.onopen = () => { this._open = true; this.emit('open'); for (const msg of this._queue) ch.send(msg); this._queue = []; }; ch.onmessage = ({ data }) => { let parsed = data; if (typeof data === 'string') { try { parsed = JSON.parse(data); } catch (_) {} } this.emit('message', parsed); }; ch.onclose = () => { this._open = false; this.emit('close'); // Auto-recreate the channel if the connection is still alive. // (Channels can close without the full connection closing, e.g. on // some mobile browsers when the page is backgrounded briefly.) if (this._pc.connectionState === 'connected') { setTimeout(() => this.initiate(), 800); } }; ch.onerror = err => this.emit('error', err); } }