Skip to content

Commit ff8b184

Browse files
committed
Merge pull request #19 from oortcloud/error-notopened
Add some test and fix a rare network condition that caused a crash
2 parents 0f4243c + e8e7e20 commit ff8b184

File tree

4 files changed

+136
-10
lines changed

4 files changed

+136
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
0.3.3 - 2013-05-29
2+
- fixed bug where an exception could be thrown when sending a message on a socket that is not opened anymore (issue #18)
3+
- added some tests (work in progress)
4+
15
0.3.2 - 2013-04-08
26
- fixed bug where client would reconnect when closing (@tarangp)
37

lib/ddp-client.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ DDPClient = function(opts) {
77
var self = this;
88

99
// default arguments
10+
opts = opts || {};
1011
self.host = opts.host || 'localhost';
1112
self.port = opts.port || 3000;
1213
self.path = opts.path || 'websocket';
@@ -51,16 +52,12 @@ DDPClient.prototype._prepareHandlers = function() {
5152

5253
self.socket.on('error', function(error) {
5354
self.emit('socket-error', error);
54-
if (self.auto_reconnect) {
55-
setTimeout(function() { self.connect(); }, self.auto_reconnect_timer);
56-
}
55+
self._recoverNetworkError();
5756
});
5857

5958
self.socket.on('close', function(code, message) {
6059
self.emit('socket-close', code, message);
61-
if (self.auto_reconnect && ! self._connectionFailed && ! self._isClosing) {
62-
setTimeout(function() { self.connect(); }, self.auto_reconnect_timer);
63-
}
60+
self._recoverNetworkError();
6461
});
6562

6663
self.socket.on('message', function(data, flags) {
@@ -70,11 +67,20 @@ DDPClient.prototype._prepareHandlers = function() {
7067
});
7168
};
7269

70+
DDPClient.prototype._recoverNetworkError = function() {
71+
var self = this;
72+
if (self.auto_reconnect && ! self._connectionFailed && ! self._isClosing) {
73+
setTimeout(function() { self.connect(); }, self.auto_reconnect_timer);
74+
}
75+
}
76+
7377
///////////////////////////////////////////////////////////////////////////
7478
// RAW, low level functions
7579
DDPClient.prototype._send = function(data) {
76-
77-
this.socket.send(JSON.stringify(data));
80+
this.socket.send(JSON.stringify(data), function(error) {
81+
// This callback to avoid an exception being thrown.
82+
// if an error happened, the socket will throw an event and we will recover.
83+
});
7884
};
7985

8086
// handle a message from the server

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ddp",
3-
"version": "0.3.2",
3+
"version": "0.3.3",
44
"description": "Node.js module to connect to servers using DDP protocol.",
55
"author": "Tom Coleman <[email protected]> (http://tom.thesnail.org), Mike Bannister <[email protected]> (http://po.ssibiliti.es)",
66
"main": "lib/ddp-client",
@@ -13,5 +13,13 @@
1313
"ws": ">=0.4.23",
1414
"underscore": ">=1.3.3"
1515
},
16+
"devDependencies": {
17+
"mocha": "1.9.x",
18+
"sinon": "1.7.x",
19+
"rewire": "1.1.x"
20+
},
21+
"scripts": {
22+
"test": "./node_modules/mocha/bin/mocha test"
23+
},
1624
"engines": { "node": "*" }
17-
}
25+
}

test/ddp-client.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
var assert = require('assert'),
2+
sinon = require('sinon'),
3+
rewire = require('rewire'),
4+
events = require('events');
5+
6+
var DDPClient = rewire("../lib/ddp-client");
7+
8+
var wsConstructor, wsMock;
9+
10+
function prepareMocks() {
11+
wsMock = new events.EventEmitter();
12+
13+
wsConstructor = sinon.stub();
14+
wsConstructor.returns(wsMock);
15+
DDPClient.__set__('WebSocket', wsConstructor);
16+
}
17+
18+
19+
describe("Connect to remote server", function() {
20+
beforeEach(function() {
21+
prepareMocks();
22+
});
23+
24+
it('should connect to localhost by default', function() {
25+
new DDPClient().connect();
26+
27+
assert(wsConstructor.calledOnce);
28+
assert(wsConstructor.calledWithNew());
29+
assert(wsConstructor.call)
30+
assert.deepEqual(wsConstructor.args, [['ws://localhost:3000/websocket']]);
31+
});
32+
it('should connect to the provided host', function() {
33+
new DDPClient({'host': 'myserver.com'}).connect();
34+
assert.deepEqual(wsConstructor.args, [['ws://myserver.com:3000/websocket']]);
35+
});
36+
it('should connect to the provided host and port', function() {
37+
new DDPClient({'host': 'myserver.com', 'port': 42}).connect();
38+
assert.deepEqual(wsConstructor.args, [['ws://myserver.com:42/websocket']]);
39+
});
40+
it('should use ssl if the port is 443', function() {
41+
new DDPClient({'host': 'myserver.com', 'port': 443}).connect();
42+
assert.deepEqual(wsConstructor.args, [['wss://myserver.com:443/websocket']]);
43+
});
44+
});
45+
46+
describe('Automatic reconnection', function() {
47+
beforeEach(function() {
48+
prepareMocks();
49+
});
50+
51+
52+
/* We should be able to get this test to work with clock.tick() but for some weird
53+
reasons it does not work. See: https://github.com/cjohansen/Sinon.JS/issues/283
54+
*/
55+
it('should reconnect when the connection fails', function(done) {
56+
var ddpclient = new DDPClient({ auto_reconnect_timer: 10 });
57+
58+
ddpclient.connect();
59+
wsMock.emit('error', {});
60+
61+
// At this point, the constructor should have been called only once.
62+
assert(wsConstructor.calledOnce);
63+
64+
setTimeout(function() {
65+
// Now the constructor should have been called twice
66+
assert(wsConstructor.calledTwice);
67+
done();
68+
}, 15);
69+
});
70+
});
71+
72+
describe("Network errors", function() {
73+
beforeEach(function() {
74+
prepareMocks();
75+
})
76+
77+
// For some weird reasons (hard to reproduce) it happens that we try to send a message and
78+
// get an exception throws at us because the connection is not opened anymore.
79+
it ('should not crash when WebSocket throws a "not-opened" error', function() {
80+
// Simulates the error generated by WebSocket if the connection is not opened:
81+
// WebSocket.send (ws/lib/WebSocket.js:175:16)
82+
wsMock.send = function(data, options, cb) {
83+
if (typeof options == 'function') {
84+
cb = options;
85+
options = {};
86+
}
87+
// if (this.readyState != WebSocket.OPEN)
88+
if (typeof cb == 'function') cb(new Error('not opened'));
89+
else throw new Error('not opened');
90+
};
91+
92+
var ddpclient = new DDPClient();
93+
ddpclient.connect();
94+
95+
var errorCB = sinon.spy();
96+
ddpclient.on('socket-error', errorCB);
97+
var callCB = sinon.spy();
98+
99+
ddpclient.call('aServerMethod', [42], callCB);
100+
101+
// First of all, the previous call should not throw anything.
102+
// The method callback should not be called
103+
// The socket-error event should not be triggered now
104+
// (but it will be triggered later by the socket)
105+
assert(!callCB.calledOnce);
106+
assert(!errorCB.calledOnce);
107+
});
108+
});

0 commit comments

Comments
 (0)