/************************************************************************ * Copyright 2010-2015 Brian McKelvey. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ***********************************************************************/ var WebSocketClient = require('./WebSocketClient'); var toBuffer = require('typedarray-to-buffer'); var yaeti = require('yaeti'); const CONNECTING = 0; const OPEN = 1; const CLOSING = 2; const CLOSED = 3; module.exports = W3CWebSocket; function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) { // Make this an EventTarget. yaeti.EventTarget.call(this); // Sanitize clientConfig. clientConfig = clientConfig || {}; clientConfig.assembleFragments = true; // Required in the W3C API. var self = this; this._url = url; this._readyState = CONNECTING; this._protocol = undefined; this._extensions = ''; this._bufferedAmount = 0; // Hack, always 0. this._binaryType = 'arraybuffer'; // TODO: Should be 'blob' by default, but Node has no Blob. // The WebSocketConnection instance. this._connection = undefined; // WebSocketClient instance. this._client = new WebSocketClient(clientConfig); this._client.on('connect', function(connection) { onConnect.call(self, connection); }); this._client.on('connectFailed', function() { onConnectFailed.call(self); }); this._client.connect(url, protocols, origin, headers, requestOptions); } // Expose W3C read only attributes. Object.defineProperties(W3CWebSocket.prototype, { url: { get: function() { return this._url; } }, readyState: { get: function() { return this._readyState; } }, protocol: { get: function() { return this._protocol; } }, extensions: { get: function() { return this._extensions; } }, bufferedAmount: { get: function() { return this._bufferedAmount; } } }); // Expose W3C write/read attributes. Object.defineProperties(W3CWebSocket.prototype, { binaryType: { get: function() { return this._binaryType; }, set: function(type) { // TODO: Just 'arraybuffer' supported. if (type !== 'arraybuffer') { throw new SyntaxError('just "arraybuffer" type allowed for "binaryType" attribute'); } this._binaryType = type; } } }); // Expose W3C readyState constants into the WebSocket instance as W3C states. [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) { Object.defineProperty(W3CWebSocket.prototype, property[0], { get: function() { return property[1]; } }); }); // Also expose W3C readyState constants into the WebSocket class (not defined by the W3C, // but there are so many libs relying on them). [['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) { Object.defineProperty(W3CWebSocket, property[0], { get: function() { return property[1]; } }); }); W3CWebSocket.prototype.send = function(data) { if (this._readyState !== OPEN) { throw new Error('cannot call send() while not connected'); } // Text. if (typeof data === 'string' || data instanceof String) { this._connection.sendUTF(data); } // Binary. else { // Node Buffer. if (data instanceof Buffer) { this._connection.sendBytes(data); } // If ArrayBuffer or ArrayBufferView convert it to Node Buffer. else if (data.byteLength || data.byteLength === 0) { data = toBuffer(data); this._connection.sendBytes(data); } else { throw new Error('unknown binary data:', data); } } }; W3CWebSocket.prototype.close = function(code, reason) { switch(this._readyState) { case CONNECTING: // NOTE: We don't have the WebSocketConnection instance yet so no // way to close the TCP connection. // Artificially invoke the onConnectFailed event. onConnectFailed.call(this); // And close if it connects after a while. this._client.on('connect', function(connection) { if (code) { connection.close(code, reason); } else { connection.close(); } }); break; case OPEN: this._readyState = CLOSING; if (code) { this._connection.close(code, reason); } else { this._connection.close(); } break; case CLOSING: case CLOSED: break; } }; /** * Private API. */ function createCloseEvent(code, reason) { var event = new yaeti.Event('close'); event.code = code; event.reason = reason; event.wasClean = (typeof code === 'undefined' || code === 1000); return event; } function createMessageEvent(data) { var event = new yaeti.Event('message'); event.data = data; return event; } function onConnect(connection) { var self = this; this._readyState = OPEN; this._connection = connection; this._protocol = connection.protocol; this._extensions = connection.extensions; this._connection.on('close', function(code, reason) { onClose.call(self, code, reason); }); this._connection.on('message', function(msg) { onMessage.call(self, msg); }); this.dispatchEvent(new yaeti.Event('open')); } function onConnectFailed() { destroy.call(this); this._readyState = CLOSED; try { this.dispatchEvent(new yaeti.Event('error')); } finally { this.dispatchEvent(createCloseEvent(1006, 'connection failed')); } } function onClose(code, reason) { destroy.call(this); this._readyState = CLOSED; this.dispatchEvent(createCloseEvent(code, reason || '')); } function onMessage(message) { if (message.utf8Data) { this.dispatchEvent(createMessageEvent(message.utf8Data)); } else if (message.binaryData) { // Must convert from Node Buffer to ArrayBuffer. // TODO: or to a Blob (which does not exist in Node!). if (this.binaryType === 'arraybuffer') { var buffer = message.binaryData; var arraybuffer = new ArrayBuffer(buffer.length); var view = new Uint8Array(arraybuffer); for (var i=0, len=buffer.length; i