File size: 3,907 Bytes
30c32c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
 * 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;