Skip to content

Commit cc0a6d3

Browse files
committed
Merge pull request #51 from arunoda/sockjs-info-support
SockJs info support
2 parents 3826cda + 3a523b2 commit cc0a6d3

File tree

9 files changed

+305
-9
lines changed

9 files changed

+305
-9
lines changed

README.markdown

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ var ddpclient = new DDPClient({
4545
// All properties optional, defaults shown
4646
host : "localhost",
4747
port : 3000,
48-
path : "websocket",
4948
ssl : false,
5049
autoReconnect : true,
5150
autoReconnectTimer : 500,
5251
maintainCollections : true,
53-
ddpVersion : '1' // ['1', 'pre2', 'pre1'] available
52+
ddpVersion : '1', // ['1', 'pre2', 'pre1'] available
53+
// uses the SockJs protocol to create the connection
54+
// this still uses websockets, but allows to get the benefits
55+
// from projects like meteorhacks:cluster
56+
// (for load balancing and service discovery)
57+
// do not use `path` option when you are using useSockJs
58+
useSockJs: true
5459
});
5560

5661
/*
@@ -173,6 +178,22 @@ ddpclient.on('socket-error', function(error) {
173178
var oid = new ddpclient.EJSON.ObjectID();
174179
```
175180

181+
SockJS Mode
182+
===============
183+
184+
By using the `useSockJs` option like below, DDP connection will use [SockJs](https://github.com/sockjs) protocol to establish the WebSocket connection.
185+
186+
```js
187+
var ddpClient = new DDPClient({ useSockJs: true });
188+
```
189+
190+
Meteor server uses SockJs to implement it's DDP server. With this mode, we can get the benefits provided by [meteorhacks:cluster](https://github.com/meteorhacks/cluster). Some of those are load balancing and service discovery.
191+
192+
* For load balancing you don't need to anything.
193+
* For service discovery, just use the `path` option to identify the service you are referring to.
194+
195+
> With this mode, `path` option has a special meaning. So, thing twice before using `path` option when you are using this option.
196+
176197
Unimplemented Features
177198
====
178199
The node DDP client does not implement ordered collections, something that while in the DDP spec has not been implemented in Meteor yet.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This file contains information which helps Meteor properly upgrade your
2+
# app when you run 'meteor update'. You should check it into version control
3+
# with your project.
4+
5+
notices-for-0.9.0
6+
notices-for-0.9.1
7+
0.9.4-platform-file
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This file contains a token that is unique to your project.
2+
# Check it into your repository along with the rest of this directory.
3+
# It can be used for purposes such as:
4+
# - ensuring you don't accidentally deploy one app on top of another
5+
# - providing package authors with aggregated statistics
6+
7+
16h678x1r5dkr01i9xj33
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
server
2+
browser

examples/example.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ var ddpclient = new DDPClient({
66
// All properties optional, defaults shown
77
host : "localhost",
88
port : 3000,
9-
path : "websocket",
109
ssl : false,
1110
autoReconnect : true,
1211
autoReconnectTimer : 500,
1312
maintainCollections : true,
14-
ddpVersion : "1" // ["1", "pre2", "pre1"] available
13+
ddpVersion : "1", // ["1", "pre2", "pre1"] available,
14+
// uses the sockJs protocol to create the connection
15+
// this still uses websockets, but allows to get the benefits
16+
// from projects like meteorhacks:cluster
17+
// (load balancing and service discovery)
18+
// do not use `path` option when you are using useSockJs
19+
useSockJs: true
1520
});
1621

1722
/*

lib/ddp-client.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ var util = require("util");
44
var events = require("events");
55
var WebSocket = require("faye-websocket");
66
var EJSON = require("ddp-ejson");
7+
var request = require('request');
8+
var pathJoin = require('path').join;
79
var _ = require("ddp-underscore-patched");
810

911
var DDPClient = function(opts) {
@@ -21,8 +23,9 @@ var DDPClient = function(opts) {
2123
// default arguments
2224
self.host = opts.host || "localhost";
2325
self.port = opts.port || 3000;
24-
self.path = opts.path || "websocket";
26+
self.path = opts.path;
2527
self.ssl = opts.ssl || self.port === 443;
28+
self.useSockJs = opts.useSockJs || false;
2629
self.autoReconnect = ("autoReconnect" in opts) ? opts.autoReconnect : true;
2730
self.autoReconnectTimer = ("autoReconnectTimer" in opts) ? opts.autoReconnectTimer : 500;
2831
self.maintainCollections = ("maintainCollections" in opts) ? opts.maintainCollections : true;
@@ -309,12 +312,66 @@ DDPClient.prototype.connect = function(connected) {
309312
});
310313
}
311314

312-
// websocket
315+
if (self.useSockJs) {
316+
self._makeSockJSConnection();
317+
} else {
318+
var url = self._buildWsUrl();
319+
self._makeWebSocketConnection(url);
320+
}
321+
};
322+
323+
DDPClient.prototype._makeSockJSConnection = function() {
324+
var self = this;
325+
326+
// do the info hit
327+
var protocol = self.ssl ? "https://" : "http://";
328+
var randomValue = "" + Math.ceil(Math.random() * 9999999);
329+
var path = pathJoin("/", self.path || "", "sockjs/info");
330+
var url = protocol + self.host + ":" + self.port + path;
331+
332+
request.get(url, function(err, res, body) {
333+
if (err) {
334+
self._recoverNetworkError();
335+
} else if (body) {
336+
var info = JSON.parse(body);
337+
if(!info.base_url) {
338+
// no base_url, then use pure WS handling
339+
var url = self._buildWsUrl();
340+
self._makeWebSocketConnection(url);
341+
} else if (info.base_url.indexOf("http") === 0) {
342+
// base url for a different host
343+
var url = info.base_url + "/websocket";
344+
url = url.replace(/^http/, "ws");
345+
self._makeWebSocketConnection(url);
346+
} else {
347+
// base url for the same host
348+
var path = info.base_url + "/websocket";
349+
var url = self._buildWsUrl(path);
350+
self._makeWebSocketConnection(url);
351+
}
352+
} else {
353+
// no body. weird. use pure WS handling
354+
var url = self._buildWsUrl();
355+
self._makeWebSocketConnection(url);
356+
}
357+
});
358+
};
359+
360+
DDPClient.prototype._buildWsUrl = function(path) {
361+
var self = this;
362+
path = path || self.path || "websocket";
313363
var protocol = self.ssl ? "wss://" : "ws://";
314-
self.socket = new WebSocket.Client(protocol + self.host + ":" + self.port + "/" + self.path);
315-
self._prepareHandlers();
364+
var url = protocol + self.host + ":" + self.port;
365+
url += (path.indexOf("/") === 0)? path : "/" + path;
366+
367+
return url;
316368
};
317369

370+
DDPClient.prototype._makeWebSocketConnection = function(url) {
371+
var self = this;
372+
self.socket = new WebSocket.Client(url);
373+
self._prepareHandlers();
374+
};
318375

319376
DDPClient.prototype.close = function() {
320377
var self = this;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"dependencies": {
2727
"ddp-underscore-patched": "0.8.1-2",
2828
"ddp-ejson": "0.8.1-3",
29-
"faye-websocket": "~0.7.1"
29+
"faye-websocket": "~0.7.1",
30+
"request": "2.53.x"
3031
},
3132
"devDependencies": {
3233
"mocha": "1.9.x",

test/ddp-client.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,146 @@ describe('Collection maintenance and observation', function() {
266266
assert.equal(oldval.value, true);
267267
});
268268
});
269+
270+
271+
describe("SockJS", function() {
272+
it("should use direct WS connection if there is a path", function() {
273+
var ddpclient = new DDPClient();
274+
ddpclient._makeWebSocketConnection = sinon.stub();
275+
ddpclient.connect();
276+
277+
assert.ok(ddpclient._makeWebSocketConnection.called);
278+
});
279+
280+
it("should fallback to sockjs if there useSockJS option", function() {
281+
var ddpclient = new DDPClient({ useSockJs: true });
282+
ddpclient._makeSockJSConnection = sinon.stub();
283+
ddpclient.connect();
284+
285+
assert.ok(ddpclient._makeSockJSConnection.called);
286+
});
287+
288+
describe("after info hit", function() {
289+
var request = require("request");
290+
it("should connect to the correct url", function(done) {
291+
var get = function(url, callback) {
292+
assert.equal(url, "http://the-host:9000/sockjs/info");
293+
done();
294+
};
295+
296+
WithRequestGet(get, function() {
297+
var opts = {
298+
host: "the-host",
299+
port: 9000
300+
};
301+
var ddpclient = new DDPClient(opts);
302+
ddpclient._makeSockJSConnection();
303+
});
304+
});
305+
306+
it("should support custom paths", function(done) {
307+
var get = function(url, callback) {
308+
assert.equal(url, "http://the-host:9000/search/sockjs/info");
309+
done();
310+
};
311+
312+
WithRequestGet(get, function() {
313+
var opts = {
314+
host: "the-host",
315+
port: 9000,
316+
path: "search"
317+
};
318+
var ddpclient = new DDPClient(opts);
319+
ddpclient._makeSockJSConnection();
320+
});
321+
});
322+
323+
it("should retry if there is an error", function() {
324+
var error = { message: "error" };
325+
var get = function(url, callback) {
326+
callback(error);
327+
};
328+
329+
WithRequestGet(get, function() {
330+
var ddpclient = new DDPClient();
331+
ddpclient._recoverNetworkError = sinon.stub();
332+
ddpclient._makeSockJSConnection();
333+
assert.ok(ddpclient._recoverNetworkError.called);
334+
});
335+
});
336+
337+
it("should use direct WS if there is no body", function() {
338+
var info = null;
339+
var get = function(url, callback) {
340+
callback(null, null, info);
341+
};
342+
343+
WithRequestGet(get, function() {
344+
var ddpclient = new DDPClient();
345+
ddpclient._makeWebSocketConnection = sinon.stub();
346+
ddpclient._makeSockJSConnection();
347+
348+
var wsUrl = "ws://localhost:3000/websocket";
349+
assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
350+
});
351+
});
352+
353+
it("should use direct WS if there is no base_url", function() {
354+
var info = '{}';
355+
var get = function(url, callback) {
356+
callback(null, null, info);
357+
};
358+
359+
WithRequestGet(get, function() {
360+
var ddpclient = new DDPClient();
361+
ddpclient._makeWebSocketConnection = sinon.stub();
362+
ddpclient._makeSockJSConnection();
363+
364+
var wsUrl = "ws://localhost:3000/websocket";
365+
assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
366+
});
367+
});
368+
369+
it("should use full base url if it's starts with http", function() {
370+
var info = '{"base_url": "https://somepath"}';
371+
var get = function(url, callback) {
372+
callback(null, null, info);
373+
};
374+
375+
WithRequestGet(get, function() {
376+
var ddpclient = new DDPClient();
377+
ddpclient._makeWebSocketConnection = sinon.stub();
378+
ddpclient._makeSockJSConnection();
379+
380+
var wsUrl = "wss://somepath/websocket";
381+
assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
382+
});
383+
});
384+
385+
it("should compute url based on the base_url if it's not starts with http", function() {
386+
var info = '{"base_url": "/somepath"}';
387+
var get = function(url, callback) {
388+
callback(null, null, info);
389+
};
390+
391+
WithRequestGet(get, function() {
392+
var ddpclient = new DDPClient();
393+
ddpclient._makeWebSocketConnection = sinon.stub();
394+
ddpclient._makeSockJSConnection();
395+
396+
var wsUrl = "ws://localhost:3000/somepath/websocket";
397+
assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
398+
});
399+
});
400+
});
401+
});
402+
403+
function WithRequestGet(getFn, fn) {
404+
var request = require("request");
405+
var originalGet = request.get;
406+
request.get = getFn;
407+
408+
fn();
409+
410+
request.get = originalGet;
411+
}

0 commit comments

Comments
 (0)