├── .gitignore ├── example.js ├── package.json ├── LICENSE ├── README.md └── lib.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | // Create a server listening on the default port 3000 2 | var server = new DDPServer(); 3 | 4 | // Create a reactive collection 5 | // All the changes below will automatically be sent to subscribers 6 | var todoList = server.publish("todolist"); 7 | 8 | // Add items 9 | todoList[0] = { title: "Cook dinner", done: false }; 10 | todoList[1] = { title: "Water the plants", done: true }; 11 | 12 | // Change items 13 | todoList[0].done = true; 14 | 15 | // Remove items 16 | delete todoList[1] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddp-server-reactive", 3 | "version": "0.4.0", 4 | "description": "A ddp server for nodejs with reactive collections", 5 | "main": "lib.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "http://github.com/Q42/DDP-Server-Reactive" 12 | }, 13 | "keywords": [ 14 | "ddp", 15 | "server", 16 | "reactive" 17 | ], 18 | "author": "Sjoerd Visscher", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Q42/DDP-Server-Reactive/issues" 22 | }, 23 | "homepage": "https://github.com/Q42/DDP-Server-Reactive", 24 | "dependencies": { 25 | "ejson": "^2.0.1", 26 | "faye-websocket": "^0.9.0", 27 | "harmony-reflect": "^1.1.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Q42, Tarang Patel 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DDP Server with reactive collections 2 | 3 | DDP-Server-Reactive is a nodejs based DDP Server. 4 | 5 | ### Usage 6 | 7 | ``` 8 | // Create a server listening on the default port 3000 9 | var server = new DDPServer(); 10 | 11 | // Create a reactive collection 12 | // All the changes below will automatically be sent to subscribers 13 | var todoList = server.publish("todolist"); 14 | 15 | // Add items 16 | todoList[0] = { title: "Cook dinner", done: false }; 17 | todoList[1] = { title: "Water the plants", done: true }; 18 | 19 | // Change items 20 | todoList[0].done = true; 21 | 22 | // Remove items 23 | delete todoList[1] 24 | 25 | // Add methods 26 | server.methods({ 27 | test: function() { 28 | return true; 29 | } 30 | }); 31 | ``` 32 | 33 | You can then connect to it using a ddp client such as `ddp` 34 | 35 | ### Advanced Usage 36 | 37 | Create a server with a different port: 38 | 39 | ``` 40 | var server = new DDPServer({ port: 80 }); 41 | ``` 42 | 43 | Create a server using an existing http server 44 | so you use the same IP number for DDP and for web: 45 | 46 | ``` 47 | var app = express(); 48 | app.server = http.createServer(app); 49 | var server = new DDPServer({ httpServer: app.server }); 50 | ``` 51 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | require('harmony-reflect'); 2 | 3 | var DDPServer = function(opts) { 4 | 5 | opts = opts || {}; 6 | var WebSocket = require('faye-websocket'), 7 | EJSON = require('ejson'), 8 | http = require('http'), 9 | server = opts.httpServer, 10 | methods = opts.methods || {}, 11 | collections = {}, 12 | subscriptions = {}, 13 | self = this; 14 | 15 | if (!server) { 16 | server = http.createServer() 17 | server.listen(opts.port || 3000); 18 | } 19 | 20 | server.on('upgrade', function (request, socket, body) { 21 | if (WebSocket.isWebSocket(request)) { 22 | var ws = new WebSocket(request, socket, body); 23 | var session_id = "" + new Date().getTime(); 24 | subscriptions[session_id] = {}; 25 | 26 | function sendMessage(data) { 27 | ws.send(EJSON.stringify(data)); 28 | } 29 | 30 | ws.on('message', function(event) { 31 | var data = JSON.parse(event.data); 32 | 33 | switch (data.msg) { 34 | 35 | case "connect": 36 | 37 | sendMessage({ 38 | msg: "connected", 39 | session: session_id 40 | }); 41 | 42 | break; 43 | 44 | case "method": 45 | 46 | if (data.method in methods) { 47 | 48 | try { 49 | var result = methods[data.method].apply(this, data.params) 50 | 51 | sendMessage({ 52 | msg: "result", 53 | id: data.id, 54 | result: result 55 | }); 56 | 57 | sendMessage({ 58 | msg: "updated", 59 | id: data.id 60 | }) 61 | 62 | } catch (e) { 63 | console.log("error calling method", data.method, e) 64 | sendMessage({ 65 | id: data.id, 66 | error: { 67 | error: 500, 68 | reason: "Internal Server Error", 69 | errorType: "Meteor.Error" 70 | } 71 | }); 72 | } 73 | 74 | } else { 75 | console.log("Error method " + data.method + " not found"); 76 | 77 | sendMessage({ 78 | id: data.id, 79 | error: { 80 | error: 404, 81 | reason: "Method not found", 82 | errorType: "Meteor.Error" 83 | } 84 | }); 85 | } 86 | 87 | break; 88 | 89 | case "sub": 90 | 91 | subscriptions[session_id][data.name] = { 92 | added: function(id, doc) { 93 | sendMessage({ 94 | msg: "added", 95 | collection: data.name, 96 | id: id, 97 | fields: doc 98 | }) 99 | }, 100 | changed: function(id, fields, cleared) { 101 | sendMessage({ 102 | msg: "changed", 103 | collection: data.name, 104 | id: id, 105 | fields: fields, 106 | cleared: cleared 107 | }) 108 | }, 109 | removed: function(id) { 110 | sendMessage({ 111 | msg: "removed", 112 | collection: data.name, 113 | id: id 114 | }) 115 | } 116 | }; 117 | 118 | var docs = collections[data.name]; 119 | for (var id in docs) 120 | subscriptions[session_id][data.name].added(id, docs[id]); 121 | 122 | sendMessage({ 123 | msg: "ready", 124 | subs: [data.id] 125 | }); 126 | 127 | break; 128 | 129 | case "ping": 130 | 131 | sendMessage({ 132 | msg: "pong", 133 | id: data.id 134 | }); 135 | 136 | break; 137 | 138 | default: 139 | } 140 | }); 141 | 142 | ws.on('close', function(event) { 143 | delete subscriptions[session_id]; 144 | ws = null; 145 | session_id = null; 146 | }); 147 | } 148 | }); 149 | 150 | this.methods = function(newMethods) { 151 | for (var key in newMethods) { 152 | if (key in methods) 153 | throw new Error(500, "A method named " + key + " already exists"); 154 | methods[key] = newMethods[key]; 155 | } 156 | } 157 | 158 | this.publish = function(name) { 159 | if (name in collections) 160 | throw new Error(500, "A collection named " + name + " already exists"); 161 | 162 | var documents = {}; 163 | var proxiedDocuments = {}; 164 | 165 | function add(id, doc) { 166 | documents[id] = doc; 167 | proxiedDocuments[id] = new Proxy(doc, { 168 | set: function(_, field, value) { 169 | var changed = {}; 170 | doc[field] = changed[field] = value; 171 | sendChanged(id, changed, []); 172 | return value; 173 | }, 174 | deleteProperty: function(_, field) { 175 | delete doc[field]; 176 | sendChanged(id, {}, [field]); 177 | return true; 178 | } 179 | }); 180 | for (var client in subscriptions) 181 | if (subscriptions[client][name]) 182 | subscriptions[client][name].added(id, doc); 183 | } 184 | 185 | function change(id, doc) { 186 | var cleared = []; 187 | for (var field in documents[id]) { 188 | if (!(field in doc)) { 189 | cleared.push(field) 190 | delete documents[id][field]; 191 | } 192 | } 193 | var changed = {}; 194 | for (var field in doc) 195 | if (doc[field] != documents[id][field]) 196 | documents[id][field] = changed[field] = doc[field]; 197 | sendChanged(id, changed, cleared); 198 | } 199 | function sendChanged(id, changed, cleared) { 200 | for (var client in subscriptions) 201 | if (subscriptions[client][name]) 202 | subscriptions[client][name].changed(id, changed, cleared); 203 | } 204 | 205 | function remove(id) { 206 | delete documents[id]; 207 | for (var client in subscriptions) 208 | if (subscriptions[client][name]) 209 | subscriptions[client][name].removed(id); 210 | } 211 | 212 | return collections[name] = new Proxy(documents, { 213 | get: function(_, id) { 214 | return proxiedDocuments[id]; 215 | }, 216 | set: function(_, id, doc) { 217 | if (documents[id]) 218 | change(id, doc); 219 | else 220 | add(id, doc); 221 | return proxiedDocuments[id]; 222 | }, 223 | deleteProperty: function(_, id) { 224 | remove(id); 225 | return true; 226 | } 227 | }); 228 | } 229 | } 230 | 231 | module.exports = DDPServer 232 | --------------------------------------------------------------------------------