123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- /************************************************************************
- * 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<len; ++i) {
- view[i] = buffer[i];
- }
- this.dispatchEvent(createMessageEvent(arraybuffer));
- }
- }
- }
- function destroy() {
- this._client.removeAllListeners();
- if (this._connection) {
- this._connection.removeAllListeners();
- }
- }
|