diff --git a/docs/connection2W3CWebSocket.md b/docs/connection2W3CWebSocket.md new file mode 100644 index 00000000..4477b37e --- /dev/null +++ b/docs/connection2W3CWebSocket.md @@ -0,0 +1,43 @@ +connection2w3cwebsocket +============ + +* [Usage](#usage) +* [Limitations](#limitations) + +`var connToW3C = require('websocket').connection2w3cwebsocket` + +Casting a WebSocketConnection into an Implementation of the [W3C WebSocket API](http://www.w3.org/TR/websockets/) for browsers. + +The exposed function lets the developer use the browser *W3C WebSocket API* with a serverside websocket connection: + +```javascript +var connToW3C = require('websocket').connection2w3cwebsocket + +server.on("request", function(request){ + const connection = request.accept(); + const wc3 = connToW3C(connection); + w3c.onmessage = function(event){ console.log(event.data) }; + w3c.send("hello world"); +}); +``` + + +Usage +----------- + +```javascript +connection2w3cwebsocket(connection) +``` + +**connection** is a [WebSocketConnection](./WebSocketConnection.md) + +Limitations +----------- + +* This resulting W3CWebSocket does not fire an open event + +The same as the [W3CWebSocket client](./W3CWebSocket.md) + +* `bufferedAmount` attribute is always 0. +* `binaryType` is "arraybuffer" by default given that "blob" is not supported (Node does not implement the `Blob` class). +* `send()` method allows arguments of type `DOMString`, `ArrayBuffer`, `ArrayBufferView` (`Int8Array`, etc) or Node `Buffer`, but does not allow `Blob`. diff --git a/docs/index.md b/docs/index.md index d42a2508..1312729d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,3 +11,4 @@ Click on one of the classes below to view its API documentation. * [WebSocketRequest](./WebSocketRequest.md) * [WebSocketServer](./WebSocketServer.md) * [W3CWebSocket](./W3CWebSocket.md) +* [connection2W3CWebSocket](./connection2W3CWebSocket.md) diff --git a/lib/W3CWebSocket.js b/lib/W3CWebSocket.js index 44a4ac98..75457612 100644 --- a/lib/W3CWebSocket.js +++ b/lib/W3CWebSocket.js @@ -15,22 +15,23 @@ ***********************************************************************/ var WebSocketClient = require('./WebSocketClient'); -var toBuffer = require('typedarray-to-buffer'); -var yaeti = require('yaeti'); +var util = require('util'); -const CONNECTING = 0; -const OPEN = 1; -const CLOSING = 2; -const CLOSED = 3; +var wrapper = require('./W3CWebSocketWrapper'); +var W3CWebSocketWrapper = wrapper.W3CWebSocketWrapper; +var onConnect = wrapper.onConnect; +var onConnectFailed = wrapper.onConnectFailed; module.exports = W3CWebSocket; function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientConfig) { - // Make this an EventTarget. - yaeti.EventTarget.call(this); + W3CWebSocketWrapper.call(this); + this.addEventListener('close', function(){ + this._client.removeAllListeners(); + }); // Sanitize clientConfig. clientConfig = clientConfig || {}; @@ -39,14 +40,7 @@ function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientCon 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); @@ -62,196 +56,4 @@ function W3CWebSocket(url, protocols, origin, headers, requestOptions, clientCon 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{ + stopServer(); + }); + server.prepare(function(err, wsServer) { + if (err) { + t.fail('Unable to start test server'); + return t.end(); + } + wsServer.once('request', function(request){ + var connection = request.accept(); + var sw3c = connToW3C(connection); + // open gets called within the connToW3C. It cannot be listened to + ++counter; + sw3c.send(message); + sw3c.onerror = function(event) { + t.fail('No errors are expected: ' + event); + }; + sw3c.onmessage = function(event) { + t.equal(++counter, 2, 'onmessage should be called second'); + + t.equal(event.data, message, 'Server received message data should match sent message data.'); + sw3c.close(); + }; + sw3c.onclose = function(event) { + t.equal(++counter, 3, 'onclose should be called last'); + t.end(); + }; + }); + + var cw3c = new WebSocket('ws://localhost:64321/'); + cw3c.onmessage = function(event){ + t.equal(event.data, message, 'Client received message data should match sent message data.'); + cw3c.send(event.data); + }; + }); +}); + +test('conn2W3C adding event listeners with ws.addEventListener', function(t) { + var counter = 0; + var message = 'This is a test message.'; + t.on('end', function(){ + stopServer(); + }); + server.prepare(function(err, wsServer) { + if (err) { + t.fail('Unable to start test server'); + return t.end(); + } + wsServer.once('request', function(request){ + var connection = request.accept(); + var sw3c = connToW3C(connection); + // open gets called within the connToW3C. It cannot be listened to + ++counter; + sw3c.send(message); + sw3c.addEventListener('error', function(event) { + t.fail('No errors are expected: ' + event); + }); + sw3c.addEventListener('message', function(event) { + t.equal(++counter, 2, 'onmessage should be called second'); + + t.equal(event.data, message, 'Server received message data should match sent message data.'); + sw3c.close(); + }); + sw3c.addEventListener('close', function(event) { + t.equal(++counter, 3, 'onclose should be called last'); + t.end(); + }); + }); + + var cw3c = new WebSocket('ws://localhost:64321/'); + cw3c.addEventListener('message', function(event){ + t.equal(event.data, message, 'Client received message data should match sent message data.'); + cw3c.send(event.data); + }); + }); +}); + +test('conn2W3C open event doesn\'t get emitted externally', function(t) { + var message = 'This is a test message.'; + t.on('end', function(){ + stopServer(); + }); + server.prepare(function(err, wsServer) { + if (err) { + t.fail('Unable to start test server'); + return t.end(); + } + wsServer.once('request', function(request){ + var connection = request.accept(); + var sw3c = connToW3C(connection); + sw3c.addEventListener('open', function(){ + t.fail('open event fired'); + }); + sw3c.addEventListener('message', function(event){ + t.equal(event.data, message, 'Server received message data should match sent message data.'); + sw3c.close(); + }); + sw3c.addEventListener('close', function() { + t.end(); + }); + }); + var cw3c = new WebSocket('ws://localhost:64321/'); + setTimeout(function(){ + cw3c.send(message); + }, 100); + }); +});