1 /************************************************************************
2 * Copyright 2010-2015 Brian McKelvey.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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 ***********************************************************************/
17 var WebSocketClient = require('./WebSocketClient');
18 var toBuffer = require('typedarray-to-buffer');
19 var yaeti = require('yaeti');
28 module.exports = W3CWebSocket;
31 function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) {
32 // Make this an EventTarget.
33 yaeti.EventTarget.call(this);
35 // Sanitize clientConfig.
36 clientConfig = clientConfig || {};
37 clientConfig.assembleFragments = true; // Required in the W3C API.
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.
48 // The WebSocketConnection instance.
49 this._connection = undefined;
51 // WebSocketClient instance.
52 this._client = new WebSocketClient(clientConfig);
54 this._client.on('connect', function(connection) {
55 onConnect.call(self, connection);
58 this._client.on('connectFailed', function() {
59 onConnectFailed.call(self);
62 this._client.connect(url, protocols, origin, headers, requestOptions);
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; } }
76 // Expose W3C write/read attributes.
77 Object.defineProperties(W3CWebSocket.prototype, {
80 return this._binaryType;
83 // TODO: Just 'arraybuffer' supported.
84 if (type !== 'arraybuffer') {
85 throw new SyntaxError('just "arraybuffer" type allowed for "binaryType" attribute');
87 this._binaryType = type;
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]; }
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]; }
109 W3CWebSocket.prototype.send = function(data) {
110 if (this._readyState !== OPEN) {
111 throw new Error('cannot call send() while not connected');
115 if (typeof data === 'string' || data instanceof String) {
116 this._connection.sendUTF(data);
121 if (data instanceof Buffer) {
122 this._connection.sendBytes(data);
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);
130 throw new Error('unknown binary data:', data);
136 W3CWebSocket.prototype.close = function(code, reason) {
137 switch(this._readyState) {
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) {
146 connection.close(code, reason);
153 this._readyState = CLOSING;
155 this._connection.close(code, reason);
157 this._connection.close();
172 function createCloseEvent(code, reason) {
173 var event = new yaeti.Event('close');
176 event.reason = reason;
177 event.wasClean = (typeof code === 'undefined' || code === 1000);
183 function createMessageEvent(data) {
184 var event = new yaeti.Event('message');
192 function onConnect(connection) {
195 this._readyState = OPEN;
196 this._connection = connection;
197 this._protocol = connection.protocol;
198 this._extensions = connection.extensions;
200 this._connection.on('close', function(code, reason) {
201 onClose.call(self, code, reason);
204 this._connection.on('message', function(msg) {
205 onMessage.call(self, msg);
208 this.dispatchEvent(new yaeti.Event('open'));
212 function onConnectFailed() {
214 this._readyState = CLOSED;
217 this.dispatchEvent(new yaeti.Event('error'));
219 this.dispatchEvent(createCloseEvent(1006, 'connection failed'));
224 function onClose(code, reason) {
226 this._readyState = CLOSED;
228 this.dispatchEvent(createCloseEvent(code, reason || ''));
232 function onMessage(message) {
233 if (message.utf8Data) {
234 this.dispatchEvent(createMessageEvent(message.utf8Data));
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) {
246 this.dispatchEvent(createMessageEvent(arraybuffer));
253 this._client.removeAllListeners();
254 if (this._connection) {
255 this._connection.removeAllListeners();