Spaces:
Running
Running
/** | |
* 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 () { | |
if (this.isOpen()) { | |
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; | |