├── .gitignore
├── examples
├── example-meteor-server
│ ├── .meteor
│ │ ├── .gitignore
│ │ ├── release
│ │ ├── platforms
│ │ ├── packages
│ │ ├── .finished-upgraders
│ │ ├── .id
│ │ └── versions
│ ├── ddp-test.css
│ ├── ddp-test.html
│ └── ddp-test.js
└── example.js
├── LICENSE
├── package.json
├── CHANGELOG.md
├── README.markdown
├── lib
└── ddp-client.js
└── test
└── ddp-client.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.0.4.2
2 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/ddp-test.css:
--------------------------------------------------------------------------------
1 | /* CSS declarations go here */
2 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | #
3 | # 'meteor add' and 'meteor remove' will edit this file for you,
4 | # but you can also edit it by hand.
5 |
6 | standard-app-packages
7 | autopublish
8 | insecure
9 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
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
8 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/ddp-test.html:
--------------------------------------------------------------------------------
1 |
2 | ddp-test
3 |
4 |
5 |
6 | {{> posts}}
7 |
8 |
9 |
10 | Hello World!
11 |
12 | {{#each posts}}
13 | - {{body}}
14 | {{/each}}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/.id:
--------------------------------------------------------------------------------
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
8 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/ddp-test.js:
--------------------------------------------------------------------------------
1 | Posts = new Meteor.Collection('posts');
2 |
3 | if (Meteor.isClient) {
4 | Template.posts.posts = function () {
5 | return Posts.find();
6 | };
7 | }
8 |
9 | if (Meteor.isServer) {
10 | Meteor.startup(function () {
11 | var postCount = Posts.find().count();
12 | if ( postCount < 10) {
13 | for ( ; postCount < 10; postCount++) {
14 | Posts.insert({
15 | body : Random.secret()
16 | });
17 | }
18 | }
19 | });
20 | }
21 |
22 | Meteor.methods({
23 | deletePosts : function () {
24 | var cursor = Posts.find({}, { limit : 5 });
25 | cursor.forEach(function (post) {
26 | Posts.remove(post._id);
27 | });
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/examples/example-meteor-server/.meteor/versions:
--------------------------------------------------------------------------------
1 | autopublish@1.0.3
2 | autoupdate@1.2.0
3 | base64@1.0.3
4 | binary-heap@1.0.3
5 | blaze@2.1.0
6 | blaze-tools@1.0.3
7 | boilerplate-generator@1.0.3
8 | callback-hook@1.0.3
9 | check@1.0.5
10 | ddp@1.1.0
11 | deps@1.0.7
12 | ejson@1.0.6
13 | fastclick@1.0.3
14 | geojson-utils@1.0.3
15 | html-tools@1.0.4
16 | htmljs@1.0.4
17 | http@1.1.0
18 | id-map@1.0.3
19 | insecure@1.0.3
20 | jquery@1.11.3_2
21 | json@1.0.3
22 | launch-screen@1.0.2
23 | livedata@1.0.13
24 | logging@1.0.7
25 | meteor@1.1.5
26 | meteor-platform@1.2.2
27 | minifiers@1.1.4
28 | minimongo@1.0.7
29 | mobile-status-bar@1.0.3
30 | mongo@1.1.0
31 | observe-sequence@1.0.5
32 | ordered-dict@1.0.3
33 | random@1.0.3
34 | reactive-dict@1.1.0
35 | reactive-var@1.0.5
36 | reload@1.1.3
37 | retry@1.0.3
38 | routepolicy@1.0.5
39 | session@1.1.0
40 | spacebars@1.0.6
41 | spacebars-compiler@1.0.5
42 | standard-app-packages@1.0.5
43 | templating@1.1.0
44 | tracker@1.0.6
45 | ui@1.0.6
46 | underscore@1.0.3
47 | url@1.0.4
48 | webapp@1.2.0
49 | webapp-hashing@1.0.3
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2013-2016 by Tom Coleman
2 |
3 | node-ddp-client is free software released under the MIT/X11 license:
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ddp",
3 | "version": "0.12.1",
4 | "description": "Node.js module to connect to servers using DDP protocol.",
5 | "author": "Tom Coleman (http://tom.thesnail.org)",
6 | "contributors": [
7 | "Thomas Sarlandie (http://www.sarfata.org)",
8 | "Mason Gravitt ",
9 | "Mike Bannister (http://po.ssibiliti.es)",
10 | "Chris Mather (http://eventedmind.com)",
11 | "Tarang Patel",
12 | "Vaughn Iverson ",
13 | "Rony Kubat "
14 | ],
15 | "license": "MIT",
16 | "main": "lib/ddp-client",
17 | "keywords": [
18 | "ddp",
19 | "meteor",
20 | "protocol"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/oortcloud/node-ddp-client.git"
25 | },
26 | "dependencies": {
27 | "ddp-underscore-patched": "0.8.1-2",
28 | "ddp-ejson": "0.8.1-3",
29 | "faye-websocket": "0.11.0",
30 | "request": "2.74.x"
31 | },
32 | "devDependencies": {
33 | "mocha": "~3.0.2",
34 | "sinon": "~1.17.5",
35 | "rewire": "~2.5.2"
36 | },
37 | "scripts": {
38 | "test": "./node_modules/mocha/bin/mocha test"
39 | },
40 | "engines": {
41 | "node": "*"
42 | },
43 | "bugs": "https://github.com/oortcloud/node-ddp-client/issues"
44 | }
45 |
--------------------------------------------------------------------------------
/examples/example.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var DDPClient = require("../lib/ddp-client");
4 |
5 | var ddpclient = new DDPClient({
6 | // All properties optional, defaults shown
7 | host : "localhost",
8 | port : 3000,
9 | ssl : false,
10 | autoReconnect : true,
11 | autoReconnectTimer : 500,
12 | maintainCollections : true,
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,
20 | // Use a full url instead of a set of `host`, `port` and `ssl`
21 | // do not set `useSockJs` option if `url` is used
22 | url: 'wss://example.com/websocket'
23 | });
24 |
25 | /*
26 | * Connect to the Meteor Server
27 | */
28 | ddpclient.connect(function(error, wasReconnect) {
29 | // If autoReconnect is true, this callback will be invoked each time
30 | // a server connection is re-established
31 | if (error) {
32 | console.log("DDP connection error!");
33 | return;
34 | }
35 |
36 | if (wasReconnect) {
37 | console.log("Reestablishment of a connection.");
38 | }
39 |
40 | console.log("connected!");
41 |
42 | setTimeout(function () {
43 | /*
44 | * Call a Meteor Method
45 | */
46 | ddpclient.call(
47 | "deletePosts", // name of Meteor Method being called
48 | ["foo", "bar"], // parameters to send to Meteor Method
49 | function (err, result) { // callback which returns the method call results
50 | console.log("called function, result: " + result);
51 | },
52 | function () { // callback which fires when server has finished
53 | console.log("updated"); // sending any updated documents as a result of
54 | console.log(ddpclient.collections.posts); // calling this method
55 | }
56 | );
57 | }, 3000);
58 |
59 | /*
60 | * Call a Meteor Method while passing in a random seed.
61 | * Added in DDP pre2, the random seed will be used on the server to generate
62 | * repeatable IDs. This allows the same id to be generated on the client and server
63 | */
64 | var Random = require("ddp-random"),
65 | random = Random.createWithSeeds("randomSeed"); // seed an id generator
66 |
67 | ddpclient.callWithRandomSeed(
68 | "createPost", // name of Meteor Method being called
69 | [{ _id : random.id(), // generate the id on the client
70 | body : "asdf" }],
71 | "randomSeed", // pass the same seed to the server
72 | function (err, result) { // callback which returns the method call results
73 | console.log("called function, result: " + result);
74 | },
75 | function () { // callback which fires when server has finished
76 | console.log("updated"); // sending any updated documents as a result of
77 | console.log(ddpclient.collections.posts); // calling this method
78 | }
79 | );
80 |
81 | /*
82 | * Subscribe to a Meteor Collection
83 | */
84 | ddpclient.subscribe(
85 | "posts", // name of Meteor Publish function to subscribe to
86 | [], // any parameters used by the Publish function
87 | function () { // callback when the subscription is complete
88 | console.log("posts complete:");
89 | console.log(ddpclient.collections.posts);
90 | }
91 | );
92 |
93 | /*
94 | * Observe a collection.
95 | */
96 | var observer = ddpclient.observe("posts");
97 | observer.added = function(id) {
98 | console.log("[ADDED] to " + observer.name + ": " + id);
99 | };
100 | observer.changed = function(id, oldFields, clearedFields) {
101 | console.log("[CHANGED] in " + observer.name + ": " + id);
102 | console.log("[CHANGED] old field values: ", oldFields);
103 | console.log("[CHANGED] cleared fields: ", clearedFields);
104 | };
105 | observer.removed = function(id, oldValue) {
106 | console.log("[REMOVED] in " + observer.name + ": " + id);
107 | console.log("[REMOVED] previous value: ", oldValue);
108 | };
109 |
110 | setTimeout(function() { observer.stop(); }, 6000);
111 | });
112 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 0.12.1 — 2016-09-15
2 |
3 | - Update npm dependencies, including request, to fix vulnerability (#89)
4 |
5 | 0.12.0 — 2016-03-31
6 |
7 | - Update npm dependencies, including faye-websocket (#78)
8 | - Catch JSON parsing exceptions (#77)
9 | - Fix adding changed handler on observer creator (#72)
10 |
11 | 0.11.0 — 2015-03-23
12 |
13 | - Allow passing url to websocket connection string (#52)
14 | - Allow passing TLS options to websocket connection (#53)
15 | - Track method calls so it incomplete calls can be handled (#54)
16 |
17 | 0.10.0 - 2015-02-03
18 |
19 | - Add optional SockJS support
20 |
21 | 0.9.4 - 2014-12-22
22 |
23 | - Handle socket errors occurring before connection is established
24 |
25 | 0.9.3 - 2014-12-20
26 |
27 | - Remove event listeners on close
28 |
29 | 0.9.2 - 2014-11-17
30 |
31 | - Observer callback emits changed fields.
32 |
33 | 0.9.1 - 2014-11-11
34 |
35 | - Collection elements now have _id field.
36 |
37 | 0.9.0 - 2014-11-05
38 |
39 | - Added collection observation
40 |
41 | 0.8.2 - 2014-10-11
42 |
43 | - Fixed variable name typo
44 |
45 | 0.8.1 - 2014-09-16
46 |
47 | - Remove debug console.logs
48 |
49 | 0.8.0 - 2014-09-16
50 |
51 | - Bump DDP version to 1
52 | - Fix connect callback handler (#41)
53 | - Change underscored_variable_names to camelCase
54 |
55 | 0.7.0 - 2014-06-23
56 |
57 | - Built-in support for authenticating to Meteor's Accounts system in has
58 | been removed, due to changes in Meteor's Accounts system in 0.8.2
59 | (https://github.com/meteor/meteor/blob/devel/History.md#meteor-accounts).
60 | If you need login support, try https://github.com/vsivsi/ddp-login
61 | - EJSON support now mandatory
62 |
63 | 0.6.0 - 2014-06-08
64 |
65 | - Update collection before emitting `message`.
66 |
67 | 0.5.2 - 2014-06-01
68 |
69 | - Added MIT License
70 |
71 | 0.5.1 - 2014-05-23
72 |
73 | - Switch to MDG-patched version of underscore.
74 |
75 | 0.5.0 - 2014-05-14
76 |
77 | - Use ddp-ejson instead of meteor-ejson. ddp-ejson is a repackage of
78 | Meteor's latest EJSON package
79 | - Use ddp-srp insead of node-srp. ddp-srp is a repackage of Meteor's
80 | latest SRP package
81 | - Added second callback to ddpclient.call, executed when the DDP
82 | `updated` message is received
83 | - Allow automatic EJSON serialization/deserialization of ObjectIDs
84 | - Expose EJSON package to allow for addition of custom EJSON types
85 | - added DDP pre2 support
86 | - DDP version negotiation
87 | - DDP heartbeat support (reply only)
88 | - `ddpclient.callWithRandomSeed` supports client-generated `_id`s
89 |
90 | 0.4.6 - 2014-04-28
91 |
92 | - Return id used when calling subscribe method
93 |
94 | 0.4.5 - 2014-04-24
95 |
96 | - Fix login with password method to return login token
97 |
98 | 0.4.4 - 2014-02-09
99 |
100 | - Fix a bug where if the server responded to an error on the first
101 | step of SRP authentication it was not handled correctly (i.e when
102 | the user is not found)
103 |
104 | 0.4.3 - 2013-12-19
105 |
106 | - Fix bug with socket reconnects tailspinning into an infinite loop
107 | (#30 by @jagill)
108 | - Fix bug when use_ejson was not always set properly by default.
109 | (#29 by @jagill)
110 |
111 | 0.4.2 - 2013-12-14
112 |
113 | - Use EJSON by default (#28)
114 |
115 | 0.4.1 - 2013-12-07
116 |
117 | - Ability to switch off collections monitoring
118 |
119 | 0.4.0 - 2013-12-07
120 |
121 | - Switched to faye-websockets (#26 by @jagill)
122 |
123 | 0.3.6 - 2013-11-07
124 |
125 | - fixed bug with default params when ignoring root certs (in case the
126 | machine doesn't have the cert)
127 | - Added DDP login with SRP authentication
128 |
129 | 0.3.5 - 2013-11-05
130 |
131 | - Added non strict SSL option in case of missing root certificates
132 |
133 | 0.3.4 - 2013-08-28
134 |
135 | - added EJSON support (default is off) with a couple tests
136 |
137 | 0.3.3 - 2013-05-29
138 |
139 | - fixed bug where an exception could be thrown when sending a message on
140 | a socket that is not opened anymore (issue #18)
141 | - added some tests (work in progress)
142 |
143 | 0.3.2 - 2013-04-08
144 |
145 | - fixed bug where client would reconnect when closing (@tarangp)
146 |
147 | 0.3.1 - 2013-04-06
148 |
149 | - added a failed message to the connect callback if version negotiation
150 | fails.
151 |
152 | 0.3.0 - 2013-03-18
153 |
154 | - moved over to DDP-pre1
155 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | Node DDP Client
2 | ===============
3 |
4 | A callback style [DDP](https://github.com/meteor/meteor/blob/devel/packages/livedata/DDP.md) ([Meteor](http://meteor.com/)'s Distributed Data Protocol) node client, originally based alansikora's [node-js_ddp-client](https://github.com/alansikora/node-js_ddp-client) and Meteor's python client. Uses a more callback style approach.
5 |
6 | The client implements version 1 of DDP, as well as fallbacks to pre1 and pre2.
7 |
8 | Installation
9 | ============
10 |
11 | ```
12 | npm install ddp
13 | ```
14 |
15 | Authentication
16 | ==============
17 | Built-in authentication support was removed in ddp 0.7.0 due to changes in Meteor version 0.8.2.
18 |
19 | DDP Authentication is now implemented by [vsivsi/ddp-login](https://github.com/vsivsi/ddp-login).
20 |
21 | A quick and dirty (but insecure) alternative is to use plain-text logins via a method call:
22 |
23 | ```js
24 | // logging in with e-mail
25 | ddpclient.call("login", [
26 | { user : { email : "user@domain.com" }, password : "password" }
27 | ], function (err, result) { ... });
28 |
29 | // logging in with username
30 | ddpclient.call("login", [
31 | { user : { username : "username" }, password : "password" }
32 | ], function (err, result) { ... });
33 | ```
34 |
35 |
36 | Example
37 | =======
38 |
39 | Please see the example in `examples/example.js`. Or here for reference:
40 |
41 | ```js
42 | var DDPClient = require("ddp");
43 |
44 | var ddpclient = new DDPClient({
45 | // All properties optional, defaults shown
46 | host : "localhost",
47 | port : 3000,
48 | ssl : false,
49 | autoReconnect : true,
50 | autoReconnectTimer : 500,
51 | maintainCollections : true,
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,
59 | // Use a full url instead of a set of `host`, `port` and `ssl`
60 | // do not set `useSockJs` option if `url` is used
61 | url: 'wss://example.com/websocket'
62 | });
63 |
64 | /*
65 | * Connect to the Meteor Server
66 | */
67 | ddpclient.connect(function(error, wasReconnect) {
68 | // If autoReconnect is true, this callback will be invoked each time
69 | // a server connection is re-established
70 | if (error) {
71 | console.log('DDP connection error!');
72 | return;
73 | }
74 |
75 | if (wasReconnect) {
76 | console.log('Reestablishment of a connection.');
77 | }
78 |
79 | console.log('connected!');
80 |
81 | setTimeout(function () {
82 | /*
83 | * Call a Meteor Method
84 | */
85 | ddpclient.call(
86 | 'deletePosts', // name of Meteor Method being called
87 | ['foo', 'bar'], // parameters to send to Meteor Method
88 | function (err, result) { // callback which returns the method call results
89 | console.log('called function, result: ' + result);
90 | },
91 | function () { // callback which fires when server has finished
92 | console.log('updated'); // sending any updated documents as a result of
93 | console.log(ddpclient.collections.posts); // calling this method
94 | }
95 | );
96 | }, 3000);
97 |
98 | /*
99 | * Call a Meteor Method while passing in a random seed.
100 | * Added in DDP pre2, the random seed will be used on the server to generate
101 | * repeatable IDs. This allows the same id to be generated on the client and server
102 | */
103 | var Random = require("ddp-random"),
104 | random = Random.createWithSeeds("randomSeed"); // seed an id generator
105 |
106 | ddpclient.callWithRandomSeed(
107 | 'createPost', // name of Meteor Method being called
108 | [{ _id : random.id(), // generate the id on the client
109 | body : "asdf" }],
110 | "randomSeed", // pass the same seed to the server
111 | function (err, result) { // callback which returns the method call results
112 | console.log('called function, result: ' + result);
113 | },
114 | function () { // callback which fires when server has finished
115 | console.log('updated'); // sending any updated documents as a result of
116 | console.log(ddpclient.collections.posts); // calling this method
117 | }
118 | );
119 |
120 | /*
121 | * Subscribe to a Meteor Collection
122 | */
123 | ddpclient.subscribe(
124 | 'posts', // name of Meteor Publish function to subscribe to
125 | [], // any parameters used by the Publish function
126 | function () { // callback when the subscription is complete
127 | console.log('posts complete:');
128 | console.log(ddpclient.collections.posts);
129 | }
130 | );
131 |
132 | /*
133 | * Observe a collection.
134 | */
135 | var observer = ddpclient.observe("posts");
136 | observer.added = function(id) {
137 | console.log("[ADDED] to " + observer.name + ": " + id);
138 | };
139 | observer.changed = function(id, oldFields, clearedFields, newFields) {
140 | console.log("[CHANGED] in " + observer.name + ": " + id);
141 | console.log("[CHANGED] old field values: ", oldFields);
142 | console.log("[CHANGED] cleared fields: ", clearedFields);
143 | console.log("[CHANGED] new fields: ", newFields);
144 | };
145 | observer.removed = function(id, oldValue) {
146 | console.log("[REMOVED] in " + observer.name + ": " + id);
147 | console.log("[REMOVED] previous value: ", oldValue);
148 | };
149 | setTimeout(function() { observer.stop() }, 6000);
150 | });
151 |
152 | /*
153 | * Useful for debugging and learning the ddp protocol
154 | */
155 | ddpclient.on('message', function (msg) {
156 | console.log("ddp message: " + msg);
157 | });
158 |
159 | /*
160 | * Close the ddp connection. This will close the socket, removing it
161 | * from the event-loop, allowing your application to terminate gracefully
162 | */
163 | ddpclient.close();
164 |
165 | /*
166 | * If you need to do something specific on close or errors.
167 | * You can also disable autoReconnect and
168 | * call ddpclient.connect() when you are ready to re-connect.
169 | */
170 | ddpclient.on('socket-close', function(code, message) {
171 | console.log("Close: %s %s", code, message);
172 | });
173 |
174 | ddpclient.on('socket-error', function(error) {
175 | console.log("Error: %j", error);
176 | });
177 |
178 | /*
179 | * You can access the EJSON object used by ddp.
180 | */
181 | var oid = new ddpclient.EJSON.ObjectID();
182 | ```
183 |
184 | SockJS Mode
185 | ===============
186 |
187 | By using the `useSockJs` option like below, DDP connection will use [SockJs](https://github.com/sockjs) protocol to establish the WebSocket connection.
188 |
189 | ```js
190 | var ddpClient = new DDPClient({ useSockJs: true });
191 | ```
192 |
193 | 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.
194 |
195 | * For load balancing you don't need to anything.
196 | * For service discovery, just use the `path` option to identify the service you are referring to.
197 |
198 | > With this mode, `path` option has a special meaning. So, thing twice before using `path` option when you are using this option.
199 |
200 | Unimplemented Features
201 | ====
202 | The node DDP client does not implement ordered collections, something that while in the DDP spec has not been implemented in Meteor yet.
203 |
204 | Thanks
205 | ======
206 |
207 | Many thanks to Alan Sikora for the ddp-client which formed the inspiration for this code.
208 |
209 | Contributions:
210 | * Tom Coleman (@tmeasday)
211 | * Thomas Sarlandie (@sarfata)
212 | * Mason Gravitt (@emgee3)
213 | * Mike Bannister (@possiblities)
214 | * Chris Mather (@eventedmind)
215 | * James Gill (@jagill)
216 | * Vaughn Iverson (@vsivsi)
217 |
--------------------------------------------------------------------------------
/lib/ddp-client.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var util = require("util");
4 | var events = require("events");
5 | var WebSocket = require("faye-websocket");
6 | var EJSON = require("ddp-ejson");
7 | var request = require('request');
8 | var pathJoin = require('path').join;
9 | var _ = require("ddp-underscore-patched");
10 |
11 | var DDPClient = function(opts) {
12 | var self = this;
13 |
14 | opts = opts || {};
15 |
16 | // backwards compatibility
17 | if ("use_ssl" in opts) { opts.ssl = opts.use_ssl; }
18 | if ("auto_reconnect" in opts) { opts.autoReconnect = opts.auto_reconnect; }
19 | if ("auto_reconnect_timer" in opts) { opts.autoReconnectTimer = opts.auto_reconnect_timer; }
20 | if ("maintain_collections" in opts) { opts.maintainCollections = opts.maintain_collections; }
21 | if ("ddp_version" in opts) { opts.ddpVersion = opts.ddp_version; }
22 |
23 | // default arguments
24 | self.host = opts.host || "localhost";
25 | self.port = opts.port || 3000;
26 | self.path = opts.path;
27 | self.ssl = opts.ssl || self.port === 443;
28 | self.tlsOpts = opts.tlsOpts || {};
29 | self.useSockJs = opts.useSockJs || false;
30 | self.autoReconnect = ("autoReconnect" in opts) ? opts.autoReconnect : true;
31 | self.autoReconnectTimer = ("autoReconnectTimer" in opts) ? opts.autoReconnectTimer : 500;
32 | self.maintainCollections = ("maintainCollections" in opts) ? opts.maintainCollections : true;
33 | self.url = opts.url;
34 | // support multiple ddp versions
35 | self.ddpVersion = ("ddpVersion" in opts) ? opts.ddpVersion : "1";
36 | self.supportedDdpVersions = ["1", "pre2", "pre1"];
37 |
38 | // Expose EJSON object, so client can use EJSON.addType(...)
39 | self.EJSON = EJSON;
40 |
41 | // very very simple collections (name -> [{id -> document}])
42 | if (self.maintainCollections) {
43 | self.collections = {};
44 | }
45 |
46 | // internal stuff to track callbacks
47 | self._isConnecting = false;
48 | self._isReconnecting = false;
49 | self._nextId = 0;
50 | self._callbacks = {};
51 | self._updatedCallbacks = {};
52 | self._pendingMethods = {};
53 | self._observers = {};
54 | };
55 |
56 |
57 | DDPClient.ERRORS = {
58 | DISCONNECTED: new Error("DDPClient: Disconnected from DDP server")
59 | };
60 |
61 |
62 | /**
63 | * Inherits from EventEmitter
64 | */
65 | util.inherits(DDPClient, events.EventEmitter);
66 |
67 |
68 | DDPClient.prototype._prepareHandlers = function() {
69 | var self = this;
70 |
71 | self.socket.on("open", function() {
72 | // just go ahead and open the connection on connect
73 | self._send({
74 | msg : "connect",
75 | version : self.ddpVersion,
76 | support : self.supportedDdpVersions
77 | });
78 | });
79 |
80 | self.socket.on("error", function(error) {
81 | // error received before connection was established
82 | if (self._isConnecting) {
83 | self.emit("failed", error.message);
84 | }
85 |
86 | self.emit("socket-error", error);
87 | });
88 |
89 | self.socket.on("close", function(event) {
90 | self.emit("socket-close", event.code, event.reason);
91 | self._endPendingMethodCalls();
92 | self._recoverNetworkError();
93 | });
94 |
95 | self.socket.on("message", function(event) {
96 | self._message(event.data);
97 | self.emit("message", event.data);
98 | });
99 | };
100 |
101 | DDPClient.prototype._clearReconnectTimeout = function() {
102 | var self = this;
103 | if (self.reconnectTimeout) {
104 | clearTimeout(self.reconnectTimeout);
105 | self.reconnectTimeout = null;
106 | }
107 | };
108 |
109 | DDPClient.prototype._recoverNetworkError = function() {
110 | var self = this;
111 | if (self.autoReconnect && ! self._connectionFailed && ! self._isClosing) {
112 | self._clearReconnectTimeout();
113 | self.reconnectTimeout = setTimeout(function() { self.connect(); }, self.autoReconnectTimer);
114 | self._isReconnecting = true;
115 | }
116 | };
117 |
118 | ///////////////////////////////////////////////////////////////////////////
119 | // RAW, low level functions
120 | DDPClient.prototype._send = function(data) {
121 | if (data.msg !== 'connect' && this._isConnecting) {
122 | this._endPendingMethodCalls()
123 | } else {
124 | this.socket.send(
125 | EJSON.stringify(data)
126 | );
127 | }
128 | };
129 |
130 | // handle a message from the server
131 | DDPClient.prototype._message = function(data) {
132 | var self = this;
133 |
134 | data = EJSON.parse(data);
135 |
136 | // TODO: 'addedBefore' -- not yet implemented in Meteor
137 | // TODO: 'movedBefore' -- not yet implemented in Meteor
138 |
139 | if (!data.msg) {
140 | return;
141 |
142 | } else if (data.msg === "failed") {
143 | if (self.supportedDdpVersions.indexOf(data.version) !== -1) {
144 | this.ddpVersion = data.version;
145 | self.connect();
146 | } else {
147 | self.autoReconnect = false;
148 | self.emit("failed", "Cannot negotiate DDP version");
149 | }
150 |
151 | } else if (data.msg === "connected") {
152 | self.session = data.session;
153 | self.emit("connected");
154 |
155 | // method result
156 | } else if (data.msg === "result") {
157 | var cb = self._callbacks[data.id];
158 |
159 | if (cb) {
160 | cb(data.error, data.result);
161 | delete self._callbacks[data.id];
162 | }
163 |
164 | // method updated
165 | } else if (data.msg === "updated") {
166 |
167 | _.each(data.methods, function (method) {
168 | var cb = self._updatedCallbacks[method];
169 | if (cb) {
170 | cb();
171 | delete self._updatedCallbacks[method];
172 | }
173 | });
174 |
175 | // missing subscription
176 | } else if (data.msg === "nosub") {
177 | var cb = self._callbacks[data.id];
178 |
179 | if (cb) {
180 | cb(data.error);
181 | delete self._callbacks[data.id];
182 | }
183 |
184 | // add document to collection
185 | } else if (data.msg === "added") {
186 | if (self.maintainCollections && data.collection) {
187 | var name = data.collection, id = data.id;
188 |
189 | if (! self.collections[name]) { self.collections[name] = {}; }
190 | if (! self.collections[name][id]) { self.collections[name][id] = {}; }
191 |
192 | self.collections[name][id]._id = id;
193 |
194 | if (data.fields) {
195 | _.each(data.fields, function(value, key) {
196 | self.collections[name][id][key] = value;
197 | });
198 | }
199 |
200 | if (self._observers[name]) {
201 | _.each(self._observers[name], function(observer) {
202 | observer.added(id, data.fields);
203 | });
204 | }
205 | }
206 |
207 | // remove document from collection
208 | } else if (data.msg === "removed") {
209 | if (self.maintainCollections && data.collection) {
210 | var name = data.collection, id = data.id;
211 |
212 | if (! self.collections[name][id]) {
213 | return;
214 | }
215 |
216 | var oldValue = self.collections[name][id];
217 |
218 | delete self.collections[name][id];
219 |
220 | if (self._observers[name]) {
221 | _.each(self._observers[name], function(observer) {
222 | observer.removed(id, oldValue);
223 | });
224 | }
225 | }
226 |
227 | // change document in collection
228 | } else if (data.msg === "changed") {
229 | if (self.maintainCollections && data.collection) {
230 | var name = data.collection, id = data.id;
231 |
232 | if (! self.collections[name]) { return; }
233 | if (! self.collections[name][id]) { return; }
234 |
235 | var oldFields = {},
236 | clearedFields = data.cleared || [],
237 | newFields = {};
238 |
239 | if (data.fields) {
240 | _.each(data.fields, function(value, key) {
241 | oldFields[key] = self.collections[name][id][key];
242 | newFields[key] = value;
243 | self.collections[name][id][key] = value;
244 | });
245 | }
246 |
247 | if (data.cleared) {
248 | _.each(data.cleared, function(value) {
249 | delete self.collections[name][id][value];
250 | });
251 | }
252 |
253 | if (self._observers[name]) {
254 | _.each(self._observers[name], function(observer) {
255 | observer.changed(id, oldFields, clearedFields, newFields);
256 | });
257 | }
258 | }
259 |
260 | // subscriptions ready
261 | } else if (data.msg === "ready") {
262 | _.each(data.subs, function(id) {
263 | var cb = self._callbacks[id];
264 | if (cb) {
265 | cb();
266 | delete self._callbacks[id];
267 | }
268 | });
269 |
270 | // minimal heartbeat response for ddp pre2
271 | } else if (data.msg === "ping") {
272 | self._send(
273 | _.has(data, "id") ? { msg : "pong", id : data.id } : { msg : "pong" }
274 | );
275 | }
276 | };
277 |
278 |
279 | DDPClient.prototype._getNextId = function() {
280 | return (this._nextId += 1).toString();
281 | };
282 |
283 |
284 | DDPClient.prototype._addObserver = function(observer) {
285 | if (! this._observers[observer.name]) {
286 | this._observers[observer.name] = {};
287 | }
288 | this._observers[observer.name][observer._id] = observer;
289 | };
290 |
291 |
292 | DDPClient.prototype._removeObserver = function(observer) {
293 | if (! this._observers[observer.name]) { return; }
294 |
295 | delete this._observers[observer.name][observer._id];
296 | };
297 |
298 | //////////////////////////////////////////////////////////////////////////
299 | // USER functions -- use these to control the client
300 |
301 | /* open the connection to the server
302 | *
303 | * connected(): Called when the 'connected' message is received
304 | * If autoReconnect is true (default), the callback will be
305 | * called each time the connection is opened.
306 | */
307 | DDPClient.prototype.connect = function(connected) {
308 | var self = this;
309 | self._isConnecting = true;
310 | self._connectionFailed = false;
311 | self._isClosing = false;
312 |
313 | if (connected) {
314 | self.addListener("connected", function() {
315 | self._clearReconnectTimeout();
316 |
317 | connected(undefined, self._isReconnecting);
318 | self._isConnecting = false;
319 | self._isReconnecting = false;
320 | });
321 | self.addListener("failed", function(error) {
322 | self._isConnecting = false;
323 | self._connectionFailed = true;
324 | connected(error, self._isReconnecting);
325 | });
326 | }
327 |
328 | if (self.useSockJs) {
329 | self._makeSockJSConnection();
330 | } else {
331 | var url = self._buildWsUrl();
332 | self._makeWebSocketConnection(url);
333 | }
334 | };
335 |
336 | DDPClient.prototype._endPendingMethodCalls = function() {
337 | var self = this;
338 | var ids = _.keys(self._pendingMethods);
339 | self._pendingMethods = {};
340 |
341 | ids.forEach(function (id) {
342 | if (self._callbacks[id]) {
343 | self._callbacks[id](DDPClient.ERRORS.DISCONNECTED);
344 | delete self._callbacks[id];
345 | }
346 |
347 | if (self._updatedCallbacks[id]) {
348 | self._updatedCallbacks[id]();
349 | delete self._updatedCallbacks[id];
350 | }
351 | });
352 | };
353 |
354 | DDPClient.prototype._makeSockJSConnection = function() {
355 | var self = this;
356 |
357 | // do the info hit
358 | var protocol = self.ssl ? "https://" : "http://";
359 | var randomValue = "" + Math.ceil(Math.random() * 9999999);
360 | var path = pathJoin("/", self.path || "", "sockjs/info");
361 | var url = protocol + self.host + ":" + self.port + path;
362 |
363 | var requestOpts = { 'url': url, 'agentOptions': self.tlsOpts };
364 |
365 | request.get(requestOpts, function(err, res, body) {
366 | if (err) {
367 | self._recoverNetworkError();
368 | } else if (body) {
369 | var info;
370 | try {
371 | info = JSON.parse(body);
372 | } catch (e) {
373 | console.error(e);
374 | }
375 | if (!info || !info.base_url) {
376 | // no base_url, then use pure WS handling
377 | var url = self._buildWsUrl();
378 | self._makeWebSocketConnection(url);
379 | } else if (info.base_url.indexOf("http") === 0) {
380 | // base url for a different host
381 | var url = info.base_url + "/websocket";
382 | url = url.replace(/^http/, "ws");
383 | self._makeWebSocketConnection(url);
384 | } else {
385 | // base url for the same host
386 | var path = info.base_url + "/websocket";
387 | var url = self._buildWsUrl(path);
388 | self._makeWebSocketConnection(url);
389 | }
390 | } else {
391 | // no body. weird. use pure WS handling
392 | var url = self._buildWsUrl();
393 | self._makeWebSocketConnection(url);
394 | }
395 | });
396 | };
397 |
398 | DDPClient.prototype._buildWsUrl = function(path) {
399 | var self = this;
400 | var url;
401 | path = path || self.path || "websocket";
402 | var protocol = self.ssl ? "wss://" : "ws://";
403 | if (self.url && !self.useSockJs) {
404 | url = self.url;
405 | } else {
406 | url = protocol + self.host + ":" + self.port;
407 | url += (path.indexOf("/") === 0)? path : "/" + path;
408 | }
409 | return url;
410 | };
411 |
412 | DDPClient.prototype._makeWebSocketConnection = function(url) {
413 | var self = this;
414 | self.socket = new WebSocket.Client(url, null, self.tlsOpts);
415 | self._prepareHandlers();
416 | };
417 |
418 | DDPClient.prototype.close = function() {
419 | var self = this;
420 | self._isClosing = true;
421 | self.socket.close();
422 | self.removeAllListeners("connected");
423 | self.removeAllListeners("failed");
424 | };
425 |
426 |
427 | // call a method on the server,
428 | //
429 | // callback = function(err, result)
430 | DDPClient.prototype.call = function(name, params, callback, updatedCallback) {
431 | var self = this;
432 | var id = self._getNextId();
433 |
434 | self._callbacks[id] = function () {
435 | delete self._pendingMethods[id];
436 |
437 | if (callback) {
438 | callback.apply(this, arguments);
439 | }
440 | };
441 |
442 | self._updatedCallbacks[id] = function () {
443 | delete self._pendingMethods[id];
444 |
445 | if (updatedCallback) {
446 | updatedCallback.apply(this, arguments);
447 | }
448 | };
449 |
450 | self._pendingMethods[id] = true;
451 |
452 | self._send({
453 | msg : "method",
454 | id : id,
455 | method : name,
456 | params : params
457 | });
458 | };
459 |
460 |
461 | DDPClient.prototype.callWithRandomSeed = function(name, params, randomSeed, callback, updatedCallback) {
462 | var self = this;
463 | var id = self._getNextId();
464 |
465 | if (callback) {
466 | self._callbacks[id] = callback;
467 | }
468 |
469 | if (updatedCallback) {
470 | self._updatedCallbacks[id] = updatedCallback;
471 | }
472 |
473 | self._send({
474 | msg : "method",
475 | id : id,
476 | method : name,
477 | randomSeed : randomSeed,
478 | params : params
479 | });
480 | };
481 |
482 | // open a subscription on the server, callback should handle on ready and nosub
483 | DDPClient.prototype.subscribe = function(name, params, callback) {
484 | var self = this;
485 | var id = self._getNextId();
486 |
487 | if (callback) {
488 | self._callbacks[id] = callback;
489 | }
490 |
491 | self._send({
492 | msg : "sub",
493 | id : id,
494 | name : name,
495 | params : params
496 | });
497 |
498 | return id;
499 | };
500 |
501 | DDPClient.prototype.unsubscribe = function(id) {
502 | var self = this;
503 |
504 | self._send({
505 | msg : "unsub",
506 | id : id
507 | });
508 | };
509 |
510 | /**
511 | * Adds an observer to a collection and returns the observer.
512 | * Observation can be stopped by calling the stop() method on the observer.
513 | * Functions for added, changed and removed can be added to the observer
514 | * afterward.
515 | */
516 | DDPClient.prototype.observe = function(name, added, changed, removed) {
517 | var self = this;
518 | var observer = {};
519 | var id = self._getNextId();
520 |
521 | // name, _id are immutable
522 | Object.defineProperty(observer, "name", {
523 | get: function() { return name; },
524 | enumerable: true
525 | });
526 |
527 | Object.defineProperty(observer, "_id", { get: function() { return id; }});
528 |
529 | observer.added = added || function(){};
530 | observer.changed = changed || function(){};
531 | observer.removed = removed || function(){};
532 |
533 | observer.stop = function() {
534 | self._removeObserver(observer);
535 | };
536 |
537 | self._addObserver(observer);
538 |
539 | return observer;
540 | };
541 |
542 | module.exports = DDPClient;
543 |
--------------------------------------------------------------------------------
/test/ddp-client.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert'),
2 | sinon = require('sinon'),
3 | rewire = require('rewire'),
4 | events = require('events'),
5 | EJSON = require('ddp-ejson');
6 |
7 | var DDPClient = rewire("../lib/ddp-client");
8 |
9 | var wsConstructor, wsMock;
10 |
11 |
12 | function prepareMocks() {
13 | wsMock = new events.EventEmitter();
14 | wsMock.close = sinon.stub();
15 |
16 | wsConstructor = sinon.stub();
17 | wsConstructor.returns(wsMock);
18 | DDPClient.__set__('WebSocket', { Client: wsConstructor });
19 | }
20 |
21 |
22 | describe("Connect to remote server", function() {
23 | beforeEach(function() {
24 | prepareMocks();
25 | });
26 |
27 | it('should connect to localhost by default', function() {
28 | new DDPClient().connect();
29 |
30 | assert(wsConstructor.calledOnce);
31 | assert(wsConstructor.calledWithNew());
32 | assert(wsConstructor.call);
33 | assert.deepEqual(wsConstructor.args, [['ws://localhost:3000/websocket', null, {}]]);
34 | });
35 |
36 | it('should connect to the provided host', function() {
37 | new DDPClient({'host': 'myserver.com'}).connect();
38 | assert.deepEqual(wsConstructor.args, [['ws://myserver.com:3000/websocket', null, {}]]);
39 | });
40 |
41 | it('should connect to the provided host and port', function() {
42 | new DDPClient({'host': 'myserver.com', 'port': 42}).connect();
43 | assert.deepEqual(wsConstructor.args, [['ws://myserver.com:42/websocket', null, {}]]);
44 | });
45 |
46 | it('should use ssl if the port is 443', function() {
47 | new DDPClient({'host': 'myserver.com', 'port': 443}).connect();
48 | assert.deepEqual(wsConstructor.args, [['wss://myserver.com:443/websocket', null, {}]]);
49 | });
50 |
51 | it('should propagate tls options if specified', function() {
52 | var tlsOpts = {
53 | 'ca': ['fake_pem_content']
54 | }
55 | new DDPClient({'host': 'myserver.com', 'port': 443, 'tlsOpts': tlsOpts}).connect();
56 | assert.deepEqual(wsConstructor.args, [['wss://myserver.com:443/websocket', null, tlsOpts]]);
57 | });
58 |
59 | it('should connect to the provided url', function() {
60 | new DDPClient({'url': 'wss://myserver.com/websocket'}).connect();
61 | assert.deepEqual(wsConstructor.args, [['wss://myserver.com/websocket', null, {} ]]);
62 | });
63 |
64 | it('should fallback to sockjs if url and useSockJs:true are provided', function() {
65 | var ddpclient = new DDPClient({'url': 'wss://myserver.com/websocket', 'useSockJs': true});
66 | ddpclient._makeSockJSConnection = sinon.stub();
67 | ddpclient.connect();
68 | assert.ok(ddpclient._makeSockJSConnection.called);
69 | });
70 |
71 | it('should clear event listeners on close', function(done) {
72 | var ddpclient = new DDPClient();
73 | var callback = sinon.stub();
74 |
75 | ddpclient.connect(callback);
76 | ddpclient.close();
77 | ddpclient.connect(callback);
78 |
79 | setTimeout(function() {
80 | assert.equal(ddpclient.listeners('connected').length, 1);
81 | assert.equal(ddpclient.listeners('failed').length, 1);
82 | done();
83 | }, 15);
84 | });
85 |
86 | it('should call the connection callback when connection is established', function(done) {
87 | var ddpclient = new DDPClient();
88 | var callback = sinon.spy();
89 |
90 | ddpclient.connect(callback);
91 | wsMock.emit('message', { data: '{ "msg": "connected" }' });
92 |
93 | setTimeout(function() {
94 | assert(callback.calledWith(undefined, false));
95 | done();
96 | }, 15);
97 | });
98 |
99 | it('should pass socket errors occurring during connection to the connection callback', function(done) {
100 | var ddpclient = new DDPClient();
101 | var callback = sinon.spy();
102 |
103 | var socketError = "Network error: ws://localhost:3000/websocket: connect ECONNREFUSED";
104 |
105 | ddpclient.connect(callback);
106 | wsMock.emit('error', { message: socketError });
107 |
108 | setTimeout(function() {
109 | assert(callback.calledWith(socketError, false));
110 | done();
111 | }, 15);
112 | });
113 | });
114 |
115 |
116 | describe('Automatic reconnection', function() {
117 | beforeEach(function() {
118 | prepareMocks();
119 | });
120 |
121 | /* We should be able to get this test to work with clock.tick() but for some weird
122 | reasons it does not work. See: https://github.com/cjohansen/Sinon.JS/issues/283
123 | */
124 | it('should reconnect when the connection fails', function(done) {
125 | var ddpclient = new DDPClient({ autoReconnectTimer: 10 });
126 |
127 | ddpclient.connect();
128 | wsMock.emit('close', {});
129 |
130 | // At this point, the constructor should have been called only once.
131 | assert(wsConstructor.calledOnce);
132 |
133 | setTimeout(function() {
134 | // Now the constructor should have been called twice
135 | assert(wsConstructor.calledTwice);
136 | done();
137 | }, 15);
138 | });
139 |
140 | it('should reconnect only once when the connection fails rapidly', function(done) {
141 | var ddpclient = new DDPClient({ autoReconnectTimer: 5 });
142 |
143 | ddpclient.connect();
144 | wsMock.emit('close', {});
145 | wsMock.emit('close', {});
146 | wsMock.emit('close', {});
147 |
148 | // At this point, the constructor should have been called only once.
149 | assert(wsConstructor.calledOnce);
150 |
151 | setTimeout(function() {
152 | // Now the constructor should have been called twice
153 | assert(wsConstructor.calledTwice);
154 | done();
155 | }, 15);
156 | });
157 |
158 | it('should save currently running method calls', function() {
159 | var ddpclient = new DDPClient();
160 | ddpclient._getNextId = sinon.stub().returns('_test');
161 | ddpclient._send = Function.prototype;
162 |
163 | ddpclient.connect();
164 | ddpclient.call();
165 |
166 | assert("_test" in ddpclient._pendingMethods)
167 | });
168 |
169 | it('should remove id when callback is called', function() {
170 | var ddpclient = new DDPClient();
171 | ddpclient._getNextId = sinon.stub().returns('_test');
172 | ddpclient._send = Function.prototype;
173 |
174 | ddpclient.connect();
175 | ddpclient.call();
176 |
177 | assert("_test" in ddpclient._pendingMethods)
178 |
179 | ddpclient._callbacks._test();
180 | assert(!("_test" in ddpclient._pendingMethods))
181 | });
182 |
183 | it('should remove id when updated-callback is called', function() {
184 | var ddpclient = new DDPClient();
185 | ddpclient._getNextId = sinon.stub().returns('_test');
186 | ddpclient._send = Function.prototype;
187 |
188 | ddpclient.connect();
189 | ddpclient.call();
190 |
191 | assert("_test" in ddpclient._pendingMethods)
192 |
193 | ddpclient._updatedCallbacks._test();
194 | assert(!("_test" in ddpclient._pendingMethods))
195 | });
196 |
197 | it('should end method calls which could not be completed', function() {
198 | var ddpclient = new DDPClient();
199 | var callback = sinon.spy();
200 | var updatedCallback = sinon.spy();
201 |
202 | ddpclient._pendingMethods = { _test: true };
203 | ddpclient._callbacks = { _test: callback };
204 | ddpclient._updatedCallbacks = { _test: updatedCallback };
205 |
206 | ddpclient.connect();
207 | ddpclient.socket.emit('close', {});
208 |
209 | assert(callback.calledOnce);
210 | assert(callback.calledWithExactly(DDPClient.ERRORS.DISCONNECTED));
211 |
212 | assert(updatedCallback.calledOnce);
213 |
214 | // callbacks should be removed after calling them
215 | assert(!("_test" in ddpclient._callbacks));
216 | assert(!("_test" in ddpclient._updatedCallbacks));
217 | assert(!("_test" in ddpclient._pendingMethods));
218 | });
219 | });
220 |
221 |
222 | describe('EJSON', function() {
223 | var DDPMessage = '{"msg":"added","collection":"posts","id":"2trpvcQ4pn32ZYXco","fields":{"date":{"$date":1371591394454},"bindata":{"$binary":"QUJDRA=="}}}';
224 | var EJSONObject = EJSON.parse(DDPMessage);
225 |
226 | it('should expose the EJSON object', function(done) {
227 | var ddpclient = new DDPClient();
228 |
229 | assert(ddpclient.EJSON);
230 | assert(ddpclient.EJSON.addType);
231 |
232 | done();
233 | });
234 |
235 | it('should decode binary and dates', function(done) {
236 | var ddpclient = new DDPClient({ use_ejson : true });
237 |
238 | ddpclient._message(DDPMessage);
239 |
240 | assert.deepEqual(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].date, new Date(1371591394454));
241 |
242 | assert.deepEqual(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].bindata, new Uint8Array([65, 66, 67, 68]));
243 |
244 | ddpclient.socket = {};
245 | ddpclient.socket.send = function (opts) {
246 | assert(opts.indexOf("date") !== -1);
247 | assert(opts.indexOf("$date") !== -1);
248 | assert(opts.indexOf("1371591394454") !== -1);
249 |
250 | assert(opts.indexOf("bindata") !== -1);
251 | assert(opts.indexOf("$binary") !== -1);
252 | assert(opts.indexOf("QUJDRA==") !== -1);
253 | };
254 |
255 | ddpclient._send(EJSONObject.fields);
256 |
257 | done();
258 | });
259 |
260 | });
261 |
262 |
263 | describe('Collection maintenance and observation', function() {
264 | var addedMessage = '{"msg":"added","collection":"posts","id":"2trpvcQ4pn32ZYXco","fields":{"text":"A cat was here","value":true}}';
265 | var changedMessage = '{"msg":"changed","collection":"posts","id":"2trpvcQ4pn32ZYXco","fields":{"text":"A dog was here"}}';
266 | var changedMessage2 = '{"msg":"changed","collection":"posts","id":"2trpvcQ4pn32ZYXco","cleared":["value"]}';
267 | var removedMessage = '{"msg":"removed","collection":"posts","id":"2trpvcQ4pn32ZYXco"}';
268 | var observer;
269 |
270 | it('should maintain collections by default', function() {
271 | var ddpclient = new DDPClient(), observed = false;
272 | observer = ddpclient.observe("posts");
273 | observer.added = function(id) { if (id === '2trpvcQ4pn32ZYXco') observed = true; }
274 |
275 | ddpclient._message(addedMessage);
276 | // ensure collections exist and are populated by add messages
277 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].text, "A cat was here");
278 | assert(observed, "addition observed");
279 | });
280 |
281 | it('should maintain collections if maintainCollections is true', function() {
282 | var ddpclient = new DDPClient({ maintainCollections : true });
283 | ddpclient._message(addedMessage);
284 | // ensure collections exist and are populated by add messages
285 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].text, "A cat was here");
286 | });
287 |
288 | it('should not maintain collections if maintainCollections is false', function() {
289 | var ddpclient = new DDPClient({ maintainCollections : false });
290 | ddpclient._message(addedMessage);
291 | // ensure there are no collections
292 | assert(!ddpclient.collections);
293 | });
294 |
295 | it('should response to "added" messages', function() {
296 | var ddpclient = new DDPClient();
297 | ddpclient._message(addedMessage);
298 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco']._id, "2trpvcQ4pn32ZYXco");
299 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].text, "A cat was here");
300 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].value, true);
301 | });
302 |
303 | it('should response to "changed" messages', function() {
304 | var ddpclient = new DDPClient(), observed = false;
305 | observer = ddpclient.observe("posts");
306 | observer.changed = function(id, oldFields, clearedFields, newFields) {
307 | if (id === "2trpvcQ4pn32ZYXco"
308 | && oldFields.text === "A cat was here"
309 | && newFields.text === "A dog was here") {
310 | observed = true;
311 | }
312 | };
313 |
314 | ddpclient._message(addedMessage);
315 | ddpclient._message(changedMessage);
316 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].text, "A dog was here");
317 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].value, true);
318 | assert(observed, "field change observed");
319 | });
320 |
321 | it('should response to "changed" messages with "cleared"', function() {
322 | var ddpclient = new DDPClient(), observed = false;
323 | observer = ddpclient.observe("posts");
324 | observer.changed = function(id, oldFields, clearedFields) {
325 | if (id === "2trpvcQ4pn32ZYXco" && clearedFields.length === 1 && clearedFields[0] === "value") {
326 | observed = true;
327 | }
328 | };
329 |
330 | ddpclient._message(addedMessage);
331 | ddpclient._message(changedMessage);
332 | ddpclient._message(changedMessage2);
333 | assert.equal(ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].text, "A dog was here");
334 | assert(!ddpclient.collections.posts['2trpvcQ4pn32ZYXco'].hasOwnProperty('value'));
335 | assert(observed, "cleared change observed")
336 | });
337 |
338 | it('should response to "removed" messages', function() {
339 | var ddpclient = new DDPClient(), oldval;
340 | observer = ddpclient.observe("posts");
341 | observer.removed = function(id, oldValue) { oldval = oldValue; };
342 |
343 | ddpclient._message(addedMessage);
344 | ddpclient._message(removedMessage);
345 | assert(!ddpclient.collections.posts.hasOwnProperty('2trpvcQ4pn32ZYXco'));
346 | assert(oldval, "Removal observed");
347 | assert.equal(oldval.text, "A cat was here");
348 | assert.equal(oldval.value, true);
349 | });
350 | });
351 |
352 |
353 | describe("SockJS", function() {
354 | it("should use direct WS connection if there is a path", function() {
355 | var ddpclient = new DDPClient();
356 | ddpclient._makeWebSocketConnection = sinon.stub();
357 | ddpclient.connect();
358 |
359 | assert.ok(ddpclient._makeWebSocketConnection.called);
360 | });
361 |
362 | it("should fallback to sockjs if there useSockJS option", function() {
363 | var ddpclient = new DDPClient({ useSockJs: true });
364 | ddpclient._makeSockJSConnection = sinon.stub();
365 | ddpclient.connect();
366 |
367 | assert.ok(ddpclient._makeSockJSConnection.called);
368 | });
369 |
370 | describe("after info hit", function() {
371 | var request = require("request");
372 | it("should connect to the correct url", function(done) {
373 | var get = function(opts, callback) {
374 | assert.equal(opts.url, "http://the-host:9000/sockjs/info");
375 | done();
376 | };
377 |
378 | WithRequestGet(get, function() {
379 | var opts = {
380 | host: "the-host",
381 | port: 9000
382 | };
383 | var ddpclient = new DDPClient(opts);
384 | ddpclient._makeSockJSConnection();
385 | });
386 | });
387 |
388 | it("should support custom paths", function(done) {
389 | var get = function(opts, callback) {
390 | assert.equal(opts.url, "http://the-host:9000/search/sockjs/info");
391 | done();
392 | };
393 |
394 | WithRequestGet(get, function() {
395 | var opts = {
396 | host: "the-host",
397 | port: 9000,
398 | path: "search"
399 | };
400 | var ddpclient = new DDPClient(opts);
401 | ddpclient._makeSockJSConnection();
402 | });
403 | });
404 |
405 | it("should retry if there is an error", function() {
406 | var error = { message: "error" };
407 | var get = function(opts, callback) {
408 | callback(error);
409 | };
410 |
411 | WithRequestGet(get, function() {
412 | var ddpclient = new DDPClient();
413 | ddpclient._recoverNetworkError = sinon.stub();
414 | ddpclient._makeSockJSConnection();
415 | assert.ok(ddpclient._recoverNetworkError.called);
416 | });
417 | });
418 |
419 | it("should use direct WS if there is no body", function() {
420 | var info = null;
421 | var get = function(opts, callback) {
422 | callback(null, null, info);
423 | };
424 |
425 | WithRequestGet(get, function() {
426 | var ddpclient = new DDPClient();
427 | ddpclient._makeWebSocketConnection = sinon.stub();
428 | ddpclient._makeSockJSConnection();
429 |
430 | var wsUrl = "ws://localhost:3000/websocket";
431 | assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
432 | });
433 | });
434 |
435 | it("should use direct WS if there is no base_url", function() {
436 | var info = '{}';
437 | var get = function(opts, callback) {
438 | callback(null, null, info);
439 | };
440 |
441 | WithRequestGet(get, function() {
442 | var ddpclient = new DDPClient();
443 | ddpclient._makeWebSocketConnection = sinon.stub();
444 | ddpclient._makeSockJSConnection();
445 |
446 | var wsUrl = "ws://localhost:3000/websocket";
447 | assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
448 | });
449 | });
450 |
451 | it("should use full base url if it's starts with http", function() {
452 | var info = '{"base_url": "https://somepath"}';
453 | var get = function(opts, callback) {
454 | callback(null, null, info);
455 | };
456 |
457 | WithRequestGet(get, function() {
458 | var ddpclient = new DDPClient();
459 | ddpclient._makeWebSocketConnection = sinon.stub();
460 | ddpclient._makeSockJSConnection();
461 |
462 | var wsUrl = "wss://somepath/websocket";
463 | assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
464 | });
465 | });
466 |
467 | it("should compute url based on the base_url if it's not starts with http", function() {
468 | var info = '{"base_url": "/somepath"}';
469 | var get = function(opts, callback) {
470 | callback(null, null, info);
471 | };
472 |
473 | WithRequestGet(get, function() {
474 | var ddpclient = new DDPClient();
475 | ddpclient._makeWebSocketConnection = sinon.stub();
476 | ddpclient._makeSockJSConnection();
477 |
478 | var wsUrl = "ws://localhost:3000/somepath/websocket";
479 | assert.ok(ddpclient._makeWebSocketConnection.calledWith(wsUrl));
480 | });
481 | });
482 |
483 | it("should propagate tls options", function(done) {
484 | var tlsOpts = {'ca': ['fake_pem_content']};
485 | var get = function(opts, callback) {
486 | assert.equal(opts.agentOptions, tlsOpts);
487 | done();
488 | };
489 |
490 | WithRequestGet(get, function() {
491 | var opts = {
492 | host: "the-host",
493 | port: 9000,
494 | path: "search",
495 | tlsOpts: tlsOpts
496 | };
497 | var ddpclient = new DDPClient(opts);
498 | ddpclient._makeSockJSConnection();
499 | });
500 | });
501 | });
502 | });
503 |
504 | function WithRequestGet(getFn, fn) {
505 | var request = require("request");
506 | var originalGet = request.get;
507 | request.get = getFn;
508 |
509 | fn();
510 |
511 | request.get = originalGet;
512 | }
513 |
--------------------------------------------------------------------------------