Skip to content

Commit

Permalink
Make W3CWebSocket an EventTarget as the W3C states for the WebSocket …
Browse files Browse the repository at this point in the history
…interface (fixes #207).
  • Loading branch information
ibc committed Jul 22, 2015
1 parent 1a90cff commit 6242cc3
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 81 deletions.
115 changes: 42 additions & 73 deletions lib/W3CWebSocket.js
Expand Up @@ -16,6 +16,7 @@

var WebSocketClient = require('./WebSocketClient');
var toBuffer = require('typedarray-to-buffer');
var yaeti = require('yaeti');


const CONNECTING = 0;
Expand All @@ -24,20 +25,19 @@ const CLOSING = 2;
const CLOSED = 3;


function WebSocket(url, protocols, origin, headers, requestOptions, clientConfig) {
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;

// W3C attributes and listeners.
this._listeners = {
onopen: undefined,
onerror: undefined,
onclose: undefined,
onmessage: undefined
};
this._url = url;
this._readyState = CONNECTING;
this._protocol = undefined;
Expand All @@ -63,26 +63,8 @@ function WebSocket(url, protocols, origin, headers, requestOptions, clientConfig
}


// Expose W3C listener setters/getters.
['onopen', 'onerror', 'onclose', 'onmessage'].forEach(function(method) {
Object.defineProperty(WebSocket.prototype, method, {
get: function() {
return this._listeners[method];
},
set: function(listener) {
if (typeof listener === 'function') {
this._listeners[method] = listener;
}
else {
this._listeners[method] = undefined;
}
}
});
});


// Expose W3C read only attributes.
Object.defineProperties(WebSocket.prototype, {
Object.defineProperties(W3CWebSocket.prototype, {
url: { get: function() { return this._url; } },
readyState: { get: function() { return this._readyState; } },
protocol: { get: function() { return this._protocol; } },
Expand All @@ -92,7 +74,7 @@ Object.defineProperties(WebSocket.prototype, {


// Expose W3C write/read attributes.
Object.defineProperties(WebSocket.prototype, {
Object.defineProperties(W3CWebSocket.prototype, {
binaryType: {
get: function() {
return this._binaryType;
Expand All @@ -108,15 +90,23 @@ Object.defineProperties(WebSocket.prototype, {
});


// Expose W3C readyState constants.
// Expose W3C readyState constants into the WebSocket instance as W3C states.
[['CONNECTING',CONNECTING], ['OPEN',OPEN], ['CLOSING',CLOSING], ['CLOSED',CLOSED]].forEach(function(property) {
Object.defineProperty(WebSocket.prototype, property[0], {
Object.defineProperty(W3CWebSocket.prototype, property[0], {
get: function() { return property[1]; }
});
});

// Also expone 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]; }
});
});


WebSocket.prototype.send = function(data) {
W3CWebSocket.prototype.send = function(data) {
if (this._readyState !== OPEN) {
throw new Error('cannot call send() while not connected');
}
Expand All @@ -143,10 +133,10 @@ WebSocket.prototype.send = function(data) {
};


WebSocket.prototype.close = function(code, reason) {
W3CWebSocket.prototype.close = function(code, reason) {
switch(this._readyState) {
case CONNECTING:
// TODO: We don't have the WebSocketConnection instance yet so no
// 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);
Expand Down Expand Up @@ -179,31 +169,23 @@ WebSocket.prototype.close = function(code, reason) {
*/


function OpenEvent(target) {
this.type = 'open';
this.target = target;
}
function createCloseEvent(code, reason) {
var event = new yaeti.Event('close');

event.code = code;
event.reason = reason;
event.wasClean = (typeof code === 'undefined' || code === 1000);

function ErrorEvent(target) {
this.type = 'error';
this.target = target;
return event;
}


function CloseEvent(target, code, reason) {
this.type = 'close';
this.target = target;
this.code = code;
this.reason = reason;
this.wasClean = (typeof code === 'undefined' || code === 1000);
}
function createMessageEvent(data) {
var event = new yaeti.Event('message');

event.data = data;

function MessageEvent(target, data) {
this.type = 'message';
this.target = target;
this.data = data;
return event;
}


Expand All @@ -223,35 +205,33 @@ function onConnect(connection) {
onMessage.call(self, msg);
});

callListener.call(this, 'onopen', new OpenEvent(this));
this.dispatchEvent(new yaeti.Event('open'));
}


function onConnectFailed() {
var self = this;

destroy.call(this);
this._readyState = CLOSED;

// Emit 'close' after 'error' even if 'error' listener throws.
global.setTimeout(function() {
callListener.call(self, 'onclose', new CloseEvent(self, 1006, 'connection failed'));
});
callListener.call(this, 'onerror', new ErrorEvent(this));
try {
this.dispatchEvent(new yaeti.Event('error'));
} finally {
this.dispatchEvent(createCloseEvent(1006, 'connection failed'));
}
}


function onClose(code, reason) {
destroy.call(this);
this._readyState = CLOSED;

callListener.call(this, 'onclose', new CloseEvent(this, code, reason || ''));
this.dispatchEvent(createCloseEvent(code, reason || ''));
}


function onMessage(message) {
if (message.utf8Data) {
callListener.call(this, 'onmessage', new MessageEvent(this, message.utf8Data));
this.dispatchEvent(createMessageEvent(message.utf8Data));
}
else if (message.binaryData) {
// Must convert from Node Buffer to ArrayBuffer.
Expand All @@ -263,7 +243,7 @@ function onMessage(message) {
for (var i=0, len=buffer.length; i<len; ++i) {
view[i] = buffer[i];
}
callListener.call(this, 'onmessage', new MessageEvent(this, arraybuffer));
this.dispatchEvent(createMessageEvent(arraybuffer));
}
}
}
Expand All @@ -275,14 +255,3 @@ function destroy() {
this._connection.removeAllListeners();
}
}


function callListener(method, event) {
var listener = this._listeners[method];
if (listener) {
listener(event);
}
}


module.exports = WebSocket;
52 changes: 44 additions & 8 deletions test/unit/w3cwebsocket.js
Expand Up @@ -4,16 +4,15 @@ var test = require('tape');
var WebSocket = require('../../lib/W3CWebSocket');
var startEchoServer = require('../shared/start-echo-server');

test('W3CWebSockets', function(t) {
test('W3CWebSockets adding event listeners with ws.onxxxxx', function(t) {
var counter = 0;

var message = 'This is a test message.';

startEchoServer(function(err, echoServer) {
if (err) { return t.fail('Unable to start echo server: ' + err); }

var ws = new WebSocket('ws://localhost:8080/');

ws.onopen = function() {
t.equal(++counter, 1, 'onopen should be called first');

Expand All @@ -24,17 +23,54 @@ test('W3CWebSockets', function(t) {
};
ws.onmessage = function(event) {
t.equal(++counter, 2, 'onmessage should be called second');

t.equal(event.data, message, 'Received message data should match sent message data.');

ws.close();
};
ws.onclose = function(event) {
t.equal(++counter, 3, 'onclose should be called last');

echoServer.kill();

t.end();
};
});
});

test('W3CWebSockets adding event listeners with ws.addEventListener', function(t) {
var counter = 0;
var message = 'This is a test message.';

startEchoServer(function(err, echoServer) {
if (err) { return t.fail('Unable to start echo server: ' + err); }

var ws = new WebSocket('ws://localhost:8080/');

ws.addEventListener('open', function() {
t.equal(++counter, 1, '"open" should be fired first');

ws.send(message);
});
ws.addEventListener('error', function(event) {
t.fail('No errors are expected: ' + event);
});
ws.addEventListener('message', function(event) {
t.equal(++counter, 2, '"message" should be fired second');

t.equal(event.data, message, 'Received message data should match sent message data.');

ws.close();
});
ws.addEventListener('close', function(event) {
t.equal(++counter, 3, '"close" should be fired');
});
ws.addEventListener('close', function(event) {
t.equal(++counter, 4, '"close" should be fired one more time');

echoServer.kill();

t.end();
});
});
});

0 comments on commit 6242cc3

Please sign in to comment.