/** * This class provides a ScratchLinkSocket implementation using WebSockets, * attempting to connect with the locally installed Scratch-Link. * * To connect with ScratchLink without WebSockets, you must implement all of the * public methods in this class. * - open() * - close() * - setOn[Open|Close|Error] * - setHandleMessage * - sendMessage(msgObj) * - isOpen() */ class ScratchLinkWebSocket { constructor (type) { this._type = type; this._onOpen = null; this._onClose = null; this._onError = null; this._handleMessage = null; this._ws = null; } open () { if (!(this._onOpen && this._onClose && this._onError && this._handleMessage)) { throw new Error('Must set open, close, message and error handlers before calling open on the socket'); } let pathname; switch (this._type) { case 'BLE': pathname = 'scratch/ble'; break; case 'BT': pathname = 'scratch/bt'; break; default: throw new Error(`Unknown ScratchLink socket Type: ${this._type}`); } // Try ws:// (the new way) and wss:// (the old way) simultaneously. If either connects, close the other. If we // were to try one and fall back to the other on failure, that could mean a delay of 30 seconds or more for // those who need the fallback. // If both connections fail we should report only one error. const setSocket = (socketToUse, socketToClose) => { socketToClose.onopen = socketToClose.onerror = null; socketToClose.close(); this._ws = socketToUse; this._ws.onopen = this._onOpen; this._ws.onclose = this._onClose; this._ws.onerror = this._onError; this._ws.onmessage = this._onMessage.bind(this); }; const ws = new WebSocket(`ws://127.0.0.1:20111/${pathname}`); const wss = new WebSocket(`wss://device-manager.scratch.mit.edu:20110/${pathname}`); const connectTimeout = setTimeout(() => { // neither socket succeeded before the timeout setSocket(ws, wss); this._ws.onerror(new Event('timeout')); }, 15 * 1000); ws.onopen = openEvent => { clearTimeout(connectTimeout); setSocket(ws, wss); this._ws.onopen(openEvent); }; wss.onopen = openEvent => { clearTimeout(connectTimeout); setSocket(wss, ws); this._ws.onopen(openEvent); }; let wsError; let wssError; const errorHandler = () => { // if only one has received an error, we haven't overall failed yet if (wsError && wssError) { clearTimeout(connectTimeout); setSocket(ws, wss); this._ws.onerror(wsError); } }; ws.onerror = errorEvent => { wsError = errorEvent; errorHandler(); }; wss.onerror = errorEvent => { wssError = errorEvent; errorHandler(); }; } close () { this._ws.close(); this._ws = null; } sendMessage (message) { const messageText = JSON.stringify(message); this._ws.send(messageText); } setOnOpen (fn) { this._onOpen = fn; } setOnClose (fn) { this._onClose = fn; } setOnError (fn) { this._onError = fn; } setHandleMessage (fn) { this._handleMessage = fn; } isOpen () { return this._ws && this._ws.readyState === this._ws.OPEN; } _onMessage (e) { const json = JSON.parse(e.data); this._handleMessage(json); } } module.exports = ScratchLinkWebSocket;