/** * Class representing any connection that can be used to send data back and forth, like a WebSocket, * an RTCDataChannel or even just a plain object used to communicate between two scripts. */ export default class DataChannel { /** * @callback MessageCallback * @param {string} message * @returns {void} */ /** * Constructs a new DataChannel endpoint from the given callbacks once the promise resolves. * @param {()=>Promise<{send:(data:string)=>void,onMessage:(callback:MessageCallback)=>void,close:()=>void}>} callback */ constructor(callback){ /** @type {MessageCallback[]} */ this._onMessageCallbacks = []; /** @type {Promise<void>} */ this._readyPromise = new Promise(async resolve=>{ // halts execution of this and puts in the microtask queue so the constructor finishes execution first, before the callback is called, // because otherwise the callback can't access the this object without throwing errors. // timing is difficult, okay? I'm sorry await Promise.resolve(); let c = await callback(); this._send = c.send; c.onMessage(message=>{ this._onMessageCallbacks.slice().forEach(callback=>callback(message)); }); this._close = c.close; resolve(); }); } /** * Waits until the connection is ready. */ async waitUntilReady(){ return this._readyPromise; } /** * Sends the given message to the other endpoint of the connection. * @param {string} message */ async send(message){ await this._readyPromise; this._send(message); } /** * Registers a callback to be called whenever a message is received. * @param {MessageCallback} callback */ onMessage(callback){ this._onMessageCallbacks.push(callback); } /** * Removes a callback registered using `onMessage(callback)`. * @param {MessageCallback} callback */ removeMessageCallback(callback){ let index = this._onMessageCallbacks.indexOf(callback); if (index!==-1){ this._onMessageCallbacks.splice(index,1); } } /** * Returns a promise that eventually resolves to the next message received. * @returns {Promise<string>} */ next(){ return new Promise(resolve=>{ let callback = message=>{ this.removeMessageCallback(callback); resolve(message); } this.onMessage(callback); }); } /** * Closes the connection. */ close(){ this._close(); } } /** * Class representing a connection to a server via a WebSocket. */ export class SocketDataChannel extends DataChannel { /** * Creates a new WebSocket connection to the given url. * @param {string} url */ constructor(url){ super(async()=>{ this._webSocket = new WebSocket(url); /** @type {(message:string)=>void} */ let send = message=>{ this._webSocket.send(message); }; /** @type {(callback:(message:string)=>void)=>void} */ let onMessage = callback=>{ this._webSocket.addEventListener("message",e=>{ callback(e.data+""); }); }; let close = ()=>{ this._webSocket.close(); } await new Promise(resolve=>{ this._webSocket.onopen = resolve; }); return {send,onMessage,close}; }); } }