├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.markdown ├── component.json ├── events-amd.js ├── package.json ├── protocol.markdown ├── samples ├── process-child.js ├── process-parent.js ├── process-shared-api.js ├── tcp-client-autoreconnect.js ├── tcp-client.js └── tcp-server.js ├── smith.js ├── test-all.sh └── tests ├── README ├── helpers.js ├── package.json ├── phantom.js ├── public ├── .gitignore ├── index.html ├── smith.js └── test.js ├── test-agent.js ├── test-browser.js ├── test-framer.js ├── test-memory-leaks.js └── test-scrubber.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .settings.xml 3 | .c9revisions 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | samples 2 | test 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.8 5 | - 0.6 6 | script: ./test-all.sh 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Ajax.org B.V 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. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Smith 2 | 3 | [![Build Status](https://secure.travis-ci.org/c9/smith.png)](http://travis-ci.org/c9/smith) 4 | 5 | Smith is an RPC agent system for Node.JS used in architect and vfs. 6 | 7 | ## Usage 8 | 9 | Smith can be used in any situation where you have a duplex node stream. This 10 | can be over tcp, stdio, a pipe, or anything that sends bytes back and forth. 11 | 12 | ### TCP client-server example 13 | 14 | In this example, I have a TCP server that serves an add function to any agent 15 | clients who want to consume the service. 16 | 17 | For the server, we create a small agent and serve it on a listening tcp port. 18 | 19 | ```js 20 | var net = require('net'); 21 | var Agent = require('smith').Agent; 22 | 23 | var api = { 24 | add: function (a, b, callback) { 25 | callback(null, a + b); 26 | } 27 | }; 28 | 29 | // Start a TCP server 30 | net.createServer(function (socket) { 31 | // Create the agent that serves the shared api. 32 | var agent = new Agent(api); 33 | // Connect to the remote agent 34 | agent.connect(socket, function (err, api) { 35 | if (err) return console.error(err.stack); 36 | console.log("A new client connected"); 37 | }); 38 | // Log when the agent disconnects 39 | agent.on("disconnect", function (err) { 40 | console.error("The client disconnected") 41 | if (err) console.error(err.stack); 42 | }); 43 | 44 | }).listen(1337, function () { 45 | console.log("Agent server listening on port 1337"); 46 | }); 47 | ``` 48 | 49 | Then to consume this TCP service, we can create an agent and connect it to the 50 | tcp server. 51 | 52 | ```js 53 | var net = require('net'); 54 | var Agent = require('smith').Agent; 55 | 56 | var socket = net.connect(1337, function () { 57 | // Create our client 58 | var agent = new Agent() 59 | agent.connect(socket, function (err, api) { 60 | api.add(4, 5, function (err, result) { 61 | if (err) throw err; 62 | console.log("4 + 5 = %s", result); 63 | agent.disconnect(); 64 | }); 65 | }); 66 | }); 67 | ``` 68 | 69 | For an example of how to reconnect if the connection goes down, see 70 | https://github.com/c9/smith/blob/master/samples/tcp-client-autoreconnect.js 71 | 72 | ### STDIO Parent-Child Example 73 | 74 | Here we create a node process that spawns a child process, and the two talk to eachother calling functions both directions. 75 | 76 | Both share a simple API library. 77 | 78 | ```js 79 | exports.ping = function (callback) { 80 | callback(null, process.pid + " pong"); 81 | } 82 | ``` 83 | 84 | The parent creates an Agent,spawns the child, and connects. 85 | 86 | ```js 87 | var spawn = require('child_process').spawn; 88 | var Agent = require('smith').Agent; 89 | var Transport = require('smith').Transport; 90 | 91 | // Create an agent instance using the shared API 92 | var agent = new Agent(require('./process-shared-api')); 93 | 94 | // Spawn the child process that runs the other half. 95 | var child = spawn(process.execPath, [__dirname + "/process-child.js"]); 96 | // Forward the child's console output 97 | child.stderr.pipe(process.stderr); 98 | 99 | var transport = new Transport(child.stdout, child.stdin); 100 | agent.connect(transport, function (err, api) { 101 | if (err) throw err; 102 | // Call the child's API in a loop 103 | function loop() { 104 | api.ping(function (err, message) { 105 | if (err) throw err; 106 | console.log("Child says %s", message); 107 | }) 108 | setTimeout(loop, Math.random() * 1000); 109 | } 110 | loop(); 111 | }); 112 | ``` 113 | 114 | The child resumes stdin, creates an Agent, and connects. 115 | 116 | ```js 117 | var Agent = require('smith').Agent; 118 | var Transport = require('smith').Transport; 119 | 120 | // Redirect logs to stderr since stdout is used for data 121 | console.log = console.error; 122 | 123 | // Start listening on stdin for smith rpc data. 124 | process.stdin.resume(); 125 | 126 | var agent = new Agent(require('./process-shared-api')); 127 | var transport = new Transport(process.stdin, process.stdout); 128 | agent.connect(transport, function (err, api) { 129 | if (err) throw err; 130 | // Call the parent's API in a loop 131 | function loop() { 132 | api.ping(function (err, message) { 133 | if (err) throw err; 134 | console.log("Got %s from parent", message); 135 | }) 136 | setTimeout(loop, Math.random() * 1000); 137 | } 138 | loop(); 139 | }); 140 | ``` 141 | 142 | ## Class: Agent 143 | 144 | Agent is the main class used in smith. It represents an agent in your mesh 145 | network. It provides a set of service functions exposed as async functions. 146 | 147 | ### new Agent(api) 148 | 149 | Create a new Agent instance that serves the functions listed in `api`. 150 | 151 | ### agent.api 152 | 153 | The functions this agent serves locally to remote agents. 154 | 155 | ### agent.remoteApi 156 | 157 | A object containing proxy functions for the api functions in the remote agent. 158 | Calling these functions when the remote is offline will result in the last 159 | argument being called with a ENOTCONNECTED error (assuming it's a function). 160 | 161 | ### agent.connectionTimeout 162 | 163 | If the connection hasn't happened by 10,000 ms, an ETIMEDOUT error will 164 | happen. To change the timeoutvalue, change `connectionTimeout` on either the 165 | instance or the prototype. Set to zero to disable. 166 | 167 | ### Event: 'connect' 168 | 169 | `function (remoteApi) { }` 170 | 171 | When the rpc handshake is complete, the agent will emit a connect event 172 | containing the remoteApi. 173 | 174 | ### Event: 'disconnect' 175 | 176 | `function () { }` 177 | 178 | Emitted when the transport dies and the remote becomes offline 179 | 180 | ### Event: 'drain' 181 | 182 | When the writable stream in the transport emits drain, it's forwarded here 183 | 184 | ### agent.connect(transport, [callback])) 185 | 186 | Start the connection to a new remote agent using `transport`. Emits `connect` when 187 | ready or `error` on failure. Optionally use the callback to get `(err, api, 188 | agent)` results. 189 | 190 | The `transport` argument is either a Transport instance or a duplex Stream. 191 | The callback will be called with `(err, remoteApi)`. 192 | 193 | ### agent.disconnect(err) 194 | 195 | Tell the agent to disconnect from the transport with optional error reason `err`. 196 | 197 | ### agent.send(message) 198 | 199 | Encode a message and send it on the transport. Used internally to send 200 | function calls. Returns false if the kernel buffer is full. 201 | 202 | ## Class: Transport 203 | 204 | Transport is a wrapper around a duplex socket to allow two Agent instances to 205 | talk to eachother. A transport will shut down itself if either end of the 206 | socket ends and emit an `error` event. 207 | 208 | ### new Transport(input, [output]) 209 | 210 | Pass in either a duplex Stream instance or two streams (one readable, one 211 | writable). This transport object can then be used to connect to another 212 | Agent. 213 | 214 | ### Event: 'message' 215 | 216 | `function (message) { }` 217 | 218 | Emitted when a message arrives from the remote end of the transport.ts 219 | 220 | ### Event: 'drain' 221 | 222 | `function () { }` 223 | 224 | Emitted when the writable stream emits drain. (The write buffer is empty.) 225 | 226 | ### Event: 'disconnect' 227 | 228 | `function (err) { }` 229 | 230 | Emitted when the transport dies. If this was caused by an error, it will be 231 | emitted here. 232 | 233 | ### transport.send(message) 234 | 235 | Send a message to the other end of the transport. Message is JSON 236 | serializable object with the addition of being able to serialize node Buffer 237 | instances and `undefined` values. Returns true if the kernel buffer is full 238 | and you should pause your incoming stream. 239 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smith", 3 | "description": "Smith is an RPC agent system for Node.JS used in architect and vfs.", 4 | "version": "0.1.16", 5 | "main": "smith.js", 6 | "dependencies": { 7 | "creationix/msgpack-js-browser": "*", 8 | "juliangruber/events": "*" 9 | }, 10 | "scripts": ["smith.js"] 11 | } 12 | -------------------------------------------------------------------------------- /events-amd.js: -------------------------------------------------------------------------------- 1 | 2 | // @see https://github.com/ryanramage/events/blob/c22287485580ba9f182a102f8c5427bcf962f73e/events.js 3 | 4 | define(function(require, exports, module) { 5 | 6 | /** 7 | * ## Events module 8 | * 9 | * This is a browser port of the node.js events module. Many objects and 10 | * modules emit events and these are instances of events.EventEmitter. 11 | * 12 | * You can access this module by doing: `require("events")` 13 | * 14 | * Functions can then be attached to objects, to be executed when an event is 15 | * emitted. These functions are called listeners. 16 | * 17 | * @module 18 | */ 19 | 20 | 21 | /** 22 | * To access the EventEmitter class, require('events').EventEmitter. 23 | * 24 | * When an EventEmitter instance experiences an error, the typical action is to 25 | * emit an 'error' event. Error events are treated as a special case. If there 26 | * is no listener for it, then the default action is for the error to throw. 27 | * 28 | * All EventEmitters emit the event 'newListener' when new listeners are added. 29 | * 30 | * @name events.EventEmitter 31 | * @api public 32 | * 33 | * ```javascript 34 | * var EventEmitter = require('events').EventEmitter; 35 | * 36 | * // create an event emitter 37 | * var emitter = new EventEmitter(); 38 | * ``` 39 | */ 40 | 41 | var EventEmitter = exports.EventEmitter = function () {}; 42 | 43 | var isArray = Array.isArray || function (obj) { 44 | return toString.call(obj) === '[object Array]'; 45 | }; 46 | 47 | 48 | /** 49 | * By default EventEmitters will print a warning if more than 10 listeners are 50 | * added for a particular event. This is a useful default which helps finding 51 | * memory leaks. Obviously not all Emitters should be limited to 10. This 52 | * function allows that to be increased. Set to zero for unlimited. 53 | * 54 | * @name emitter.setMaxListeners(n) 55 | * @param {Number} n - The maximum number of listeners 56 | * @api public 57 | */ 58 | 59 | // By default EventEmitters will print a warning if more than 60 | // 10 listeners are added to it. This is a useful default which 61 | // helps finding memory leaks. 62 | // 63 | // Obviously not all Emitters should be limited to 10. This function allows 64 | // that to be increased. Set to zero for unlimited. 65 | var defaultMaxListeners = 10; 66 | EventEmitter.prototype.setMaxListeners = function(n) { 67 | if (!this._events) this._events = {}; 68 | this._events.maxListeners = n; 69 | }; 70 | 71 | 72 | /** 73 | * Execute each of the listeners in order with the supplied arguments. 74 | * 75 | * @name emitter.emit(event, [arg1], [arg2], [...]) 76 | * @param {String} event - The event name/id to fire 77 | * @api public 78 | */ 79 | 80 | EventEmitter.prototype.emit = function(type) { 81 | // If there is no 'error' event listener then throw. 82 | if (type === 'error') { 83 | if (!this._events || !this._events.error || 84 | (isArray(this._events.error) && !this._events.error.length)) 85 | { 86 | if (arguments[1] instanceof Error) { 87 | throw arguments[1]; // Unhandled 'error' event 88 | } else { 89 | throw new Error("Uncaught, unspecified 'error' event."); 90 | } 91 | return false; 92 | } 93 | } 94 | 95 | if (!this._events) return false; 96 | var handler = this._events[type]; 97 | if (!handler) return false; 98 | 99 | if (typeof handler == 'function') { 100 | switch (arguments.length) { 101 | // fast cases 102 | case 1: 103 | handler.call(this); 104 | break; 105 | case 2: 106 | handler.call(this, arguments[1]); 107 | break; 108 | case 3: 109 | handler.call(this, arguments[1], arguments[2]); 110 | break; 111 | // slower 112 | default: 113 | var args = Array.prototype.slice.call(arguments, 1); 114 | handler.apply(this, args); 115 | } 116 | return true; 117 | 118 | } else if (isArray(handler)) { 119 | var args = Array.prototype.slice.call(arguments, 1); 120 | 121 | var listeners = handler.slice(); 122 | for (var i = 0, l = listeners.length; i < l; i++) { 123 | listeners[i].apply(this, args); 124 | } 125 | return true; 126 | 127 | } else { 128 | return false; 129 | } 130 | }; 131 | 132 | 133 | /** 134 | * Adds a listener to the end of the listeners array for the specified event. 135 | * 136 | * @name emitter.on(event, listener) | emitter.addListener(event, listener) 137 | * @param {String} event - The event name/id to listen for 138 | * @param {Function} listener - The function to bind to the event 139 | * @api public 140 | * 141 | * ```javascript 142 | * session.on('change', function (userCtx) { 143 | * console.log('session changed!'); 144 | * }); 145 | * ``` 146 | */ 147 | 148 | // EventEmitter is defined in src/node_events.cc 149 | // EventEmitter.prototype.emit() is also defined there. 150 | EventEmitter.prototype.addListener = function(type, listener) { 151 | if ('function' !== typeof listener) { 152 | throw new Error('addListener only takes instances of Function'); 153 | } 154 | 155 | if (!this._events) this._events = {}; 156 | 157 | // To avoid recursion in the case that type == "newListeners"! Before 158 | // adding it to the listeners, first emit "newListeners". 159 | this.emit('newListener', type, listener); 160 | 161 | if (!this._events[type]) { 162 | // Optimize the case of one listener. Don't need the extra array object. 163 | this._events[type] = listener; 164 | } else if (isArray(this._events[type])) { 165 | 166 | // Check for listener leak 167 | if (!this._events[type].warned) { 168 | var m; 169 | if (this._events.maxListeners !== undefined) { 170 | m = this._events.maxListeners; 171 | } else { 172 | m = defaultMaxListeners; 173 | } 174 | 175 | if (m && m > 0 && this._events[type].length > m) { 176 | this._events[type].warned = true; 177 | console.error('(node) warning: possible EventEmitter memory ' + 178 | 'leak detected. %d listeners added. ' + 179 | 'Use emitter.setMaxListeners() to increase limit.', 180 | this._events[type].length); 181 | console.trace(); 182 | } 183 | } 184 | 185 | // If we've already got an array, just append. 186 | this._events[type].push(listener); 187 | } else { 188 | // Adding the second element, need to change to array. 189 | this._events[type] = [this._events[type], listener]; 190 | } 191 | 192 | return this; 193 | }; 194 | 195 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 196 | 197 | /** 198 | * Adds a one time listener for the event. This listener is invoked only the 199 | * next time the event is fired, after which it is removed. 200 | * 201 | * @name emitter.once(event, listener) 202 | * @param {String} event- The event name/id to listen for 203 | * @param {Function} listener - The function to bind to the event 204 | * @api public 205 | * 206 | * ```javascript 207 | * db.once('unauthorized', function (req) { 208 | * // this event listener will fire once, then be unbound 209 | * }); 210 | * ``` 211 | */ 212 | 213 | EventEmitter.prototype.once = function(type, listener) { 214 | var self = this; 215 | self.on(type, function g() { 216 | self.removeListener(type, g); 217 | listener.apply(this, arguments); 218 | }); 219 | 220 | return this; 221 | }; 222 | 223 | /** 224 | * Remove a listener from the listener array for the specified event. Caution: 225 | * changes array indices in the listener array behind the listener. 226 | * 227 | * @name emitter.removeListener(event, listener) 228 | * @param {String} event - The event name/id to remove the listener from 229 | * @param {Function} listener - The listener function to remove 230 | * @api public 231 | * 232 | * ```javascript 233 | * var callback = function (init) { 234 | * console.log('duality app loaded'); 235 | * }; 236 | * devents.on('init', callback); 237 | * // ... 238 | * devents.removeListener('init', callback); 239 | * ``` 240 | */ 241 | 242 | EventEmitter.prototype.removeListener = function(type, listener) { 243 | if ('function' !== typeof listener) { 244 | throw new Error('removeListener only takes instances of Function'); 245 | } 246 | 247 | // does not use listeners(), so no side effect of creating _events[type] 248 | if (!this._events || !this._events[type]) return this; 249 | 250 | var list = this._events[type]; 251 | 252 | if (isArray(list)) { 253 | var i = list.indexOf(listener); 254 | if (i < 0) return this; 255 | list.splice(i, 1); 256 | if (list.length == 0) 257 | delete this._events[type]; 258 | } else if (this._events[type] === listener) { 259 | delete this._events[type]; 260 | } 261 | 262 | return this; 263 | }; 264 | 265 | /** 266 | * Removes all listeners, or those of the specified event. 267 | * 268 | * @name emitter.removeAllListeners([event]) 269 | * @param {String} event - Event name/id to remove all listeners for (optional) 270 | * @api public 271 | */ 272 | 273 | EventEmitter.prototype.removeAllListeners = function(type) { 274 | // does not use listeners(), so no side effect of creating _events[type] 275 | if (type && this._events && this._events[type]) this._events[type] = null; 276 | return this; 277 | }; 278 | 279 | /** 280 | * Returns an array of listeners for the specified event. This array can be 281 | * manipulated, e.g. to remove listeners. 282 | * 283 | * @name emitter.listeners(event) 284 | * @param {String} events - The event name/id to return listeners for 285 | * @api public 286 | * 287 | * ```javascript 288 | * session.on('change', function (stream) { 289 | * console.log('session changed'); 290 | * }); 291 | * console.log(util.inspect(session.listeners('change'))); // [ [Function] ] 292 | * ``` 293 | */ 294 | 295 | EventEmitter.prototype.listeners = function(type) { 296 | if (!this._events) this._events = {}; 297 | if (!this._events[type]) this._events[type] = []; 298 | if (!isArray(this._events[type])) { 299 | this._events[type] = [this._events[type]]; 300 | } 301 | return this._events[type]; 302 | }; 303 | 304 | 305 | /** 306 | * @name emitter Event: 'newListener' 307 | * 308 | * This event is emitted any time someone adds a new listener. 309 | * 310 | * ```javascript 311 | * emitter.on('newListener', function (event, listener) { 312 | * // new listener added 313 | * }); 314 | * ``` 315 | */ 316 | 317 | }); 318 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ajax.org B.V. ", 3 | "contributors": [ 4 | { 5 | "name": "Tim Caswell", 6 | "email": "tim@c9.io>" 7 | } 8 | ], 9 | "name": "smith", 10 | "description": "Smith is an RPC agent system for Node.JS used in architect and vfs.", 11 | "version": "0.1.22", 12 | "scripts": { 13 | "test": "./test-all.sh" 14 | }, 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "http://github.com/c9/smith/raw/master/LICENSE" 19 | } 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/c9/smith.git" 24 | }, 25 | "main": "smith.js", 26 | "engines": { 27 | "node": ">=0.6.0" 28 | }, 29 | "dependencies": { 30 | "msgpack-js": "~0.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /protocol.markdown: -------------------------------------------------------------------------------- 1 | This document explains the actual protocol smith uses to communicate between 2 | agents. 3 | 4 | The Transport class handles message framing and msgpack serializing on top of 5 | whatever stream you give it. This can be tcp, pipe, or something else. The 6 | msgpack library used has slightly extended the format to add `Buffer` and 7 | `undefined` value types for a fuller range of node primitives. If desired 8 | another transport can be used in place that implements the same interface. 9 | 10 | Encoded on top of the serialzation format is reference cycles and callback 11 | functions. 12 | 13 | Since functions can be serialized, rpc using callbacks is natural. Simply 14 | pass your callback as an argument and the other side will get a proxy function 15 | when it's deserialized. When they call that proxy function, a message will be 16 | sent back and your callback will get called with the deserialized arguments 17 | (which can include yet another callback). Since the callbacks are executed on 18 | their native side, closure variables and all other state is preserved. 19 | Callbacks functions must be called once and only once to prevent memory leaks. 20 | If the transport goes down, all pending callbacks will get called with an 21 | error object as their first argument. It's advised to use node-style 22 | callbacks in your APIs. The named function in the Agent can be called 23 | multiple times. 24 | 25 | ### Function Encoding 26 | 27 | Functions are encoded as an object with one key, `$`. The value of this object is the 28 | unique function index in the local function repository. Function keys are 29 | integers. An example encoded function can look like `{$: 3}` where 30 | `remote.callbacks[3]` is the real function. Numbers are reused as soon as 31 | they are freed (when the function is called). 32 | 33 | ### Cycle Encoding 34 | 35 | Cycles are also encoded as `$` keyed objects. The value is the path to 36 | the actual value as an array of strings. In this way it works like a file- 37 | system symlink. For example. Given the following cyclic object: 38 | 39 | ```js 40 | var entry = { 41 | name: "Bob", 42 | boss: { name: "Steve" } 43 | }; 44 | entry.self = entry; 45 | entry.manager = entry.boss; 46 | ``` 47 | 48 | The following encoded object is generated by the internal `freeze` function. 49 | 50 | ```js 51 | { 52 | name: 'Bob', 53 | boss: { name: 'Steve' }, 54 | self: { $: [] }, 55 | manager: { $: [ 'boss' ] } 56 | } 57 | ``` 58 | 59 | See that the path `[]` points to the root object itself, and `['boss']` points to 60 | the boss property in the root. 61 | 62 | ## Function Calling 63 | 64 | Every RPC message is a function call. There are three kinds of function calls 65 | and they are all encoded the same way. A call is sent over the transport as a flat array. The first item is the function name or key, the rest are arguments to that function. There is no return value since everything is async. Use callbacks to get results. 66 | 67 | In these examples, we'll assume the following setup: Given a pair A and B, A has the api function "add", and B has none. They are connected to eachother through some transport. 68 | 69 | ### Named functions 70 | 71 | To call a named function simply pass the function name as the first array value. 72 | 73 | B calls A's "add" function and passes it `(3, 5, function (err, result) {...})` 74 | 75 | ``` 76 | B->A ["add", 3, 4, {$:1}] 77 | ``` 78 | 79 | ### Callback functions 80 | 81 | To call a callback, use the integer key the other side gave you and it will route the arguments to the callback. 82 | 83 | A responds to B's "add" query. 84 | 85 | ``` 86 | A->B [1, null, 7] 87 | ``` 88 | 89 | ### "ready" call (connection handshake) 90 | 91 | In the initial connection handshake, both sides call a virtual "ready" function passing in a single callback. The other side will reply using this callback with an array of function names. This result is used to populate the wrapper functions in remote.api. 92 | 93 | The handshake in our example would have looked like this. 94 | 95 | ``` 96 | A->B ["ready", {$:1}] 97 | B->A ["ready", {$:1}] 98 | B->A [1, []] 99 | A->B [1, ["add"]] 100 | ``` 101 | 102 | Note that both sides can do the handshake at the same time. Also note that the function key numbers are independently namespaced per Remote instance. Since this was the first callback for both sides, they both used `1` as the callback key. B then responded using the callback that it has no api functions by sending an empty array. A responded by saying it has one. 103 | 104 | As soon as one side receives the api list for the other side, it will emit the "connect" event and be ready to use. 105 | 106 | ## Debugging tips 107 | 108 | A very powerful debugging trick is to log all messages in the protocol. On the first line of `Remote.prototype._onMessage`, add this log statement. 109 | 110 | ```js 111 | console.log(process.pid, message); 112 | ``` 113 | 114 | This will log the process id of the process receiving the message as well as the message already msgpack decoded (but not yet livened). 115 | -------------------------------------------------------------------------------- /samples/process-child.js: -------------------------------------------------------------------------------- 1 | var Agent = require('smith').Agent; 2 | var Transport = require('smith').Transport; 3 | 4 | // Redirect logs to stderr since stdout is used for data 5 | console.log = console.error; 6 | 7 | // Start listening on stdin for smith rpc data. 8 | process.stdin.resume(); 9 | 10 | var agent = new Agent(require('./process-shared-api')); 11 | var transport = new Transport(process.stdin, process.stdout); 12 | agent.connect(transport, function (err, api) { 13 | if (err) throw err; 14 | // Call the parent's API in a loop 15 | function loop() { 16 | api.ping(function (err, message) { 17 | if (err) throw err; 18 | console.log("Got %s from parent", message); 19 | }) 20 | setTimeout(loop, Math.random() * 1000); 21 | } 22 | loop(); 23 | }); 24 | -------------------------------------------------------------------------------- /samples/process-parent.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var Agent = require('smith').Agent; 3 | var Transport = require('smith').Transport; 4 | 5 | // Create an agent instance using the shared API 6 | var agent = new Agent(require('./process-shared-api')); 7 | 8 | // Spawn the child process that runs the other half. 9 | var child = spawn(process.execPath, [__dirname + "/process-child.js"]); 10 | // Forward the child's console output 11 | child.stderr.pipe(process.stderr); 12 | 13 | var transport = new Transport(child.stdout, child.stdin); 14 | agent.connect(transport, function (err, api) { 15 | if (err) throw err; 16 | // Call the child's API in a loop 17 | function loop() { 18 | api.ping(function (err, message) { 19 | if (err) throw err; 20 | console.log("Child says %s", message); 21 | }) 22 | setTimeout(loop, Math.random() * 1000); 23 | } 24 | loop(); 25 | }); 26 | -------------------------------------------------------------------------------- /samples/process-shared-api.js: -------------------------------------------------------------------------------- 1 | // A very simple API 2 | exports.ping = function (callback) { 3 | callback(null, process.pid + " pong"); 4 | } 5 | -------------------------------------------------------------------------------- /samples/tcp-client-autoreconnect.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var Agent = require('smith').Agent; 3 | var Transport = require('smith').Transport; 4 | 5 | // Create our client agent. 6 | var agent = new Agent(); 7 | 8 | var backoff = 1000; 9 | var add; 10 | 11 | // Query the API to test the connection 12 | function query() { 13 | var a = Math.floor(Math.random() * 10) + 1; 14 | var b = Math.floor(Math.random() * 10) + 1; 15 | console.log("%s + %s = ...", a, b); 16 | add(a, b, function (err, result) { 17 | if (err) console.error(err.stack); 18 | else console.log("%s + %s = %s", a, b, result); 19 | }); 20 | } 21 | 22 | // On the first connect, store a reference to the add function and start 23 | // calling it on an interval 24 | agent.once("connect", function (api) { 25 | add = api.add; 26 | console.log("Running query() every 3000 ms"); 27 | setInterval(query, 3000); 28 | query(); 29 | }); 30 | 31 | // On every connect, log the connection and reset the backoff time. 32 | agent.on("connect", function () { 33 | console.log("Connected!"); 34 | if (backoff > 1000) { 35 | console.log(" Resetting backoff to 1000ms."); 36 | backoff = 1000; 37 | } 38 | }); 39 | 40 | // Set up auto-reconnect and do initial connection 41 | agent.on("disconnect", onError); 42 | connect(); 43 | 44 | function connect() { 45 | var socket = net.connect(1337, function () { 46 | agent.connect(new Transport(socket)); 47 | }); 48 | socket.on("error", onError); 49 | } 50 | 51 | function onError(err) { 52 | if (err) console.error(err.stack); 53 | console.log("Reconnecting in %s ms", backoff); 54 | setTimeout(connect, backoff); 55 | backoff *= 2; 56 | } 57 | -------------------------------------------------------------------------------- /samples/tcp-client.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var Agent = require('smith').Agent; 3 | 4 | var socket = net.connect(1337, function () { 5 | // Create our client 6 | var agent = new Agent() 7 | agent.connect(socket, function (err, api) { 8 | api.add(4, 5, function (err, result) { 9 | if (err) throw err; 10 | console.log("4 + 5 = %s", result); 11 | agent.disconnect(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /samples/tcp-server.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var Agent = require('smith').Agent; 3 | 4 | var api = { 5 | add: function (a, b, callback) { 6 | callback(null, a + b); 7 | } 8 | }; 9 | 10 | // Start a TCP server 11 | net.createServer(function (socket) { 12 | // Create the agent that serves the shared api. 13 | var agent = new Agent(api); 14 | // Connect to the remote agent 15 | agent.connect(socket, function (err, api) { 16 | if (err) return console.error(err.stack); 17 | console.log("A new client connected"); 18 | }); 19 | // Log when the agent disconnects 20 | agent.on("disconnect", function (err) { 21 | console.error("The client disconnected") 22 | if (err) console.error(err.stack); 23 | }); 24 | 25 | }).listen(1337, function () { 26 | console.log("Agent server listening on port 1337"); 27 | }); 28 | -------------------------------------------------------------------------------- /smith.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012 Ajax.org B.V 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | ( // Module boilerplate to support browser globals, node.js and AMD. 23 | (typeof module !== "undefined" && function (m) { module.exports = m(require('events'), require('msgpack-js')); }) || 24 | (typeof define === "function" && function (m) { define("smith", ["./events-amd", "msgpack-js"], m); }) || 25 | (function (m) { window.smith = m(window.events, window.msgpack); }) 26 | )(function (events, msgpack) { 27 | "use strict"; 28 | var EventEmitter = events.EventEmitter; 29 | 30 | function inherits(Child, Parent) { 31 | Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child }}); 32 | } 33 | 34 | var exports = {}; 35 | 36 | exports.msgpack = msgpack; 37 | exports.Agent = Agent; 38 | exports.Transport = Transport; 39 | exports.deFramer = deFramer; 40 | exports.liven = liven; 41 | exports.freeze = freeze; 42 | exports.getType = getType; 43 | 44 | //////////////////////////////////////////////////////////////////////////////// 45 | 46 | // Transport is a connection between two Agents. It lives on top of a duplex, 47 | // binary stream. 48 | // @input - the stream we listen for data on 49 | // @output - the stream we write to (can be the same object as input) 50 | // @send(message) - send a message to the other side 51 | // "message" - event emitted when we get a message from the other side. 52 | // "disconnect" - the transport was disconnected 53 | // "error" - event emitted for stream error or disconnect 54 | // "drain" - drain event from output stream 55 | function Transport(input, debug) { 56 | var self = this; 57 | this.debug = debug; 58 | var output; 59 | if (Array.isArray(input)) { 60 | output = input[1]; 61 | input = input[0]; 62 | } else { 63 | output = input; 64 | } 65 | this.input = input; 66 | this.output = output; 67 | 68 | if (!input.readable) throw new Error("Input is not readable"); 69 | if (!output.writable) throw new Error("Output is not writable"); 70 | 71 | // Attach event listeners 72 | input.on("data", onData); 73 | input.on("end", onDisconnect); 74 | input.on("timeout", onDisconnect); 75 | input.on("close", onDisconnect); 76 | input.on("error", onError); 77 | output.on("drain", onDrain); 78 | if (output !== input) { 79 | output.on("end", onDisconnect); 80 | output.on("timeout", onDisconnect); 81 | output.on("close", onDisconnect); 82 | output.on("error", onError); 83 | } 84 | 85 | var parse = deFramer(function (frame) { 86 | var message; 87 | try { 88 | message = msgpack.decode(frame); 89 | } catch (err) { 90 | return self.emit("error", err); 91 | } 92 | debug && console.log(process.pid + " <- " + require('util').inspect(message, false, 2, true)); 93 | self.emit("message", message); 94 | }); 95 | 96 | // Route data chunks to the parser, but check for errors 97 | function onData(chunk) { 98 | try { 99 | parse(chunk); 100 | } catch (err) { 101 | self.emit("error", err); 102 | } 103 | } 104 | 105 | // Forward drain events from the writable stream 106 | function onDrain() { 107 | self.emit("drain"); 108 | } 109 | // Forward all error events to the transport 110 | function onError(err) { 111 | self.emit("error", err); 112 | } 113 | function onDisconnect() { 114 | // Remove all the listeners we added and destroy the streams 115 | input.removeListener("data", onData); 116 | input.removeListener("end", onDisconnect); 117 | input.removeListener("timeout", onDisconnect); 118 | input.removeListener("close", onDisconnect); 119 | output.removeListener("drain", onDrain); 120 | if (input.destroy) input.destroy(); 121 | if (input !== output) { 122 | output.removeListener("end", onDisconnect); 123 | output.removeListener("timeout", onDisconnect); 124 | output.removeListener("close", onDisconnect); 125 | if (output.destroy && output !== process.stdout) output.destroy(); 126 | } 127 | self.emit("disconnect"); 128 | } 129 | this.disconnect = onDisconnect; 130 | } 131 | inherits(Transport, EventEmitter); 132 | 133 | Transport.prototype.send = function (message) { 134 | // Uncomment to debug protocol 135 | this.debug && console.log(process.pid + " -> " + require('util').inspect(message, false, 2, true)); 136 | 137 | // Serialize the messsage. 138 | var frame = msgpack.encode(message); 139 | 140 | // Send a 4 byte length header before the frame. 141 | var header = new Buffer(10); 142 | header.writeUInt32BE(frame.length, 0); 143 | 144 | // Compute 4 byte jenkins hash 145 | var a = frame.length >> 24, 146 | b = (frame.length >> 16) & 0xff, 147 | c = (frame.length >> 8) & 0xff, 148 | d = frame.length & 0xff; 149 | 150 | // Little bit inlined, but fast 151 | var hash = 0; 152 | hash += a; 153 | hash += hash << 10; 154 | hash += hash >> 6; 155 | hash += b; 156 | hash += hash << 10; 157 | hash += hash >> 6; 158 | hash += c; 159 | hash += hash << 10; 160 | hash += hash >> 6; 161 | hash += d; 162 | hash += hash << 10; 163 | hash += hash >> 6; 164 | 165 | // Shuffle bits 166 | hash += hash << 3; 167 | hash = hash ^ (hash >> 11); 168 | hash += hash << 15; 169 | hash |= 0; 170 | header.writeInt32BE(hash, 4, true); 171 | 172 | // 2 Reserved bytes for future usage 173 | header.writeUInt16BE(0, 8); 174 | 175 | this.output.write(header); 176 | 177 | 178 | // Send the serialized message. 179 | return this.output.write(frame); 180 | }; 181 | 182 | // A simple state machine that consumes raw bytes and emits frame events. 183 | // Returns a parser function that consumes buffers. It emits message buffers 184 | // via onMessage callback passed in. 185 | function deFramer(onFrame) { 186 | var buffer; 187 | var state = 0; 188 | var length = 0; 189 | var expected_hash = 0; 190 | var hash = 0; 191 | var offset; 192 | return function parse(chunk) { 193 | for (var i = 0, l = chunk.length; i < l; i++) { 194 | switch (state) { 195 | case 0: 196 | length |= chunk[i] << 24; 197 | expected_hash = 0; 198 | state = 1; 199 | break; 200 | case 1: length |= chunk[i] << 16; state = 2; break; 201 | case 2: length |= chunk[i] << 8; state = 3; break; 202 | case 3: 203 | length |= chunk[i]; 204 | expected_hash += chunk[i]; 205 | expected_hash += expected_hash << 10; 206 | expected_hash += expected_hash >> 6; 207 | 208 | // Shuffle bits 209 | expected_hash += expected_hash << 3; 210 | expected_hash = expected_hash ^ (expected_hash >> 11); 211 | expected_hash += expected_hash << 15; 212 | expected_hash |= 0; 213 | 214 | hash = 0; 215 | state = 4; 216 | break; 217 | case 4: hash |= chunk[i] << 24; state = 5; break; 218 | case 5: hash |= chunk[i] << 16; state = 6; break; 219 | case 6: hash |= chunk[i] << 8; state = 7; break; 220 | case 7: hash |= chunk[i]; state = 8; 221 | if (hash !== expected_hash) { 222 | throw new Error("Hash mismatch, expected: " + expected_hash + 223 | " got: " + hash + ", chunk: " + chunk); 224 | } 225 | 226 | if (length > 100 * 1024 * 1024) { 227 | throw new Error("Too big buffer " + length + 228 | ", chunk: " + chunk); 229 | } 230 | 231 | buffer = new Buffer(length); 232 | offset = 0; 233 | state = 9; 234 | break; 235 | // Two reserved bytes 236 | case 9: state = 10; break; 237 | case 10: state = 11; break; 238 | 239 | // Data itself 240 | case 11: 241 | var len = l - i; 242 | var emit = false; 243 | if (len + offset >= length) { 244 | emit = true; 245 | len = length - offset; 246 | } 247 | // TODO: optimize for case where a copy isn't needed can a slice can 248 | // be used instead? 249 | chunk.copy(buffer, offset, i, i + len); 250 | offset += len; 251 | i += len - 1; 252 | if (emit) { 253 | onFrame(buffer); 254 | state = 0; 255 | length = 0; 256 | buffer = undefined; 257 | offset = undefined; 258 | } 259 | break; 260 | } 261 | 262 | // Common case 263 | if (state <= 3 && !emit) { 264 | expected_hash += chunk[i]; 265 | expected_hash += expected_hash << 10; 266 | expected_hash += expected_hash >> 6; 267 | } 268 | 269 | emit = false; 270 | } 271 | }; 272 | } 273 | 274 | //////////////////////////////////////////////////////////////////////////////// 275 | 276 | // Agent is an API serving node in the architect-agent rpc mesh. It contains 277 | // a table of functions that actually do the work and serve them to a Agent 278 | // agent. An agent can connect to one other agent at a time. 279 | function Agent(api) { 280 | if (!this instanceof Agent) throw new Error("Forgot to use new with Agent constructor"); 281 | 282 | this.api = api || {}; 283 | 284 | // Bind event handlers and callbacks 285 | this.disconnect = this.disconnect.bind(this); 286 | this._onMessage = this._onMessage.bind(this); 287 | this._onDrain = this._onDrain.bind(this); 288 | this._onReady = this._onReady.bind(this); 289 | this._getFunction = this._getFunction.bind(this); 290 | this._storeFunction = this._storeFunction.bind(this); 291 | 292 | this.remoteApi = {}; // Persist the API object between connections 293 | this.transport = undefined; 294 | this.callbacks = undefined; 295 | this.nextKey = undefined; 296 | } 297 | inherits(Agent, EventEmitter); 298 | 299 | // Time to wait for Agent connections to finish 300 | Agent.prototype.connectionTimeout = 10000; 301 | 302 | Agent.prototype.connect = function (transport, callback) { 303 | // If they passed in a raw stream, wrap it. 304 | if (!(transport instanceof Transport)) transport = new Transport(transport); 305 | 306 | this.transport = transport; 307 | this.callbacks = {}; 308 | this.nextKey = 1; 309 | 310 | transport.on("error", this.disconnect); 311 | transport.on("disconnect", this.disconnect); 312 | transport.on("message", this._onMessage); 313 | transport.on("drain", this._onDrain); 314 | 315 | // Handshake with the other end 316 | this.send(["ready", this._onReady]); 317 | 318 | // Start timeout and route events to callback 319 | this.on("connect", onConnect); 320 | this.on("disconnect", onDisconnect); 321 | this.on("error", onError); 322 | var timeout; 323 | if (this.connectionTimeout) { 324 | timeout = setTimeout(onTimeout, this.connectionTimeout); 325 | } 326 | 327 | var self = this; 328 | function onConnect(api) { 329 | reset(); 330 | if (callback) callback(null, api); 331 | } 332 | function onDisconnect(err) { 333 | onError(err || new Error("EDISCONNECT: Agent disconnected")); 334 | } 335 | function onError(err) { 336 | reset(); 337 | if (callback) callback(err); 338 | else self.emit("error", err); 339 | } 340 | function onTimeout() { 341 | reset(); 342 | var err = new Error("ETIMEDOUT: Timeout while waiting for Agent agent to connect."); 343 | err.code = "ETIMEDOUT"; 344 | if (callback) callback(err); 345 | else self.emit("error", err); 346 | } 347 | // Only one event should happen, so stop event listeners on first event. 348 | function reset() { 349 | self.removeListener("connect", onConnect); 350 | self.removeListener("disconnect", onDisconnect); 351 | self.removeListener("error", onError); 352 | clearTimeout(timeout); 353 | } 354 | }; 355 | 356 | Agent.prototype.send = function (message) { 357 | message = freeze(message, this._storeFunction); 358 | return this.transport.send(message); 359 | }; 360 | 361 | Agent.prototype._onReady = function (names, env) { 362 | if (!Array.isArray(names)) return; 363 | var self = this; 364 | names.forEach(function (name) { 365 | // Ignore already set functions so that existing function references 366 | // stay valid. 367 | if (self.remoteApi[name]) return; 368 | self.remoteApi[name] = function () { 369 | // When disconnected we can't forward the call. 370 | if (!self.transport) { 371 | var callback = arguments[arguments.length - 1]; 372 | if (typeof callback === "function") { 373 | var err = new Error("ENOTCONNECTED: Agent is offline, try again later"); 374 | err.code = "ENOTCONNECTED"; 375 | callback(err); 376 | } 377 | return; 378 | } 379 | var args = [name]; 380 | args.push.apply(args, arguments); 381 | return self.send(args); 382 | }; 383 | }); 384 | this.remoteEnv = env; 385 | this._emitConnect(); 386 | }; 387 | 388 | Agent.prototype._emitConnect = function () { 389 | this.emit("connect", this.remoteApi); 390 | }; 391 | 392 | // Disconnect resets the state of the Agent, flushes callbacks and emits a 393 | // "disconnect" event with optional error object. 394 | Agent.prototype.disconnect = function (err) { 395 | // if (!this.transport) { 396 | // if (err) return this.emit("error", err); 397 | // } 398 | 399 | // Disconnect from transport 400 | if (this.transport) { 401 | this.transport.removeListener("error", this.disconnect); 402 | this.transport.removeListener("disconnect", this.disconnect); 403 | this.transport.removeListener("message", this._onMessage); 404 | this.transport.removeListener("drain", this._onDrain); 405 | this.transport.disconnect(); 406 | this.transport = undefined; 407 | } 408 | 409 | var cerr = err; 410 | if (!cerr) { 411 | cerr = new Error("EDISCONNECT: Agent disconnected"); 412 | cerr.code = "EDISCONNECT"; 413 | } 414 | 415 | this.emit("disconnect", err); 416 | 417 | // Flush any callbacks 418 | if (this.callbacks) { 419 | var callbacks = this.callbacks; 420 | this.callbacks = undefined; 421 | forEach(callbacks, function (callback) { 422 | callback(cerr); 423 | }); 424 | } 425 | this.nextKey = undefined; 426 | 427 | }; 428 | 429 | // Forward drain events 430 | Agent.prototype._onDrain = function () { 431 | this.emit("drain"); 432 | }; 433 | 434 | // Route incoming messages to the right functions 435 | Agent.prototype._onMessage = function (message) { 436 | 437 | if (!(Array.isArray(message) && message.length)) { 438 | return this.emit("error", new Error("Message should be an array")); 439 | } 440 | message = liven(message, this._getFunction); 441 | var id = message[0]; 442 | var fn; 443 | if (id === "ready") { 444 | var keys = Object.keys(this.api); 445 | var env = typeof process !== "undefined" && process.env; 446 | fn = function (callback) { 447 | callback(keys, env); 448 | }; 449 | } 450 | else { 451 | fn = typeof id === "string" ? this.api[id] : this.callbacks[id]; 452 | } 453 | if (typeof fn !== "function") { 454 | return this.emit("error", new Error("Should be function")); 455 | } 456 | fn.apply(this, message.slice(1)); 457 | }; 458 | 459 | // Create a proxy function that calls fn key on the Agent side. 460 | // This is for when a Agent passes a callback to a local function. 461 | Agent.prototype._getFunction = function (key) { 462 | var transport = this.transport; 463 | return function () { 464 | // Call a Agent function using [key, args...] 465 | var args = [key]; 466 | // Push is actually fast http://jsperf.com/array-push-vs-concat-vs-unshift 467 | args.push.apply(args, arguments); 468 | return transport.send(args); 469 | }; 470 | }; 471 | 472 | // This is for when we call a Agent function and pass in a callback 473 | Agent.prototype._storeFunction = function (fn) { 474 | var key = this.nextKey; 475 | while (this.callbacks.hasOwnProperty(key)) { 476 | key = (key + 1) >> 0; 477 | if (key === this.nextKey) { 478 | throw new Error("Ran out of keys!!"); 479 | } 480 | } 481 | this.nextKey = (key + 1) >> 0; 482 | 483 | var callbacks = this.callbacks; 484 | var self = this; 485 | // Wrap is a self cleaning function and store in the index 486 | callbacks[key] = function () { 487 | delete callbacks[key]; 488 | self.nextKey = key; 489 | return fn.apply(this, arguments); 490 | }; 491 | return key; 492 | }; 493 | 494 | // Convert a js object into a serializable object when functions are 495 | // encountered, the storeFunction callback is called for each one. 496 | // storeFunction takes in a function and returns a unique id number. Cycles 497 | // are stored as object with a single $ key and an array of strigs as the 498 | // path. Functions are stored as objects with a single $ key and id as value. 499 | // props. properties starting with "$" have an extra $ prepended. 500 | function freeze(value, storeFunction) { 501 | var seen = []; 502 | var paths = []; 503 | function find(value, path) { 504 | // find the type of the value 505 | var type = getType(value); 506 | // pass primitives through as-is 507 | if (type !== "function" && type !== "object" && type !== "array" && type !== "date") { 508 | return value; 509 | } 510 | 511 | // Look for duplicates 512 | var index = seen.indexOf(value); 513 | if (index >= 0) { 514 | return { "$": paths[index] }; 515 | } 516 | // If not seen, put it in the registry 517 | index = seen.length; 518 | seen[index] = value; 519 | paths[index] = path; 520 | 521 | var o; 522 | // Look for functions 523 | if (type === "function") { 524 | o = storeFunction(value); 525 | } 526 | 527 | if (type === "date") { 528 | o = {d:value.getTime()}; 529 | } 530 | 531 | if (o) return {$:o}; 532 | 533 | // Recurse on objects and arrays 534 | return map(value, function (sub, key) { 535 | return find(sub, path.concat([key])); 536 | }, null, function (key) { 537 | return key[0] === "$" ? "$" + key : key; 538 | }); 539 | } 540 | return find(value, []); 541 | } 542 | 543 | // Converts flat objects into live objects. Cycles are re-connected and 544 | // functions are inserted. The getFunction callback is called whenever a 545 | // frozen function is encountered. It expects an ID and returns the function 546 | function liven(message, getFunction) { 547 | function find(value, parent, key) { 548 | // find the type of the value 549 | var type = getType(value); 550 | 551 | // Unescape $$+ escaped keys 552 | if (key[0] === "$") key = key.substr(1); 553 | 554 | // pass primitives through as-is 555 | if (type !== "function" && type !== "object" && type !== "array") { 556 | parent[key] = value; 557 | return value; 558 | } 559 | 560 | // Load Specials 561 | if (value.hasOwnProperty("$")) { 562 | var special = value.$; 563 | // Load backreferences 564 | if (Array.isArray(special)) { 565 | parent[key] = get(obj.root, special); 566 | return parent[key]; 567 | } 568 | if (typeof special === "object") { 569 | parent[key] = new Date(special.d); 570 | return parent[key]; 571 | } 572 | // Load functions 573 | parent[key] = getFunction(special); 574 | return parent[key]; 575 | } 576 | 577 | // Recurse on objects and arrays 578 | var o = Array.isArray(value) ? [] : {}; 579 | parent[key] = o; 580 | forEach(value, function (sub, key) { 581 | find(sub, o, key); 582 | }); 583 | return obj; 584 | } 585 | var obj = {}; 586 | find(message, obj, "root"); 587 | return obj.root; 588 | } 589 | 590 | //////////////////////////////////////////////////////////////////////////////// 591 | 592 | // Typeof is broken in javascript, add support for null and buffer types 593 | function getType(value) { 594 | if (value === null) { 595 | return "null"; 596 | } 597 | if (Array.isArray(value)) { 598 | return "array"; 599 | } 600 | if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { 601 | return "buffer"; 602 | } 603 | // TODO: find a way to work with Date instances from other contexts. 604 | if (value instanceof Date) { 605 | return "date"; 606 | } 607 | return typeof value; 608 | } 609 | 610 | // Traverse an object to get a value at a path 611 | function get(root, path) { 612 | var target = root; 613 | for (var i = 0, l = path.length; i < l; i++) { 614 | target = target[path[i]]; 615 | } 616 | return target; 617 | } 618 | 619 | // forEach that works on both arrays and objects 620 | function forEach(value, callback, thisp) { 621 | if (typeof value.forEach === "function") { 622 | return value.forEach.call(value, callback, thisp); 623 | } 624 | var keys = Object.keys(value); 625 | for (var i = 0, l = keys.length; i < l; i++) { 626 | var key = keys[i]; 627 | callback.call(thisp, value[key], key, value); 628 | } 629 | } 630 | 631 | // map that works on both arrays and objects 632 | function map(value, callback, thisp, keyMap) { 633 | if (typeof value.map === "function") { 634 | return value.map.call(value, callback, thisp); 635 | } 636 | var obj = {}; 637 | var keys = Object.keys(value); 638 | for (var i = 0, l = keys.length; i < l; i++) { 639 | var key = keys[i]; 640 | obj[keyMap ? keyMap(key) : key] = callback.call(thisp, value[key], key, value); 641 | } 642 | return obj; 643 | } 644 | 645 | 646 | exports.WebSocketTransport = WebSocketTransport; 647 | inherits(WebSocketTransport, Transport); 648 | 649 | // "message" - event emitted when we get a message from the other side. 650 | // "disconnect" - the transport was disconnected 651 | // "error" - event emitted for stream error or disconnect 652 | // "drain" - drain event from output stream 653 | function WebSocketTransport(socket, debug) { 654 | this.socket = socket; 655 | this.debug = debug; 656 | var self = this; 657 | 658 | socket.on("message", onMessage); 659 | socket.on("close", onDisconnect); 660 | socket.on("error", onError); 661 | function onError(err) { 662 | self.emit("error", err); 663 | } 664 | function onMessage(data) { 665 | var message; 666 | try { message = msgpack.decode(data); } 667 | catch (err) { return onError(err); } 668 | debug && console.log(process.pid + " <- " + require('util').inspect(message, false, 2, true)); 669 | self.emit("message", message); 670 | } 671 | function onDisconnect() { 672 | // Remove all the listeners we added and destroy the socket 673 | socket.removeListener("message", onMessage); 674 | socket.removeListener("close", onDisconnect); 675 | self.emit("disconnect"); 676 | } 677 | this.disconnect = onDisconnect; 678 | // TODO: Implement "drain" event, pause(), and resume() properly. 679 | // function onDrain() { 680 | // self.emit("drain"); 681 | // } 682 | } 683 | 684 | WebSocketTransport.prototype.send = function (message) { 685 | // Uncomment to debug protocol 686 | this.debug && console.log(process.pid + " -> " + require('util').inspect(message, false, 2, true)); 687 | var data; 688 | try { data = msgpack.encode(message); } 689 | catch (err) { return this.emit("error", err); } 690 | this.socket.send(data, {binary: true}); 691 | }; 692 | 693 | exports.BrowserTransport = BrowserTransport; 694 | inherits(BrowserTransport, Transport); 695 | 696 | function BrowserTransport(websocket, debug) { 697 | this.websocket = websocket; 698 | this.debug = debug; 699 | var self = this; 700 | 701 | websocket.binaryType = 'arraybuffer'; 702 | websocket.onmessage = function (evt) { 703 | var message; 704 | try { message = msgpack.decode(evt.data); } 705 | catch (err) { return onError(err); } 706 | debug && console.log("<-", message); 707 | self.emit("message", message); 708 | }; 709 | 710 | websocket.onclose = function (evt) { 711 | }; 712 | 713 | websocket.onerror = function (evt) { 714 | onError(new Error(evt.data)); 715 | }; 716 | 717 | function onError(err) { 718 | self.emit("error", err); 719 | } 720 | 721 | function onDisconnect() { 722 | // Remove all the listeners we added and destroy the socket 723 | delete websocket.onmessage; 724 | delete websocket.onclose; 725 | self.emit("disconnect"); 726 | } 727 | this.disconnect = onDisconnect; 728 | } 729 | 730 | BrowserTransport.prototype.send = function (message) { 731 | // Uncomment to debug protocol 732 | this.debug && console.log("->", message); 733 | var data; 734 | try { data = msgpack.encode(message); } 735 | catch (err) { return this.emit("error", err); } 736 | this.websocket.send(data); 737 | }; 738 | 739 | exports.EngineIoTransport = EngineIoTransport; 740 | inherits(EngineIoTransport, Transport); 741 | function EngineIoTransport(socket, debug) { 742 | var self = this; 743 | this.debug = debug; 744 | 745 | // Route errors from socket to transport. 746 | socket.on("error", function (err) { 747 | self.emit("error", err); 748 | }); 749 | 750 | // Parse and route messages from socket to transport. 751 | socket.on("message", function (json) { 752 | var message; 753 | try { 754 | message = JSON.parse(json); 755 | } 756 | catch (err) { 757 | self.emit("error", err); 758 | return; 759 | } 760 | if (Array.isArray(message)) { 761 | if (debug) { 762 | console.log("<-", message); 763 | } 764 | self.emit("message", message); 765 | } 766 | else { 767 | self.emit("legacy", message); 768 | } 769 | }); 770 | 771 | // Route close events as disconnect events 772 | socket.on("close", function (reason) { 773 | self.emit("disconnect", reason); 774 | }); 775 | 776 | this.disconnect = function () { 777 | socket.close(); 778 | }; 779 | 780 | // Encode and route send calls to socket. 781 | this.send = function (message) { 782 | var json; 783 | try { 784 | json = JSON.stringify(message); 785 | } 786 | catch (err) { 787 | self.emit("error", err); 788 | return; 789 | } 790 | if (this.debug && Array.isArray(message)) { 791 | console.log("->", message); 792 | } 793 | return socket.send(json); 794 | }; 795 | 796 | } 797 | 798 | 799 | return exports; 800 | }); 801 | -------------------------------------------------------------------------------- /test-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd tests 3 | npm install 4 | echo "\nChecking message deframer..." && \ 5 | node test-framer.js && \ 6 | echo "\nChecking message scrubber..." && \ 7 | node test-scrubber.js && \ 8 | echo "\nChecking Agent interface..." && \ 9 | node test-agent.js && \ 10 | echo "\nChecking for memory leaks..." && \ 11 | node test-memory-leaks.js && \ 12 | echo "\nTesting in browser..." && \ 13 | echo "DISABLED: phantomjs doesn't support binary-websockets yet" 14 | # node test-browser.js 15 | -------------------------------------------------------------------------------- /tests/README: -------------------------------------------------------------------------------- 1 | To run these tests, grab the server's dependencies with `npm install`. You may need to `npm install -g jam` first. 2 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | // Mini test framework for async tests. 2 | var assert = require('assert'); 3 | global.setImmediate = global.setImmediate || process.nextTick; 4 | 5 | var expectations = {}; 6 | function expect(message) { expectations[message] = new Error("Missing expectation: " + message); } 7 | function fulfill(message) { delete expectations[message]; } 8 | process.addListener('exit', function () { 9 | Object.keys(expectations).forEach(function (message) { 10 | throw expectations[message]; 11 | }); 12 | }); 13 | 14 | global.assert = assert; 15 | global.expect = expect; 16 | global.fulfill = fulfill; 17 | 18 | var Stream = require('stream').Stream; 19 | var Transport = require('..').Transport; 20 | 21 | // Make a fake pipe pair for testing. 22 | global.makePair = function makePair(a, b, log) { 23 | var left = new Stream(); 24 | var right = new Stream(); 25 | left.writable = true; 26 | left.readable = true; 27 | right.writable = true; 28 | right.readable = true; 29 | left.write = function (chunk) { 30 | setImmediate(function () { 31 | if (log) console.log(a,"->",b,chunk); 32 | right.emit("data", chunk); 33 | }); 34 | }; 35 | right.write = function (chunk) { 36 | setImmediate(function () { 37 | if (log) console.log(b,"->",a,chunk); 38 | left.emit("data", chunk); 39 | }); 40 | }; 41 | var pair = {}; 42 | pair[a] = new Transport(left); 43 | pair[b] = new Transport(right); 44 | return pair; 45 | } 46 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smith-browsertest", 3 | "prvate": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "install": "jam install" 7 | }, 8 | "dependencies": { 9 | "ws": "~0.4.21", 10 | "stack": "~0.1.0", 11 | "creationix": "~0.3.1", 12 | "jamjs": "~0.2.8" 13 | }, 14 | "jam": { 15 | "packageDir": "public/jam", 16 | "baseUrl": "public", 17 | "dependencies": { 18 | "msgpack-js": "~0.1.2" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/phantom.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env phantomjs 2 | 3 | var url = require('system').env.URL; 4 | console.log('Loading ' + url + ' in headless browser...'); 5 | 6 | var page = require('webpage').create(); 7 | 8 | page.onConsoleMessage = function (msg) { 9 | console.log(msg); 10 | }; 11 | 12 | page.onError = function (msg, trace) { 13 | console.log(msg); 14 | trace.forEach(function(item) { 15 | console.log(' ', item.file, ':', item.line); 16 | }); 17 | phantom.exit(1); 18 | }; 19 | 20 | page.onLoadFinished = function (status) { 21 | if (status !== "success") { 22 | console.log("page.open failed"); 23 | phantom.exit(2); 24 | } 25 | }; 26 | 27 | page.open(url); 28 | 29 | -------------------------------------------------------------------------------- /tests/public/.gitignore: -------------------------------------------------------------------------------- 1 | jam 2 | -------------------------------------------------------------------------------- /tests/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Smith 6 | 17 | 18 | 19 |

Smith

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
TestResult
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/public/smith.js: -------------------------------------------------------------------------------- 1 | ../../smith.js -------------------------------------------------------------------------------- /tests/public/test.js: -------------------------------------------------------------------------------- 1 | // PhantomJS doesn't support bind yet 2 | Function.prototype.bind = Function.prototype.bind || function (thisp) { 3 | var fn = this; 4 | return function () { 5 | return fn.apply(thisp, arguments); 6 | }; 7 | }; 8 | 9 | var body = document.querySelector("tbody"); 10 | var expectations = {}; 11 | function expect(name) { 12 | expectations[name] = setTimeout(function () { 13 | post("expected '" + name + "'", "timeout", false); 14 | }, 1000); 15 | } 16 | function fulfill(name) { 17 | post("fulfilled '" + name + "'", name, true); 18 | clearTimeout(expectations[name]); 19 | } 20 | 21 | function fail(err) { 22 | post(err.message, err.stack, false); 23 | } 24 | function assert(name, value) { 25 | post(name, value, value); 26 | } 27 | function assertEqual(name, expected, actual) { 28 | post(name + " === " + expected, actual + " === " + expected, expected === actual); 29 | } 30 | 31 | function post(name, result, pass) { 32 | var tr = document.createElement('tr'); 33 | tr.setAttribute("class", pass ? "passed" : "failed"); 34 | var td = document.createElement('td'); 35 | td.textContent = name; 36 | tr.appendChild(td); 37 | td = document.createElement('td'); 38 | td.textContent = result; 39 | tr.appendChild(td); 40 | body.appendChild(tr); 41 | if (pass) console.log(name); 42 | else throw new Error(name + "\n" + result); 43 | } 44 | 45 | 46 | expect("smith loads"); 47 | require(["smith"], function (smith) { 48 | fulfill("smith loads"); 49 | 50 | 51 | ////////////////////////////////////////////////////////////////////////// 52 | 53 | var Agent = smith.Agent; 54 | var BrowserTransport = smith.BrowserTransport; 55 | 56 | assertEqual("typeof smith", "object", typeof smith); 57 | assertEqual("typeof smith.Agent", "function", typeof smith.Agent); 58 | assertEqual("typeof smith.BrowserTransport", "function", typeof smith.BrowserTransport); 59 | 60 | var agent = new Agent(); 61 | 62 | assert("agent instanceof smith.Agent", agent instanceof smith.Agent); 63 | 64 | expect("socket opened"); 65 | var ws = new WebSocket(window.location.origin.replace(/^http/, "ws") + "/"); 66 | ws.onopen = function () { 67 | fulfill("socket opened"); 68 | expect("agent connected"); 69 | agent.connect(new BrowserTransport(ws, true), function (err, serverAgent) { 70 | if (err) return fail(err); 71 | fulfill("agent connected"); 72 | assertEqual("typeof serverAgent", "object", typeof serverAgent); 73 | expect("called add"); 74 | serverAgent.add(5, 7, function (err, result) { 75 | if (err) return fail(err); 76 | fulfill("called add"); 77 | assertEqual("5 + 7", 12, result); 78 | }); 79 | }); 80 | }; 81 | 82 | ////////////////////////////////////////////////////////////////////////// 83 | 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /tests/test-agent.js: -------------------------------------------------------------------------------- 1 | require('./helpers'); 2 | var Agent = require('..').Agent; 3 | var Transport = require('..').Transport; 4 | 5 | var a = new Agent({ 6 | add: function (a, b, callback) { 7 | callback(a + b); 8 | } 9 | }); 10 | var b = new Agent(); 11 | process.nextTick(testFakeTransport) 12 | 13 | expect("test1"); 14 | function testFakeTransport() { 15 | fulfill("test1"); 16 | console.log("Testing fake transport"); 17 | var pair = makePair("A", "B", true) 18 | expect("connect AB"); 19 | a.connect(pair.A, function (err, AB) { 20 | if (err) throw err; 21 | fulfill("connect AB"); 22 | console.log("A is connected to B!"); 23 | }); 24 | expect("connect BA"); 25 | b.connect(pair.B, function (err, BA) { 26 | if (err) throw err; 27 | fulfill("connect BA"); 28 | console.log("B is connected to A!"); 29 | expect("result"); 30 | BA.add(1, 2, function (result) { 31 | fulfill("result"); 32 | console.log("Result", result); 33 | assert.equal(result, 3); 34 | testSocketTransport(); 35 | }); 36 | }); 37 | } 38 | 39 | expect("alldone"); 40 | expect("test2"); 41 | function testSocketTransport() { 42 | console.log("Test 2 using real tcp server"); 43 | fulfill("test2"); 44 | var net = require('net'); 45 | expect("connect1"); 46 | var server = net.createServer(function (socket) { 47 | fulfill("connect1"); 48 | socket.on('data', function (chunk) { 49 | console.log("B->A (%s):", chunk.length, chunk); 50 | }); 51 | expect("connectAB"); 52 | a.connect(new Transport(socket), function (err, AB) { 53 | if (err) throw err; 54 | fulfill("connectAB"); 55 | console.log("A is connected to B!"); 56 | }); 57 | console.log("connection"); 58 | }); 59 | server.listen(function () { 60 | var port = server.address().port; 61 | expect("connect2"); 62 | var socket = net.connect(port, function () { 63 | fulfill("connect2"); 64 | expect("connectBA"); 65 | b.connect(new Transport(socket), function (err, BA) { 66 | if (err) throw err; 67 | fulfill("connectBA"); 68 | console.log("B is connected to A!"); 69 | expect("result2"); 70 | BA.add(1, 2, function (result) { 71 | fulfill("result2"); 72 | console.log("Result", result); 73 | assert.equal(result, 3); 74 | socket.end(); 75 | server.close(); 76 | fulfill("alldone"); 77 | }); 78 | }); 79 | }); 80 | socket.on("data", function (chunk) { 81 | console.log("A->B (%s):", chunk.length, chunk); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /tests/test-browser.js: -------------------------------------------------------------------------------- 1 | var creationix = require('creationix'); 2 | var stack = require('stack'); 3 | var http = require('http'); 4 | var WebSocketServer = require('ws').Server 5 | var spawn = require('child_process').spawn; 6 | 7 | var Agent = require('smith').Agent; 8 | var WebSocketTransport = require('smith').WebSocketTransport 9 | 10 | 11 | var api = { 12 | add: function (a, b, callback) { 13 | callback(null, a + b); 14 | } 15 | }; 16 | 17 | var server = http.createServer(stack( 18 | creationix.log(), 19 | creationix.static("/", __dirname + "/public") 20 | )); 21 | 22 | var wss = new WebSocketServer({server: server}); 23 | wss.on("connection", function (websocket) { 24 | var agent = new Agent(api); 25 | agent.connect(new WebSocketTransport(websocket, true), function (err, browserAgent) { 26 | if (err) throw err; 27 | console.log({browserAgent:browserAgent}); 28 | }); 29 | }); 30 | 31 | server.listen(8080, function () { 32 | var url = "http://localhost:" + server.address().port + "/index.html"; 33 | console.log(__dirname + "/phantom.js") 34 | var env = Object.create(process.env); 35 | env.URL = url; 36 | console.log(url); 37 | var phantom = spawn("phantomjs", [__dirname + "/phantom.js"], {env: env, customFds: [-1, 1, 2]}); 38 | phantom.on("exit", function (code, signal) { 39 | if (code) throw new Error("Child died with code " + code); 40 | }); 41 | 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /tests/test-framer.js: -------------------------------------------------------------------------------- 1 | require('./helpers'); 2 | var deFramer = require('..').deFramer; 3 | 4 | // Given an array of message buffers, this returns a single buffer that contains 5 | // all the messages framed. 6 | function frameMessages(messages) { 7 | var i, l = messages.length; 8 | 9 | // Calculate total size of final buffer 10 | var total = l * 10; 11 | for (i = 0; i < l; i++) { 12 | total += messages[i].length; 13 | } 14 | 15 | // Create and fill in final buffer 16 | var buffer = new Buffer(total); 17 | var offset = 0; 18 | for (i = 0; i < l; i++) { 19 | var message = messages[i]; 20 | var length = message.length; 21 | buffer.writeUInt32BE(length, offset); 22 | 23 | // Compute 4 byte hash 24 | var a = length >> 24, 25 | b = (length >> 16) & 0xff, 26 | c = (length >> 8) & 0xff, 27 | d = length & 0xff; 28 | 29 | // Little bit inlined, but fast 30 | var hash = 0; 31 | hash += a; 32 | hash += hash << 10; 33 | hash += hash >> 6; 34 | hash += b; 35 | hash += hash << 10; 36 | hash += hash >> 6; 37 | hash += c; 38 | hash += hash << 10; 39 | hash += hash >> 6; 40 | hash += d; 41 | hash += hash << 10; 42 | hash += hash >> 6; 43 | 44 | // Shuffle bits 45 | hash += hash << 3; 46 | hash = hash ^ (hash >> 11); 47 | hash += hash << 15; 48 | hash |= 0; 49 | buffer.writeInt32BE(hash, offset + 4); 50 | 51 | // Reserved bytes 52 | buffer.writeUInt16BE(0, offset + 8); 53 | 54 | message.copy(buffer, offset + 10); 55 | offset += length + 10; 56 | } 57 | 58 | return buffer; 59 | }; 60 | 61 | // Test the de-framer by creating a sample message stream and simulating packet 62 | // sizes from one-byte-per-packet to all-messages-in-one-packet. 63 | var input = [ 64 | {hello: "world"}, 65 | {Goodbye: "Sanity"}, 66 | [1,2,3,4,5,6,7,6,5,4,3,2,1], 67 | 68 | // Big string that will use multiple bytes for length 69 | // (Regression test for hashing) 70 | new Array(300).join('A') 71 | ]; 72 | var message = frameMessages(input.map(function (item) { 73 | return new Buffer(JSON.stringify(item)); })); 74 | var length = message.length; 75 | for (var step = 1; step < length; step++) { 76 | var output = []; 77 | var parser = deFramer(function (message) { 78 | output.push(JSON.parse(message.toString())); 79 | }); 80 | for (var offset = 0; offset < length; offset += step) { 81 | var end = offset + step 82 | if (end > length) { end = length; } 83 | var chunk = message.slice(offset, end); 84 | console.log(chunk); 85 | parser(chunk); 86 | } 87 | assert.deepEqual(input, output); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /tests/test-memory-leaks.js: -------------------------------------------------------------------------------- 1 | require('./helpers'); 2 | var Agent = require('..').Agent; 3 | var Transport = require('..').Transport; 4 | 5 | var a = new Agent({ 6 | add: function (a, b, callback) { 7 | callback(a + b); 8 | } 9 | }); 10 | var b = new Agent(); 11 | var samples = []; 12 | 13 | var pair = makePair("A", "B"); 14 | a.connect(pair.A, function (err, AB) { 15 | if (err) throw err; 16 | console.log("A is connected to B!"); 17 | }); 18 | b.connect(pair.B, function (err, BA) { 19 | if (err) throw err; 20 | console.log("B is connected to A!"); 21 | var left = 300000; 22 | for (var i = 0; i < 100; i++) { 23 | test(); 24 | } 25 | 26 | function test() { 27 | BA.add(1, 2, function (result) { 28 | assert.equal(result, 3); 29 | if (left % 10000 === 0) { 30 | var sample = process.memoryUsage(); 31 | console.log(sample); 32 | samples.push(sample); 33 | } 34 | if (--left > 0) test(); 35 | else if (left === 0) done(); 36 | }); 37 | } 38 | }); 39 | 40 | 41 | expect("done"); 42 | function done() { 43 | // Trim the first few samples to not include startup time 44 | samples = samples.slice(4); 45 | getSlope("rss"); 46 | fulfill("done"); 47 | } 48 | 49 | function getSlope(key) { 50 | var sum = 0; 51 | var max = 0; 52 | var min = Infinity; 53 | samples.forEach(function (sample) { 54 | var value = sample[key]; 55 | sum += value; 56 | if (value > max) max = value; 57 | if (value < min) min = value; 58 | }); 59 | var mean = sum / samples.length; 60 | var deviation = 0; 61 | samples.forEach(function (sample) { 62 | var diff = mean - sample[key]; 63 | deviation += diff * diff; 64 | }); 65 | deviation = Math.sqrt(deviation / (samples.length - 1)); 66 | var limit = mean / 10; 67 | console.log("%s: min %s, mean %s, max %s, standard deviation %s", key, min, mean, max, deviation); 68 | if (deviation > limit) { 69 | throw new Error("Deviation for " + key + " over " + limit + ", probably a memory leak"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/test-scrubber.js: -------------------------------------------------------------------------------- 1 | require('./helpers'); 2 | var freeze = require('..').freeze; 3 | var liven = require('..').liven; 4 | var getType = require('..').getType; 5 | 6 | function foo() {} 7 | var cycle = {a:true,b:false, d:[1,2,3]}; 8 | cycle.e = cycle.d; 9 | cycle.cycle = cycle; 10 | var pairs = [ 11 | [{$:1,$$:2,$$$:3,$$$$:4}, {$$:1,$$$:2,$$$$:3,$$$$$:4}], 12 | [true, true], 13 | [false, false], 14 | [null, null], 15 | [undefined, undefined], 16 | [42, 42], 17 | [foo, {$:1}], 18 | [[1,2,3,foo],[1,2,3,{$:2}]], 19 | ["Hello", "Hello"], 20 | [new Buffer([0,1,2,3,4,5,6]), new Buffer([0,1,2,3,4,5,6])], 21 | [{fn:foo}, {fn:{$:3}}], 22 | [cycle, {a:true,b:false,d:[1,2,3],e:{$:["d"]},cycle:{$:[]}}], 23 | [new Date("Sun, 28 Mar 1982 11:46:00 MST"), {$:{d:0x59eaaaee40}}] 24 | ]; 25 | 26 | var functions = {}; 27 | var nextKey = 0; 28 | function storeFunction(fn) { 29 | var id = ++nextKey; 30 | functions[id] = fn; 31 | return id; 32 | } 33 | function getFunction(id) { 34 | var fn = functions[id]; 35 | delete functions[id]; 36 | return fn; 37 | } 38 | 39 | pairs.forEach(function (pair) { 40 | var live = pair[0]; 41 | var dead = pair[1]; 42 | console.log("testing", pair); 43 | var frozen = freeze(live, storeFunction); 44 | if (!deepEqual(frozen, dead)) { 45 | console.error({actual:frozen,expected:dead}); 46 | throw new Error("freeze fail"); 47 | } 48 | var relive = liven(frozen, getFunction); 49 | if (!deepEqual(relive, live)) { 50 | console.error({actual:relive,expected:live}); 51 | throw new Error("liven fail"); 52 | } 53 | }); 54 | 55 | function deepEqual(a, b) { 56 | var seen = []; 57 | function find(a, b) { 58 | if (a === b) return true; 59 | var type = getType(a); 60 | if (getType(b) !== type) return false; 61 | if (type === "buffer" || type === "date") return a.toString() === b.toString(); 62 | if (type !== "object" && type !== "array") return a === b; 63 | 64 | // Ignore cycles for now 65 | // TODO: this isn't enough 66 | if (seen.indexOf(a) >= 0) { 67 | return true; 68 | } 69 | seen.push(a); 70 | 71 | if (type === "array") { 72 | if (a.length !== b.length) return false; 73 | for (var i = 0, l = a.length; i < l; i++) { 74 | if (!find(a[i], b[i])) return false; 75 | } 76 | return true; 77 | } 78 | var keys = Object.getOwnPropertyNames(a); 79 | if (!deepEqual(keys, Object.getOwnPropertyNames(b))) return false; 80 | for (var i = 0, l = keys.length; i < l; i++) { 81 | var key = keys[i]; 82 | if (!find(a[key],b[key])) return false; 83 | } 84 | return true; 85 | } 86 | return find(a, b); 87 | } 88 | --------------------------------------------------------------------------------