Websocket
[VSoRC/.git] / node_modules / websocket / lib / W3CWebSocket.js
1 /************************************************************************
2  *  Copyright 2010-2015 Brian McKelvey.
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  ***********************************************************************/
16
17 var WebSocketClient = require('./WebSocketClient');
18 var toBuffer = require('typedarray-to-buffer');
19 var yaeti = require('yaeti');
20
21
22 const CONNECTING = 0;
23 const OPEN = 1;
24 const CLOSING = 2;
25 const CLOSED = 3;
26
27
28 module.exports = W3CWebSocket;
29
30
31 function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) {
32     // Make this an EventTarget.
33     yaeti.EventTarget.call(this);
34
35     // Sanitize clientConfig.
36     clientConfig = clientConfig || {};
37     clientConfig.assembleFragments = true;  // Required in the W3C API.
38
39     var self = this;
40
41     this._url = url;
42     this._readyState = CONNECTING;
43     this._protocol = undefined;
44     this._extensions = '';
45     this._bufferedAmount = 0;  // Hack, always 0.
46     this._binaryType = 'arraybuffer';  // TODO: Should be 'blob' by default, but Node has no Blob.
47
48     // The WebSocketConnection instance.
49     this._connection = undefined;
50
51     // WebSocketClient instance.
52     this._client = new WebSocketClient(clientConfig);
53
54     this._client.on('connect', function(connection) {
55         onConnect.call(self, connection);
56     });
57
58     this._client.on('connectFailed', function() {
59         onConnectFailed.call(self);
60     });
61
62     this._client.connect(url, protocols, origin, headers, requestOptions);
63 }
64
65
66 // Expose W3C read only attributes.
67 Object.defineProperties(W3CWebSocket.prototype, {
68     url:            { get: function() { return this._url;            } },
69     readyState:     { get: function() { return this._readyState;     } },
70     protocol:       { get: function() { return this._protocol;       } },
71     extensions:     { get: function() { return this._extensions;     } },
72     bufferedAmount: { get: function() { return this._bufferedAmount; } }
73 });
74
75
76 // Expose W3C write/read attributes.
77 Object.defineProperties(W3CWebSocket.prototype, {
78     binaryType: {
79         get: function() {
80             return this._binaryType;
81         },
82         set: function(type) {
83             // TODO: Just 'arraybuffer' supported.
84             if (type !== 'arraybuffer') {
85                 throw new SyntaxError('just "arraybuffer" type allowed for "binaryType" attribute');
86             }
87             this._binaryType = type;
88         }
89     }
90 });
91
92
93 // Expose W3C readyState constants into the WebSocket instance as W3C states.
94 [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
95     Object.defineProperty(W3CWebSocket.prototype, property[0], {
96         get: function() { return property[1]; }
97     });
98 });
99
100 // Also expose W3C readyState constants into the WebSocket class (not defined by the W3C,
101 // but there are so many libs relying on them).
102 [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
103     Object.defineProperty(W3CWebSocket, property[0], {
104         get: function() { return property[1]; }
105     });
106 });
107
108
109 W3CWebSocket.prototype.send = function(data) {
110     if (this._readyState !== OPEN) {
111         throw new Error('cannot call send() while not connected');
112     }
113
114     // Text.
115     if (typeof data === 'string' || data instanceof String) {
116         this._connection.sendUTF(data);
117     }
118     // Binary.
119     else {
120         // Node Buffer.
121         if (data instanceof Buffer) {
122             this._connection.sendBytes(data);
123         }
124         // If ArrayBuffer or ArrayBufferView convert it to Node Buffer.
125         else if (data.byteLength || data.byteLength === 0) {
126             data = toBuffer(data);
127             this._connection.sendBytes(data);
128         }
129         else {
130             throw new Error('unknown binary data:', data);
131         }
132     }
133 };
134
135
136 W3CWebSocket.prototype.close = function(code, reason) {
137     switch(this._readyState) {
138         case CONNECTING:
139             // NOTE: We don't have the WebSocketConnection instance yet so no
140             // way to close the TCP connection.
141             // Artificially invoke the onConnectFailed event.
142             onConnectFailed.call(this);
143             // And close if it connects after a while.
144             this._client.on('connect', function(connection) {
145                 if (code) {
146                     connection.close(code, reason);
147                 } else {
148                     connection.close();
149                 }
150             });
151             break;
152         case OPEN:
153             this._readyState = CLOSING;
154             if (code) {
155                 this._connection.close(code, reason);
156             } else {
157                 this._connection.close();
158             }
159             break;
160         case CLOSING:
161         case CLOSED:
162             break;
163     }
164 };
165
166
167 /**
168  * Private API.
169  */
170
171
172 function createCloseEvent(code, reason) {
173     var event = new yaeti.Event('close');
174
175     event.code = code;
176     event.reason = reason;
177     event.wasClean = (typeof code === 'undefined' || code === 1000);
178
179     return event;
180 }
181
182
183 function createMessageEvent(data) {
184     var event = new yaeti.Event('message');
185
186     event.data = data;
187
188     return event;
189 }
190
191
192 function onConnect(connection) {
193     var self = this;
194
195     this._readyState = OPEN;
196     this._connection = connection;
197     this._protocol = connection.protocol;
198     this._extensions = connection.extensions;
199
200     this._connection.on('close', function(code, reason) {
201         onClose.call(self, code, reason);
202     });
203
204     this._connection.on('message', function(msg) {
205         onMessage.call(self, msg);
206     });
207
208     this.dispatchEvent(new yaeti.Event('open'));
209 }
210
211
212 function onConnectFailed() {
213     destroy.call(this);
214     this._readyState = CLOSED;
215
216     try {
217         this.dispatchEvent(new yaeti.Event('error'));
218     } finally {
219         this.dispatchEvent(createCloseEvent(1006, 'connection failed'));
220     }
221 }
222
223
224 function onClose(code, reason) {
225     destroy.call(this);
226     this._readyState = CLOSED;
227
228     this.dispatchEvent(createCloseEvent(code, reason || ''));
229 }
230
231
232 function onMessage(message) {
233     if (message.utf8Data) {
234         this.dispatchEvent(createMessageEvent(message.utf8Data));
235     }
236     else if (message.binaryData) {
237         // Must convert from Node Buffer to ArrayBuffer.
238         // TODO: or to a Blob (which does not exist in Node!).
239         if (this.binaryType === 'arraybuffer') {
240             var buffer = message.binaryData;
241             var arraybuffer = new ArrayBuffer(buffer.length);
242             var view = new Uint8Array(arraybuffer);
243             for (var i=0, len=buffer.length; i<len; ++i) {
244                 view[i] = buffer[i];
245             }
246             this.dispatchEvent(createMessageEvent(arraybuffer));
247         }
248     }
249 }
250
251
252 function destroy() {
253     this._client.removeAllListeners();
254     if (this._connection) {
255         this._connection.removeAllListeners();
256     }
257 }