├── .gitignore ├── .gitmodules ├── .npmignore ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── docs └── index.restdown ├── examples ├── example.d └── server.js ├── lib ├── client.js ├── index.js ├── protocol │ ├── dtrace.js │ ├── index.js │ ├── message_decoder.js │ ├── message_encoder.js │ ├── protocol.js │ ├── rpc_decoder.js │ └── rpc_encoder.js └── server.js ├── package.json ├── test ├── client.test.js ├── message.test.js ├── rpc.test.js └── test.js └── tools ├── jsl.node.conf ├── jsstyle.conf └── mk ├── Makefile.defs ├── Makefile.deps └── Makefile.targ /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /tmp 4 | build 5 | docs/*.json 6 | docs/*.html 7 | cscope.in.out 8 | cscope.po.out 9 | cscope.out 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/jsstyle"] 2 | path = deps/jsstyle 3 | url = git://github.com/davepacheco/jsstyle.git 4 | [submodule "deps/restdown"] 5 | path = deps/restdown 6 | url = git://github.com/trentm/restdown.git 7 | [submodule "deps/javascriptlint"] 8 | path = deps/javascriptlint 9 | url = git://github.com/davepacheco/javascriptlint.git 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .dir-locals.el 2 | .git* 3 | .travis.yml 4 | build 5 | .coverage_data 6 | cover_html 7 | deps 8 | docs 9 | examples 10 | node_modules 11 | tools 12 | Makefile* 13 | *.tar.gz 14 | *.log 15 | coverage 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # node-fast Changelog 2 | 3 | ## 0.5.3 4 | 5 | - Fix case where client blows assertion on `close()` 6 | 7 | ## 0.5.2 8 | 9 | - Clear client connect timeout when `close()` is called 10 | 11 | ## 0.5.1 12 | 13 | - Fix client to properly clean up pending socket when `close()` called while a 14 | TCP connection is being established. 15 | 16 | ## 0.5.0 17 | 18 | - Remove domains from server API 19 | - Update dependencies 20 | - Drop node 0.8.x compatibility 21 | - Improve server error behavior for ended/canceled RPCs 22 | 23 | ## 0.4.1 24 | 25 | - Update dtrace-provider to 0.3.0 26 | 27 | ## 0.4.0 28 | 29 | - Add support for canceling in-progress RPCs 30 | - Force missing RPC endpoint errors in server 31 | * This was formerly optional behavior enabled by checkDefined in versions 32 | 0.3.9 and 0.3.10. Now, any RPC to a missing endpoint will result in an 33 | error emitted to the client. 34 | - Change dtrace-provider to optional dependency 35 | - Update microtime to 1.0.1 36 | - Add client property for pending request count 37 | 38 | ## 0.3.10 39 | 40 | - MANTA-2315: Simplify socket error/close events 41 | 42 | ## 0.3.9 43 | 44 | - Add optional error when calling undefined RPC 45 | * When server is created with checkDefined parameter enabled, RPC calls to 46 | non-existent endpoint will result in an error being emitted to the client. 47 | - Change tests to tape/faucet/istanbul 48 | - Update node-backoff to 2.4.0 49 | - MANTA-2315: Improve connect attempt error events 50 | - Fix #8: server throwing EPIPE when connection is closed 51 | - Expose connection for requests 52 | 53 | ## 0.3.8 54 | 55 | - MANTA-1987: Fix race in connection close 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Mark Cavage, All rights reserved. 2 | Copyright 2015 Joyent, Inc. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2012, Mark Cavage. All rights reserved. 3 | 4 | # 5 | # Tools 6 | # 7 | ISTANBUL := ./node_modules/.bin/istanbul 8 | FAUCET := ./node_modules/.bin/faucet 9 | NPM := npm 10 | 11 | # 12 | # Files 13 | # 14 | DOC_FILES = index.restdown 15 | JS_FILES := $(shell find lib test -name '*.js') 16 | JSL_CONF_NODE = tools/jsl.node.conf 17 | JSL_FILES_NODE = $(JS_FILES) 18 | JSSTYLE_FILES = $(JS_FILES) 19 | JSSTYLE_FLAGS = -f tools/jsstyle.conf 20 | 21 | include ./tools/mk/Makefile.defs 22 | 23 | # 24 | # Repo-specific targets 25 | # 26 | .PHONY: all 27 | all: $(ISTANBUL) $(REPO_DEPS) 28 | $(NPM) rebuild 29 | 30 | $(ISTANBUL): | $(NPM_EXEC) 31 | $(NPM) install 32 | 33 | CLEAN_FILES += ./node_modules ./coverage 34 | 35 | .PHONY: test 36 | test: $(ISTANBUL) 37 | $(ISTANBUL) cover --print none test/test.js | $(FAUCET) 38 | 39 | include ./tools/mk/Makefile.deps 40 | include ./tools/mk/Makefile.targ 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `fast` is a very small JSON over TCP messaging framework. Effectively, it lets 2 | you write RPC systems that "stream" many results back for a single message (not in 3 | the sense of a streaming JSON parser, but in the sense of many objects that are 4 | correlated). For example: 5 | 6 | var fast = require('fast'); 7 | var server = fast.createServer(); 8 | 9 | server.rpc('echo', function (fname, lname, res) { 10 | res.write({first: fname}); 11 | res.end({last: lname}); 12 | }); 13 | 14 | server.listen(1234); 15 | 16 | /// Client 17 | var client = fast.createClient({host: 'localhost', port: 1234}); 18 | client.on('connect', function () { 19 | var req = client.rpc('echo', 'mark', 'cavage'); 20 | req.on('message', function (obj) { 21 | console.log(JSON.stringify(obj, null, 2)); 22 | }); 23 | req.on('end', function () { 24 | client.close(); 25 | server.close(); 26 | }); 27 | }); 28 | 29 | 30 | While does what you think it does. A few things to note: 31 | 32 | * There's a "gentlemen's agreement" in argument reconstruction. Whatever you 33 | pass client side as arguments shows up, in that order, server side. So in 34 | the example above, note that `server.rpc('echo', function (f, l, res) {})`, 35 | gave us the client's set of strings and a `res` object you use to kick back 36 | results on as the last argument. It just does that. 37 | * Whatever you send back server side shows up on the client `.on('message')` 38 | the same as the server. So above, I sent back an object, but you can send 39 | back anything, and the arguments will "line up". 40 | * Server-side, you can send data either via write or end (as above). Also, if 41 | you pass something that `instanceof Error` returns true on, that gets 42 | spit out as a `req.on('error', function (err) {})` client-side. 43 | 44 | That's pretty much it. This needs a lot more docs, but for now, I'm throwing 45 | this up on github as-is, and I'll add more over time. 46 | 47 | # Installation 48 | 49 | npm install fast 50 | 51 | # Protocol 52 | 53 | Basically, I cooked a small header+data payload like this: 54 | 55 | ``` 56 | Byte/ 0 | 1 | 2 | 3 | 57 | / | | | | 58 | |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| 59 | +---------------+---------------+---------------+---------------+ 60 | 0|Version |Type |Status |MessageID 61 | +---------------+---------------+---------------+---------------+ 62 | 4| |CRC16 63 | +---------------+---------------+---------------+---------------+ 64 | 8| |DataLen 65 | +---------------+---------------+---------------+---------------+ 66 | 12| |Data... 67 | +---------------+---------------+---------------+---------------+ 68 | 16|... 69 | ``` 70 | 71 | Where: 72 | 73 | * Version: Currently always `0x01` 74 | * Type: Currently always `0x01` (Means JSON -> may add GZIP JSON, etc., later) 75 | * Status: An enum to reflect the what this message is in the sequence: 76 | * 0x01: `data`: More messages to come 77 | * 0x02: `end`: No more messages to come (All is well) 78 | * 0x03: `error`: No more messages to come; error returned from server in `data` 79 | * MessageID: A 32-bit UInt32 (big endian encoded) from 1 - (2^32 − 1). A 80 | client sets this initially, and all messages returned from the server to the 81 | client that correspond to the request must carry the same messageID. 82 | * CRC16: CRC16 of the data, encoded as a 32bit signed integer (big endian) 83 | * DataLen: 32-bit UInt32, encoded big endian. 84 | * Data: JSON-encoded data payload. 85 | 86 | On top of that, there is "moar gentlemenly agreement" of what "data" looks like 87 | to facilitate RPC. Basically, `data` is a JSON object like this: 88 | 89 | { 90 | m: { 91 | name: 'echo', 92 | uts: gettimeofday(2) // microseconds since epoch 93 | }, 94 | d: [] // "arguments" to JS function 95 | } 96 | 97 | That's pretty much it. Note there is effectively no try/catch or anything like 98 | that in this framework, as it's intended to be run "carefully". If it's too 99 | problematic I'll add that, but clearly this is meant to do one thing: go fast 100 | from internal service A to internal service B. YMMV. 101 | 102 | ## CRC Woes 103 | It turns out that the CRC implementation used by node-fast relies on its string 104 | input being 7-bit clean. Because UTF-8 stringified data is being passed to it, 105 | instead of a buffer, this constraint is violated and the calculation result is 106 | _not_ equivalent to a "real" CRC of the bytewise data. This issue could be 107 | fixed with an updated CRC routine, but doing so will render communication 108 | impossible between new and old implementations. 109 | 110 | # Licence 111 | 112 | MIT 113 | -------------------------------------------------------------------------------- /docs/index.restdown: -------------------------------------------------------------------------------- 1 | # TODO 2 | -------------------------------------------------------------------------------- /examples/example.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | #pragma D option quiet 3 | 4 | fast*:::rpc-start 5 | { 6 | tracker[arg1] = timestamp; 7 | } 8 | 9 | fast*:::rpc-done 10 | /tracker[arg1]/ 11 | { 12 | @[copyinstr(arg0)] = quantize(((timestamp - tracker[arg1]) / 1000000)); 13 | tracker[arg1] = 0 14 | } 15 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Mark Cavage. All rights reserved. 2 | 3 | var fast = require('../lib'); 4 | 5 | 6 | var server = fast.createServer(); 7 | 8 | server.rpc('echo', function (name, res) { 9 | res.write({user: name}); 10 | res.end(); 11 | }); 12 | 13 | server.listen(1234); 14 | 15 | var client = fast.createClient({host: '127.0.0.1', port: 1234}); 16 | client.on('connect', function client_run() { 17 | var req = client.rpc('echo', process.env.USER); 18 | req.on('message', function (obj) { 19 | if (process.env.DEBUG) 20 | console.log(JSON.stringify(obj, null, 2)); 21 | }); 22 | req.on('end', function () { 23 | client_run(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var dns = require('dns'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var net = require('net'); 6 | var util = require('util'); 7 | 8 | var assert = require('assert-plus'); 9 | var backoff = require('backoff'); 10 | var once = require('once'); 11 | var WError = require('verror').WError; 12 | 13 | var protocol = require('./protocol'); 14 | 15 | 16 | 17 | ///--- Globals 18 | 19 | var slice = Function.prototype.call.bind(Array.prototype.slice); 20 | var sprintf = util.format; 21 | 22 | /* JSSTYLED */ 23 | var IP_RE = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 24 | var MAX_MSGID = Math.pow(2, 31) - 1; 25 | var MSGID = 0; 26 | 27 | 28 | 29 | ///--- Errors 30 | 31 | function ConnectionClosedError(msg) { 32 | WError.call(this, msg || 'the underlying connection has been closed'); 33 | } 34 | util.inherits(ConnectionClosedError, WError); 35 | ConnectionClosedError.prototype.name = 'ConnectionClosedError'; 36 | 37 | 38 | function ConnectionTimeoutError(time) { 39 | WError.call(this, 'failed to establish connection after %dms', time); 40 | } 41 | util.inherits(ConnectionTimeoutError, WError); 42 | ConnectionTimeoutError.prototype.name = 'ConnectionTimeoutError'; 43 | 44 | 45 | function DNSError(err, host) { 46 | WError.call(this, err, host + ' could not be found in DNS'); 47 | } 48 | util.inherits(DNSError, WError); 49 | DNSError.prototype.name = 'DNSError'; 50 | 51 | 52 | function NoConnectionError() { 53 | WError.call(this, 'no connection'); 54 | } 55 | util.inherits(NoConnectionError, WError); 56 | NoConnectionError.prototype.name = 'NoConnectionError'; 57 | 58 | 59 | function UnsolicitedMessageError(message) { 60 | WError.call(this, 'unsolicited message'); 61 | this.msg = message; 62 | } 63 | util.inherits(UnsolicitedMessageError, WError); 64 | 65 | 66 | 67 | ///--- Helpers 68 | 69 | function clone(obj) { 70 | if (!obj) { 71 | return (obj); 72 | } 73 | var copy = {}; 74 | Object.keys(obj).forEach(function (k) { 75 | copy[k] = obj[k]; 76 | }); 77 | return (copy); 78 | } 79 | 80 | 81 | function cleanupListener(l) { 82 | l.removeAllListeners('close'); 83 | l.removeAllListeners('data'); 84 | l.removeAllListeners('drain'); 85 | l.removeAllListeners('end'); 86 | l.removeAllListeners('error'); 87 | l.removeAllListeners('timeout'); 88 | } 89 | 90 | 91 | function shuffle(array) { 92 | var current; 93 | var tmp; 94 | var top = array.length; 95 | 96 | if (top) { 97 | while (--top) { 98 | current = Math.floor(Math.random() * (top + 1)); 99 | tmp = array[current]; 100 | array[current] = array[top]; 101 | array[top] = tmp; 102 | } 103 | } 104 | 105 | return (array); 106 | } 107 | 108 | 109 | // Blackhole for canceled requests 110 | var blackhole = new EventEmitter(); 111 | blackhole.on('error', function () {}); 112 | 113 | 114 | 115 | ///--- API 116 | 117 | function Client(options) { 118 | assert.object(options, 'options'); 119 | assert.number(options.connectTimeout, 'options.connectTimeout'); 120 | assert.string(options.host, 'options.host'); 121 | assert.number(options.port, 'options.port'); 122 | assert.object(options.retry, 'options.retry'); 123 | 124 | EventEmitter.call(this); 125 | 126 | var self = this; 127 | this.fast_msgid = 0; 128 | this.fast_conn = null; 129 | this._fast_connect_timeout = null; 130 | this.fast_requests = {}; 131 | this._pending_requests = 0; 132 | this._options = options; 133 | 134 | this.__defineGetter__('countPending', function () { 135 | return (self._pending_requests); 136 | }); 137 | 138 | if (options.reconnect) { 139 | var r = options.reconnect; 140 | var num = (typeof (r) === 'number' ? r : 1000); 141 | 142 | this.fast_reconnect = function () { 143 | self.fast_timer = setTimeout(function () { 144 | self.connect(); 145 | }, num); 146 | }; 147 | } else { 148 | this.fast_reconnect = false; 149 | } 150 | 151 | this.connect(); 152 | } 153 | util.inherits(Client, EventEmitter); 154 | 155 | 156 | Client.prototype.close = function close() { 157 | this.closed = true; 158 | this.fast_reconnect = false; 159 | 160 | if (this._fast_connect_timeout) { 161 | clearTimeout(this._fast_connect_timeout); 162 | this._fast_connect_timeout = null; 163 | } 164 | 165 | if (this.fast_conn) { 166 | this.fast_conn.destroy(); 167 | } else { 168 | // Un-wire and destroy any pending connection 169 | if (this._fast_pending_conn) { 170 | // Swallow socket events as we assume the user doesn't care about 171 | // state changes after calling close(). 172 | this._fast_pending_conn.removeAllListeners(); 173 | this._fast_pending_conn.on('error', function () {}); 174 | this._fast_pending_conn.destroy(); 175 | this._fast_pending_conn = null; 176 | } 177 | // Stop looping for any pending connection attempts 178 | clearTimeout(this.fast_timer); 179 | if (this._fast_retry) 180 | this._fast_retry.abort(); 181 | setImmediate(this.emit.bind(this, 'close')); 182 | } 183 | }; 184 | 185 | 186 | Client.prototype.connect = function connect() { 187 | if (this._fast_retry) 188 | throw new Error('already connecting'); 189 | 190 | var self = this; 191 | this.closed = false; 192 | var max = Infinity; 193 | var opts = this._options; 194 | var retry = backoff.call(this._createSocket.bind(this), {}, 195 | function (err, conn) { 196 | // Starting with backoff 2.5.0, the backoff callback is called when the 197 | // backoff instance is aborted. In this case, the _onConnection callback 198 | // should not be called, since its purpose is to handle the result of 199 | // the bind call that is being backed off, not events in the backoff 200 | // process itself. 201 | if (!retry.isAborted()) { 202 | self._onConnection(err, conn); 203 | } 204 | }); 205 | 206 | retry.on('backoff', this.emit.bind(this, 'connectAttempt')); 207 | retry.setStrategy(new backoff.ExponentialStrategy({ 208 | initialDelay: opts.retry.minTimeout || 1000, 209 | maxDelay: opts.retry.maxTimeout || Infinity 210 | })); 211 | 212 | if (typeof (opts.retry.retries) === 'number') 213 | max = opts.retry.retries; 214 | 215 | retry.failAfter(max); 216 | 217 | this._fast_retry = retry; 218 | this._fast_retry.start(); 219 | }; 220 | 221 | 222 | Client.prototype._createSocket = function _createSocket(_, cb) { 223 | var self = this; 224 | var options = this._options; 225 | var callback = once(function (err, res) { 226 | if (err && !self.closed) { 227 | self.emit('connectError', err); 228 | } 229 | cb(err, res); 230 | }); 231 | 232 | function _socket() { 233 | var c = net.connect(options); 234 | var to = options.connectTimeout; 235 | 236 | if (options.connectTimeout > 0) { 237 | self._fast_connect_timeout = setTimeout(function () { 238 | c.removeAllListeners('connect'); 239 | c.removeAllListeners('error'); 240 | c.destroy(); 241 | callback(new ConnectionTimeoutError(to)); 242 | }, to); 243 | } 244 | 245 | function done(err, res) { 246 | if (self._fast_connect_timeout) { 247 | clearTimeout(self._fast_connect_timeout); 248 | self._fast_connect_timeout = null; 249 | } 250 | self._fast_pending_conn = null; 251 | callback(err, res); 252 | } 253 | 254 | c.once('connect', function onConnect() { 255 | c.removeAllListeners('error'); 256 | done(null, c); 257 | }); 258 | 259 | c.once('error', function onError(err) { 260 | c.removeAllListeners('connect'); 261 | done(err); 262 | }); 263 | 264 | self._fast_pending_conn = c; 265 | } 266 | 267 | if (IP_RE.test(options.host)) { 268 | _socket(); 269 | } else if (options.host === 'localhost' || options.host === '::1') { 270 | options.host = '127.0.0.1'; 271 | _socket(); 272 | } else { 273 | dns.resolve4(options.host, function (err, addrs) { 274 | if (err) { 275 | callback(new DNSError(err, options.host)); 276 | return; 277 | } else if (!addrs || addrs.length === 0) { 278 | callback(new DNSError(options.host)); 279 | return; 280 | } 281 | 282 | options = clone(options); 283 | options.host = shuffle(addrs).pop(); 284 | _socket(); 285 | }); 286 | } 287 | }; 288 | 289 | 290 | Client.prototype.cancelRequests = function cancelRequests(err) { 291 | var self = this; 292 | Object.keys(this.fast_requests).forEach(function (msgid) { 293 | self.cancel(msgid, err); 294 | }); 295 | }; 296 | 297 | 298 | Client.prototype.cancel = function cancel(msgid, err) { 299 | var req = this.fast_requests[msgid]; 300 | if (!err) { 301 | err = new Error('RPC canceled'); 302 | err.name = 'RPCCanceled'; 303 | } 304 | if (req && req != blackhole) { 305 | req.emit('error', err); 306 | cleanupListener(req); 307 | if (this.fast_conn && this.fast_conn.writable) { 308 | // notify server of canceled RPC 309 | req._encoder.encode(err); 310 | } 311 | // Further responses for this msgid will be blackholed. 312 | // This block will be cleared once an ERROR or END event is received 313 | // from the server or the TCP connection is severed. 314 | req.removeAllListeners(); 315 | this.fast_requests[msgid] = blackhole; 316 | } 317 | }; 318 | 319 | 320 | Client.prototype.rpc = function rpc(method) { 321 | assert.string(method, 'method'); 322 | 323 | var req = new EventEmitter(); 324 | if (!this.fast_conn || 325 | !this.fast_conn.readable || 326 | !this.fast_conn.writable) { 327 | 328 | setImmediate(req.emit.bind(req, 'error', new NoConnectionError())); 329 | return (req); 330 | } 331 | 332 | var msgid = this._nextMessageId(); 333 | var self = this; 334 | var encoder = new protocol.RpcEncoder({ 335 | connection: self.fast_conn, 336 | encoder: self.messageEncoder, 337 | msgid: msgid, 338 | method: method 339 | }); 340 | req._encoder = encoder; 341 | req.cancel = this.cancel.bind(this, msgid); 342 | 343 | encoder.encode.apply(encoder, slice(arguments, 1)); 344 | 345 | this.fast_requests[msgid] = req; 346 | this._pending_requests++; 347 | 348 | return (req); 349 | }; 350 | 351 | 352 | Client.prototype.setTimeout = function setTimeout(timeout) { 353 | assert.number(timeout, 'timeout'); 354 | 355 | if (!this.fast_conn) 356 | throw new NoConnectionError(); 357 | 358 | this.fast_conn.setTimeout(timeout); 359 | }; 360 | 361 | 362 | Client.prototype.toString = function toString() { 363 | var c = this.fast_conn; 364 | var str = sprintf('[object FastClient]', 365 | c ? c.remoteAddress : 'no_host', 366 | c ? c.remotePort : 'no_port'); 367 | 368 | return (str); 369 | }; 370 | 371 | 372 | //-- "private" methods 373 | 374 | Client.prototype._handleMessage = function _handleMessage(msg) { 375 | if (!this.fast_conn) { 376 | this.emit('unhandledMessage', msg); 377 | return; 378 | } 379 | 380 | if (!msg.data || !msg.data.m || !msg.data.d) { 381 | this.emit('error', new Error('bad message')); 382 | return; 383 | } 384 | 385 | var args; 386 | var err; 387 | var req; 388 | 389 | if ((req = this.fast_requests[msg.msgid])) { 390 | switch (msg.status) { 391 | 392 | case protocol.STATUS.DATA: 393 | args = msg.data.d; 394 | args.unshift('message'); 395 | req.emit.apply(req, args); 396 | break; 397 | 398 | case protocol.STATUS.END: 399 | if (msg.data.d.length) { 400 | args = msg.data.d; 401 | args.unshift('message'); 402 | req.emit.apply(req, args); 403 | } 404 | delete this.fast_requests[msg.msgid]; 405 | this._pending_requests--; 406 | req.emit('end'); 407 | cleanupListener(req); 408 | break; 409 | 410 | default: 411 | err = new Error(msg.data.d.message); 412 | err.name = msg.data.d.name; 413 | err.stack = msg.data.d.stack; 414 | err.context = msg.data.d.context || {}; 415 | err.ase_errors = msg.data.d.ase_errors || []; 416 | delete this.fast_requests[msg.msgid]; 417 | this._pending_requests--; 418 | req.emit('error', err); 419 | cleanupListener(req); 420 | break; 421 | } 422 | } else { 423 | this.emit('error', new UnsolicitedMessageError(msg)); 424 | } 425 | }; 426 | 427 | 428 | Client.prototype._nextMessageId = function _nextMessageId() { 429 | if (++this.fast_msgid >= MAX_MSGID) 430 | this.fast_msgid = 1; 431 | 432 | return (this.fast_msgid); 433 | }; 434 | 435 | 436 | Client.prototype._onConnection = function _onConnection(connect_err, conn) { 437 | assert.notEqual(this.closed, true, 'connection state change after close'); 438 | 439 | if (connect_err) { 440 | this.emit('error', connect_err); 441 | return; 442 | } 443 | 444 | var self = this; 445 | 446 | conn.on('close', function (had_err) { 447 | cleanupListener(conn); 448 | 449 | self.fast_conn = null; 450 | // Clean up any pending requests with an error 451 | self.cancelRequests(new ConnectionClosedError()); 452 | 453 | // Queue up a reconnection, if requested 454 | if (self.fast_reconnect) 455 | self.fast_reconnect(); 456 | 457 | self.emit('close', had_err); 458 | }); 459 | conn.on('error', function (err) { 460 | conn.end(); 461 | if (self.listeners('error').length > 0) 462 | self.emit('error', err); 463 | }); 464 | 465 | this.fast_conn = conn; 466 | this.fast_conn.setKeepAlive(true, 60000); 467 | 468 | this.messageDecoder = new protocol.MessageDecoder(); 469 | this.messageEncoder = new protocol.MessageEncoder(); 470 | 471 | this.fast_conn.pipe(this.messageDecoder); 472 | this.messageEncoder.pipe(this.fast_conn); 473 | 474 | this.messageDecoder.on('message', function onMessage(msg) { 475 | self._handleMessage(msg); 476 | }); 477 | 478 | this._fast_retry = null; 479 | this.emit('connect'); 480 | }; 481 | 482 | 483 | 484 | ///--- Exports 485 | 486 | module.exports = { 487 | createClient: function createClient(options) { 488 | var opts = clone(options); 489 | opts.connectTimeout = opts.connectTimeout || 1000; 490 | opts.host = opts.host || '127.0.0.1'; 491 | if (opts.reconnect === undefined) 492 | opts.reconnect = 1000; 493 | 494 | opts.retry = opts.retry || { 495 | retries: 3 496 | }; 497 | return (new Client(opts)); 498 | } 499 | }; 500 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | module.exports = {}; 4 | 5 | function reexport(name) { 6 | var obj = require(name); 7 | Object.keys(obj).forEach(function (k) { 8 | module.exports[k] = obj[k]; 9 | }); 10 | } 11 | 12 | reexport('./client'); 13 | reexport('./protocol'); 14 | reexport('./server'); 15 | -------------------------------------------------------------------------------- /lib/protocol/dtrace.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | 4 | 5 | 6 | ///--- Globals 7 | 8 | var PROBES = { 9 | // method_name, message_id, JSON.stringify(arguments) 10 | 'rpc-start': ['char *', 'int', 'char *'], 11 | 12 | // method_name, message_id, status, JSON.stringify(arguments) 13 | 'rpc-msg': ['char *', 'int', 'int', 'char *'], 14 | 15 | // method_name, message_id 16 | 'rpc-done': ['char *', 'int'] 17 | }; 18 | var PROVIDER; 19 | 20 | 21 | 22 | ///--- API 23 | 24 | module.exports = function exportStaticProvider() { 25 | if (!PROVIDER) { 26 | try { 27 | var dtrace = require('dtrace-provider'); 28 | PROVIDER = dtrace.createDTraceProvider('fast'); 29 | 30 | Object.keys(PROBES).forEach(function (p) { 31 | var args = PROBES[p].splice(0); 32 | args.unshift(p); 33 | 34 | PROVIDER.addProbe.apply(PROVIDER, args); 35 | }); 36 | PROVIDER.enable(); 37 | } catch (e) { 38 | PROVIDER = { 39 | fire: function () {}, 40 | enable: function () {}, 41 | addProbe: function () { 42 | return ({fire: function () {}}); 43 | }, 44 | removeProbe: function () {}, 45 | disable: function () {} 46 | }; 47 | } 48 | } 49 | 50 | return (PROVIDER); 51 | }(); 52 | -------------------------------------------------------------------------------- /lib/protocol/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var MessageDecoder = require('./message_decoder').MessageDecoder; 4 | var MessageEncoder = require('./message_encoder').MessageEncoder; 5 | var RpcDecoder = require('./rpc_decoder').RpcDecoder; 6 | var RpcEncoder = require('./rpc_encoder').RpcEncoder; 7 | 8 | 9 | 10 | module.exports = { 11 | MessageDecoder: MessageDecoder, 12 | MessageEncoder: MessageEncoder, 13 | RpcDecoder: RpcDecoder, 14 | RpcEncoder: RpcEncoder 15 | }; 16 | 17 | var proto = require('./protocol'); 18 | Object.keys(proto).forEach(function (k) { 19 | module.exports[k] = proto[k]; 20 | }); 21 | -------------------------------------------------------------------------------- /lib/protocol/message_decoder.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var Writable = require('readable-stream').Writable; 4 | var util = require('util'); 5 | 6 | var assert = require('assert-plus'); 7 | var crc = require('crc'); 8 | var WError = require('verror').WError; 9 | 10 | var proto = require('./protocol'); 11 | 12 | 13 | 14 | ///--- Errors 15 | 16 | function ChecksumError(exp, actual, msg) { 17 | WError.call(this, {}, 'checksum error(%d): caclulated %d', exp, actual); 18 | 19 | this.context = { 20 | expected_crc: exp, 21 | actual_crc: actual, 22 | message: msg 23 | }; 24 | this.name = this.constructor.name; 25 | } 26 | util.inherits(ChecksumError, WError); 27 | 28 | 29 | function InvalidContentError(cause, msg) { 30 | WError.call(this, cause, 'invalid JSON encountered'); 31 | 32 | this.context = { 33 | message: msg 34 | }; 35 | this.name = this.constructor.name; 36 | } 37 | util.inherits(InvalidContentError, WError); 38 | 39 | 40 | 41 | ///--- Internal Functions 42 | 43 | function parseBuffer(buf, msg) { 44 | assert.object(buf, 'buffer'); 45 | assert.object(msg, 'message'); 46 | 47 | if (buf.length < proto.HEADER_LEN) 48 | return (false); 49 | 50 | msg._offset = msg._offset || 0; 51 | if (msg._offset === 0) { 52 | msg.version = buf.readUInt8(msg._offset++, true); 53 | msg.type = buf.readUInt8(msg._offset++, true); 54 | msg.status = buf.readUInt8(msg._offset++, true); 55 | msg.msgid = buf.readUInt32BE(msg._offset, true); 56 | msg._offset += 4; 57 | msg.checksum = buf.readInt32BE(msg._offset, true); 58 | msg._offset += 4; 59 | msg.length = buf.readUInt32BE(msg._offset, true); 60 | msg._offset += 4; 61 | } 62 | 63 | var remain = msg._offset + msg.length; 64 | if (buf.length < remain) 65 | return (false); 66 | 67 | msg.data = buf.slice(msg._offset, remain).toString('utf8'); 68 | msg._offset += msg.length; 69 | return (true); 70 | } 71 | 72 | 73 | 74 | ///--- API 75 | 76 | function MessageDecoder() { 77 | Writable.call(this); 78 | 79 | this._buf = null; 80 | this._msg = null; 81 | } 82 | util.inherits(MessageDecoder, Writable); 83 | 84 | 85 | MessageDecoder.prototype._write = function _write(buf, encoding, cb) { 86 | var checksum; 87 | var msg; 88 | var self = this; 89 | 90 | if (this._buf) { 91 | // Wed Underrun data on a previous call 92 | var len = this._buf.length + buf.length; 93 | buf = Buffer.concat([this._buf, buf], len); 94 | } 95 | 96 | assert.ok(Buffer.isBuffer(buf)); 97 | msg = this._msg || {}; 98 | 99 | while (buf.length > 0) { 100 | if (!parseBuffer(buf, msg)) { 101 | this._buf = buf; 102 | this._msg = msg; 103 | cb(); 104 | return; 105 | } 106 | 107 | checksum = crc.crc16(msg.data); 108 | if (msg.checksum !== checksum) { 109 | var e = new ChecksumError(msg.checksum, checksum, msg); 110 | self.emit('error', e); 111 | } 112 | 113 | try { 114 | msg.data = JSON.parse(msg.data); 115 | } catch (parse_err) { 116 | self.emit('error', new InvalidContentError(parse_err)); 117 | } 118 | 119 | msg.start = process.hrtime(); 120 | this.emit('message', msg); 121 | 122 | buf = buf.slice(msg._offset); 123 | msg = {}; 124 | } 125 | 126 | this._buf = null; 127 | this._msg = null; 128 | cb(); 129 | }; 130 | 131 | ///--- Exports 132 | 133 | module.exports = { 134 | 135 | MessageDecoder: MessageDecoder, 136 | 137 | createMessageDecoder: function createMessageDecoder() { 138 | return (new MessageDecoder()); 139 | } 140 | 141 | }; 142 | -------------------------------------------------------------------------------- /lib/protocol/message_encoder.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var Readable = require('readable-stream').Readable; 4 | var util = require('util'); 5 | 6 | var assert = require('assert-plus'); 7 | var crc = require('crc'); 8 | 9 | var proto = require('./protocol'); 10 | 11 | 12 | 13 | ///--- Globals 14 | 15 | var MSGID = 0; 16 | 17 | 18 | 19 | ///--- Internal Functions 20 | 21 | function nextMessageId() { 22 | if (++MSGID >= proto.MAX_MSGID) 23 | MSGID = 1; 24 | 25 | return (MSGID); 26 | } 27 | 28 | 29 | function serialize(object) { 30 | assert.object(object, 'object'); 31 | 32 | var buf; 33 | var data = JSON.stringify(object.data); 34 | var len = Buffer.byteLength(data); 35 | var msgid = object.msgid || nextMessageId(); 36 | var offset = 0; 37 | var status = object.status || proto.VERSION.DATA; 38 | var type = object.type || proto.TYPE_JSON; 39 | var version = object.version || proto.VERSION; 40 | 41 | buf = new Buffer(proto.HEADER_LEN + len); 42 | buf.writeUInt8(version, offset++, true); 43 | buf.writeUInt8(type, offset++, true); 44 | buf.writeUInt8(status, offset++, true); 45 | buf.writeUInt32BE(msgid, offset, true); 46 | offset += 4; 47 | buf.writeInt32BE(crc.crc16(data), offset, true); 48 | offset += 4; 49 | buf.writeUInt32BE(len, offset, true); 50 | offset += 4; 51 | buf.write(data, offset, len, 'utf8'); 52 | 53 | return (buf); 54 | } 55 | 56 | 57 | function emitAfter(object) { 58 | var req = object._arguments || []; 59 | var diff = process.hrtime(object.start); 60 | object.elapsed = Math.round((diff[0] * 1e6) + (diff[1] / 1000)); 61 | this.emit('after', object.data.m.name, req, object); 62 | } 63 | 64 | 65 | 66 | ///--- API 67 | 68 | function MessageEncoder() { 69 | Readable.call(this); 70 | this._outbound = []; 71 | } 72 | util.inherits(MessageEncoder, Readable); 73 | 74 | MessageEncoder.prototype.toString = function toString() { 75 | return ('[stream MessageEncoder]'); 76 | }; 77 | 78 | 79 | MessageEncoder.prototype.send = function send(object) { 80 | var buf = serialize(object); 81 | 82 | this._outbound.push(buf); 83 | 84 | this.read(0); 85 | 86 | if (object.start) { 87 | setImmediate(emitAfter.bind(this, object)); 88 | } 89 | }; 90 | 91 | MessageEncoder.prototype._read = function (n) { 92 | if (this._outbound.length === 0) { 93 | this.push(''); 94 | return; 95 | } 96 | 97 | var chunk; 98 | 99 | while (this._outbound.length > 0) { 100 | chunk = this._outbound.shift(); 101 | if (!this.push(chunk)) { 102 | break; 103 | } 104 | } 105 | }; 106 | 107 | 108 | ///--- Exports 109 | 110 | module.exports = { 111 | 112 | MessageEncoder: MessageEncoder, 113 | 114 | createMessageEncoder: function createMessageEncoder() { 115 | return (new MessageEncoder()); 116 | } 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /lib/protocol/protocol.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | // The protocol looks like this (rougly): 4 | // 5 | // version|type|status|msgid|crc16|data_len|data 6 | // 7 | // There are no actual '|' separators. The first 3 fields are 8 | // one byte UInts. `msgid` is 4 bytes encoded Big Endian uint32 9 | // CRC16 is 4 bytes, encoded big endian (so, take the 17 bit number 10 | // and just write it out to all 4 bytes). data_len is 4 bytes BE 11 | // uint32, and data is just encoded JSON. There you go. 12 | // 13 | // Version is currently 1, and type is currently 1 (which means JSON). 14 | // I may tack in GZIP'd JSON or some such later. 15 | // 16 | // Status byte is one of: 17 | // 1 -> data 18 | // 2 -> end 19 | // 3 -> error 20 | // 21 | module.exports = { 22 | HEADER_LEN: 15, 23 | MAX_MSGID: Math.pow(2, 31) -1, 24 | STATUS: { 25 | DATA: 0x01, 26 | END: 0x02, 27 | ERROR: 0x03 28 | }, 29 | TYPE_JSON: 0x01, 30 | VERSION: 0x01 31 | }; 32 | -------------------------------------------------------------------------------- /lib/protocol/rpc_decoder.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var util = require('util'); 5 | 6 | ///--- API 7 | 8 | function RpcDecoder() { 9 | EventEmitter.call(this); 10 | } 11 | util.inherits(RpcDecoder, EventEmitter); 12 | 13 | 14 | RpcDecoder.prototype.decode = function decode(msg) { 15 | if (!msg.data || !msg.data.m || !msg.data.d) { 16 | this.emit('error', new Error('invalid message')); 17 | return (undefined); 18 | } 19 | 20 | var name = msg.data.m.name; 21 | var args = msg.data.d.slice(); 22 | 23 | this.emit('rpc', name, args, msg); 24 | return (undefined); 25 | }; 26 | 27 | 28 | ///--- Exports 29 | 30 | module.exports = { 31 | RpcDecoder: RpcDecoder 32 | }; 33 | -------------------------------------------------------------------------------- /lib/protocol/rpc_encoder.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var util = require('util'); 5 | 6 | var assert = require('assert-plus'); 7 | var microtime = require('microtime'); 8 | 9 | var DTrace = require('./dtrace'); 10 | var proto = require('./protocol'); 11 | 12 | 13 | ///--- Globals 14 | 15 | var slice = Function.prototype.call.bind(Array.prototype.slice); 16 | 17 | 18 | 19 | ///--- API 20 | 21 | function RpcEncoder(options) { 22 | assert.object(options, 'options'); 23 | assert.object(options.encoder, 'options.encoder'); 24 | assert.string(options.method, 'options.method'); 25 | assert.number(options.msgid, 'options.msgid'); 26 | 27 | this.encoder = options.encoder; 28 | this.method = options.method; 29 | this.msgid = options.msgid; 30 | this.start = options.start; // optional 31 | this.status = proto.STATUS.DATA; 32 | 33 | this.canceled = false; 34 | 35 | // This is ghetto - we'll need to shift()/pop() these 36 | // later (in message_encoder) - really these exist for the 'after' 37 | // event on a server 38 | this._arguments = options._arguments; 39 | } 40 | util.inherits(RpcEncoder, EventEmitter); 41 | 42 | 43 | RpcEncoder.prototype.cancel = function cancel(err) { 44 | if (this.canceled) 45 | return; 46 | 47 | if (err) 48 | this.end(err); 49 | 50 | this.canceled = true; 51 | this.emit('cancel'); 52 | }; 53 | 54 | 55 | RpcEncoder.prototype.end = function end() { 56 | if (this.canceled || this.done) { 57 | // end() is a no-op after RPC is canceled 58 | return (null); 59 | } 60 | var self = this; 61 | 62 | this.status = proto.STATUS.END; 63 | 64 | DTrace.fire('rpc-done', function (p) { 65 | return ([self.method, self.msgid]); 66 | }); 67 | 68 | return (this.encode.apply(this, arguments)); 69 | }; 70 | 71 | 72 | RpcEncoder.prototype.encode = function encode() { 73 | if (this.canceled || this.done) { 74 | // write()/encode() are a no-op after RPC is canceled 75 | return (null); 76 | } 77 | var data; 78 | var now = microtime.now(); 79 | var self = this; 80 | 81 | function encodeError(err) { 82 | self.status = proto.STATUS.ERROR; 83 | return { 84 | name: err.name || 'Error', 85 | message: err.message || 'error', 86 | stack: err.stack, 87 | context: err.context || {} 88 | }; 89 | } 90 | 91 | if (arguments[0] && arguments[0] instanceof Error) { 92 | data = encodeError(arguments[0]); 93 | 94 | // Parse verror.MultiError -- the multiple errors are on the 95 | // MultiError.ase_error field 96 | var ase_errors = arguments[0].ase_errors; 97 | if (ase_errors) { 98 | data.ase_errors = []; 99 | ase_errors.forEach(function (err) { 100 | data.ase_errors.push(encodeError(err)); 101 | }); 102 | } 103 | } else { 104 | data = slice(arguments); 105 | } 106 | 107 | var msg = { 108 | msgid: self.msgid, 109 | data: { 110 | m: { 111 | name: self.method, 112 | uts: now 113 | }, 114 | d: data 115 | }, 116 | start: self.start, 117 | status: self.status 118 | }; 119 | 120 | msg._arguments = this._arguments; 121 | this.encoder.send(msg); 122 | 123 | DTrace.fire('rpc-msg', function (p) { 124 | return ([self.method, 125 | self.msgid, 126 | self.status, 127 | JSON.stringify(data)]); 128 | }); 129 | if (this.status == proto.STATUS.END || this.status == proto.STATUS.ERROR) { 130 | // Prevent further messages related to this RPC from being sent. 131 | // They would be considered unsolicited by the client. 132 | this.done = true; 133 | } 134 | 135 | return (msg); 136 | }; 137 | RpcEncoder.prototype.write = RpcEncoder.prototype.encode; 138 | 139 | 140 | 141 | ///--- Exports 142 | 143 | module.exports = { 144 | RpcEncoder: RpcEncoder 145 | }; 146 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mark Cavage. All rights reserved. 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var net = require('net'); 5 | var util = require('util'); 6 | 7 | var assert = require('assert-plus'); 8 | 9 | var DTrace = require('./protocol/dtrace'); 10 | var protocol = require('./protocol'); 11 | var STATUS = protocol.STATUS; 12 | 13 | 14 | 15 | ///--- Globals 16 | 17 | var slice = Function.prototype.call.bind(Array.prototype.slice); 18 | 19 | 20 | 21 | ///--- Helpers 22 | 23 | function cleanup(conn, decoder, encoder, isEnd) { 24 | if (conn.rpcDecoder) 25 | delete conn.rpcDecoder; 26 | 27 | decoder.removeAllListeners('data'); 28 | decoder.removeAllListeners('drain'); 29 | encoder.removeAllListeners('after'); 30 | encoder.removeAllListeners('data'); 31 | encoder.removeAllListeners('drain'); 32 | 33 | conn.removeAllListeners('close'); 34 | conn.removeAllListeners('data'); 35 | conn.removeAllListeners('drain'); 36 | conn.removeAllListeners('end'); 37 | if (isEnd) { 38 | // Until node#7015 is fixed, don't remove the error listener until 39 | // the end event - otherwise, we could get a second unhandled error 40 | // event (and throw instead of logging it) 41 | conn.removeAllListeners('error'); 42 | } 43 | conn.removeAllListeners('timeout'); 44 | conn.destroy(); 45 | } 46 | 47 | 48 | ///--- API 49 | 50 | function Server(options) { 51 | EventEmitter.call(this); 52 | 53 | var self = this; 54 | options = options || {}; 55 | 56 | this._rpc = null; 57 | this.srv = net.createServer(this.onConnection.bind(this)); 58 | 59 | //-- Properties 60 | ['connections', 'maxConnections'].forEach(function (p) { 61 | self.__defineSetter__(p, function (v) { 62 | self.srv[p] = v; 63 | }); 64 | self.__defineGetter__(p, function () { 65 | return (self.srv[p]); 66 | }); 67 | }); 68 | 69 | //-- Events 70 | ['close', 'connection', 'error', 'listening'].forEach(function (e) { 71 | self.srv.on(e, function () { 72 | var args = slice(arguments); 73 | args.unshift(e); 74 | self.emit.apply(self, args); 75 | }); 76 | }); 77 | } 78 | util.inherits(Server, EventEmitter); 79 | 80 | Server.prototype.onConnection = function onConnection(conn) { 81 | var self = this; 82 | var messageDecoder = new protocol.MessageDecoder(); 83 | var messageEncoder = new protocol.MessageEncoder(); 84 | var rpcDecoder = new protocol.RpcDecoder({ 85 | decoder: messageDecoder, 86 | encoder: messageEncoder 87 | }); 88 | 89 | conn.msgs = {}; 90 | conn.rpcDecoder = rpcDecoder; 91 | 92 | messageDecoder.on('message', function (msg) { 93 | if (msg.status === STATUS.ERROR) { 94 | // cancel pending RPCs on client request 95 | if (conn.msgs[msg.msgid]) { 96 | var err = new Error('RPC request canceled'); 97 | err.name = 'RPCCanceledError'; 98 | conn.msgs[msg.msgid].cancel(err); 99 | delete conn.msgs[msg.msgid]; 100 | } 101 | } else { 102 | rpcDecoder.decode(msg); 103 | } 104 | }); 105 | 106 | rpcDecoder.on('rpc', function (name, args, msg) { 107 | var encoder = new protocol.RpcEncoder({ 108 | encoder: messageEncoder, 109 | method: name, 110 | msgid: msg.msgid, 111 | start: msg.start, 112 | _arguments: args 113 | }); 114 | 115 | if (self.listeners(name).length === 0) { 116 | // complain about missing RPC handler 117 | var err = new Error('no handler for ' + name); 118 | err.name = 'RPCNotDefinedError'; 119 | encoder.end(err); 120 | } else { 121 | DTrace.fire('rpc-start', function (p) { 122 | return ([msg.data.m.name, 123 | msg.msgid, 124 | JSON.stringify(msg.data.d)]); 125 | }); 126 | // track rpc "session" 127 | conn.msgs[msg.msgid] = encoder; 128 | self.emit.apply(self, [].concat(name, args, encoder)); 129 | } 130 | }); 131 | 132 | messageEncoder.on('after', function (method, req, msg) { 133 | if (msg.status === STATUS.END) { 134 | self.emit('after', method, req, msg); 135 | } 136 | // cease rpc tracking after sending end/error response 137 | if (msg.status === STATUS.END || msg.status === STATUS.ERROR) { 138 | if (conn.msgs[msg.msgid]) { 139 | delete conn.msgs[msg.msgid]; 140 | } 141 | } 142 | }); 143 | 144 | conn.pipe(messageDecoder); 145 | messageEncoder.pipe(conn); 146 | 147 | function cancelAll() { 148 | // Inform all active RPCs that connection has been lost 149 | Object.keys(conn.msgs).forEach(function (msgid) { 150 | conn.msgs[msgid].cancel(); 151 | }); 152 | conn.msgs = {}; 153 | } 154 | 155 | conn.once('error', function onError(err) { 156 | self.emit('clientError', err); 157 | cancelAll(); 158 | cleanup(conn, messageDecoder, messageEncoder, false); 159 | // ignore further errors 160 | conn.on('error', function () {}); 161 | }); 162 | conn.once('end', function onEnd() { 163 | messageDecoder.emit('end'); 164 | cancelAll(); 165 | cleanup(conn, messageDecoder, messageEncoder, true); 166 | }); 167 | }; 168 | 169 | 170 | 171 | //-- Direct wrappers 172 | ['close', 'listen', 'address'].forEach(function (m) { 173 | Server.prototype[m] = function () { 174 | this.srv[m].apply(this.srv, arguments); 175 | }; 176 | }); 177 | 178 | 179 | Server.prototype.rpc = function rpc(name, cb) { 180 | assert.string(name, 'name'); 181 | assert.func(cb, 'callback'); 182 | this.on(name, cb.bind(this)); 183 | return (this); 184 | }; 185 | 186 | 187 | 188 | ///--- Exports 189 | 190 | module.exports = { 191 | createServer: function createServer(options) { 192 | return (new Server(options)); 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Mark Cavage ", 3 | "contributors": [ 4 | "Mike Harsch", 5 | "Nate Fitch", 6 | "Yunong Xiao", 7 | "Patrick Mooney", 8 | "Julien Gilli" 9 | ], 10 | "name": "fast", 11 | "homepage": "https://github.com/mcavage/node-fast", 12 | "version": "0.5.3", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/mcavage/node-fast.git" 17 | }, 18 | "main": "lib/index.js", 19 | "engines": { 20 | "node": ">=0.10" 21 | }, 22 | "dependencies": { 23 | "assert-plus": "^0.1.5", 24 | "backoff": "~2.5.0", 25 | "crc": "^0.3.0", 26 | "microtime": "^2.0.0", 27 | "once": "^1.3.2", 28 | "readable-stream": "^2.0.3", 29 | "verror": "^1.6.0" 30 | }, 31 | "optionalDependencies": { 32 | "dtrace-provider": "^0.6.0" 33 | }, 34 | "devDependencies": { 35 | "tape": "^4.2.2", 36 | "faucet": "^0.0.1", 37 | "istanbul": "^0.4.0" 38 | }, 39 | "scripts": { 40 | "test": "./node_modules/.bin/istanbul cover --print none test/test.js | ./node_modules/.bin/faucet" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Joyent, Inc. All rights reserved. 2 | 3 | var fast = require('../lib'); 4 | var test = require('tape').test; 5 | 6 | 7 | ///--- Globals 8 | 9 | var HOST = process.env.TEST_HOST || '127.0.0.1'; 10 | var PORT = process.env.TEST_PORT || 12345; 11 | // a bogus loopback address should work for testing connect timeout 12 | var TIMEOUT_HOST = process.env.TEST_TIMEOUT_HOST || '127.1.1.1'; 13 | var TIMEOUT_MS = 1000; 14 | 15 | var client; 16 | var server; 17 | 18 | 19 | 20 | ///--- Tests 21 | 22 | test('connect timeout', function (t) { 23 | function done() { 24 | client.removeAllListeners(); 25 | client.close(); 26 | t.end(); 27 | } 28 | 29 | client = fast.createClient({ 30 | host: TIMEOUT_HOST, 31 | port: PORT, 32 | connectTimeout: TIMEOUT_MS 33 | }); 34 | var failTimer = setTimeout(function () { 35 | t.ok(false, 'timeout failed'); 36 | done(); 37 | }, TIMEOUT_MS * 2); 38 | client.once('connectError', function (err) { 39 | t.equal(err.name, 'ConnectionTimeoutError', 'timeout error'); 40 | clearTimeout(failTimer); 41 | done(); 42 | }); 43 | }); 44 | 45 | 46 | test('close suppress connectErrors', function (t) { 47 | client = fast.createClient({ 48 | host: TIMEOUT_HOST, 49 | port: PORT, 50 | connectTimeout: TIMEOUT_MS 51 | }); 52 | client.on('connectError', function (err) { 53 | t.fail('error not suppressed'); 54 | }); 55 | setImmediate(function () { 56 | client.close(); 57 | setTimeout(function () { 58 | t.ok(true); 59 | t.end(); 60 | }, TIMEOUT_MS); 61 | }); 62 | }); 63 | 64 | 65 | test('connect retry limit', function (t) { 66 | var targetCount = 3; 67 | var realCount = 0; 68 | 69 | client = fast.createClient({ 70 | host: HOST, 71 | port: PORT, 72 | retry: { 73 | retries: targetCount 74 | } 75 | }); 76 | client.on('connectError', function (err) { 77 | realCount++; 78 | }); 79 | client.once('error', function (err) { 80 | // The first failure is not a retry 81 | t.equal(realCount, targetCount+1, 'retry count'); 82 | client.close(); 83 | t.end(); 84 | }); 85 | }); 86 | 87 | 88 | test('countPending', function (t) { 89 | server = fast.createServer(); 90 | server.rpc('sleep', function (timeout, res) { 91 | setTimeout(function () { 92 | res.end(null); 93 | }, parseInt(timeout, 10)); 94 | }); 95 | server.listen(PORT, function () { 96 | client = fast.createClient({ 97 | host: 'localhost', 98 | port: PORT 99 | }); 100 | client.once('connect', function () { 101 | client.rpc('sleep', 900); 102 | client.rpc('sleep', 1900); 103 | client.rpc('sleep', 2900); 104 | 105 | var expected = 3; 106 | function check() { 107 | t.equal(expected, client.countPending); 108 | if (expected === 0) { 109 | client.close(); 110 | server.close(); 111 | t.end(); 112 | } else { 113 | expected--; 114 | setTimeout(check, 1000); 115 | } 116 | } 117 | check(); 118 | }); 119 | }); 120 | //test 121 | }); 122 | 123 | 124 | test('RPC error on close', function (t) { 125 | server = fast.createServer(); 126 | server.rpc('slow', function (res) { 127 | // Don't respond to simulate indefinite hang 128 | }); 129 | server.listen(PORT, function () { 130 | client = fast.createClient({ 131 | host: HOST, 132 | port: PORT 133 | }); 134 | client.once('connect', function () { 135 | t.pass('connected'); 136 | var res = client.rpc('slow'); 137 | res.on('error', function (err) { 138 | t.equal(err.name, 'ConnectionClosedError'); 139 | server.close(); 140 | t.end(); 141 | }); 142 | setImmediate(function () { 143 | t.pass('closing'); 144 | client.close(); 145 | }); 146 | }); 147 | }); 148 | }); 149 | 150 | 151 | test('RPC error when not connected', function (t) { 152 | server = fast.createServer(); 153 | server.rpc('pass', function (res) { 154 | res.end(null); 155 | }); 156 | server.listen(PORT, function () { 157 | client = fast.createClient({ 158 | host: HOST, 159 | port: PORT, 160 | reconnect: false 161 | }); 162 | client.once('connect', function () { 163 | // Simulate server close 164 | client.fast_conn.destroy(); 165 | }); 166 | client.once('close', function () { 167 | var res = client.rpc('pass'); 168 | res.once('error', function (err) { 169 | t.ok(err); 170 | t.equal(err.name, 'NoConnectionError'); 171 | server.close(); 172 | client.close(); 173 | t.end(); 174 | 175 | }); 176 | res.on('end', t.fail.bind(t, 'end called')); 177 | }); 178 | }); 179 | }); 180 | 181 | // Regression test for https://smartos.org/bugview/MORAY-324. 182 | test('socket properly hangs up via close', function (t) { 183 | var client1EmittedClose = false; 184 | 185 | server = fast.createServer(); 186 | server.listen(PORT, function () { 187 | // Create a first client that we'll close right away. 188 | // The goal is to reproduce the issue described by MORAY-324 where 189 | // a client that would be immediately closed before establishing 190 | // a connection would _not_ be closed, and would still connect 191 | // to the server. 192 | client = fast.createClient({ 193 | host: HOST, 194 | port: PORT 195 | }); 196 | 197 | client.close(); 198 | 199 | client.on('connect', function onClosedClientConnect() { 200 | t.ok(false, 'closed client should not emit connect event'); 201 | }); 202 | 203 | client.on('close', function onClient1Close() { 204 | client1EmittedClose = true; 205 | }); 206 | 207 | // Create a second client, only for the purpose of making 208 | // sure that the first one has the time to connect if the bug 209 | // described in MORAY-324 is still present. 210 | var client2 = fast.createClient({ 211 | host: HOST, 212 | port: PORT 213 | }); 214 | 215 | // Close the second client as soon as it connects so that it 216 | // doesn't hold the libuv event loop open and allows the server 217 | // to close (and thus the test to end) if the first client 218 | // manages to close its connection. 219 | client2.on('connect', function onClient2Connected() { 220 | client2.close(); 221 | }); 222 | 223 | client2.on('close', function onClient2Closed() { 224 | t.equal(client1EmittedClose, true, 'first client should ' + 225 | 'have emitted close'); 226 | 227 | // Use a timeout to check for the number of current connections 228 | // on the server, as when client2 closed its connection, the 229 | // other end of the connection may not have closed yet. 230 | // Delay of 1000ms is arbitrary, but should be enough to let 231 | // the server side of the connection to close, and the 232 | // net.Server's .connections property to be updated. 233 | setTimeout(function waitForClientsClose() { 234 | server.srv.getConnections(function (err, nbConnections) { 235 | t.ifError(err, 'getConnections should not result in ' + 236 | 'an error'); 237 | t.equal(nbConnections, 0, 238 | 'after second client closed, server should have ' + 239 | 'no remaining client connected'); 240 | }); 241 | 242 | // When the second client closes, if the bug described by 243 | // MORAY-324 is still present, the first client will have 244 | // established a connection, and the server won't be able to 245 | // close since the first client will never close. 246 | // If MORAY-324 is fixed, the first client will have closed 247 | // its connection before it's established, and thus as soon 248 | // as the second client closes its connection, the server 249 | // can close and the test can end. 250 | server.close(); 251 | 252 | server.on('close', function onServerClosed() { 253 | t.end(); 254 | }); 255 | 256 | }, 1000); 257 | }); 258 | }); 259 | }); 260 | 261 | test('client timeout should be cleared on close', function (t) { 262 | server = fast.createServer(); 263 | server.listen(PORT, function () { 264 | server.srv.unref(); 265 | 266 | client = fast.createClient({ 267 | host: HOST, 268 | port: PORT 269 | }); 270 | 271 | client.close(); 272 | 273 | client.on('close', function onClient1Close() { 274 | t.equal(process._getActiveHandles().length, 0, 275 | 'there should be no active handle left after client ' + 276 | 'closed its connection'); 277 | server.close(); 278 | server.on('close', function onServerClose() { 279 | t.end(); 280 | }); 281 | }); 282 | }); 283 | }); 284 | 285 | // This is essentially a regression test for 286 | // https://smartos.org/bugview/CNAPI-648. 287 | test('close does not assert', function (t) { 288 | // Arguments passed to createClient below are set just to that the client 289 | // creation does not assert. The port and connectTimeout arguments do not 290 | // have a specific purpose, since the test closes the client before it had a 291 | // chance to establish any connection anyway. 292 | client = fast.createClient({ 293 | port: PORT, 294 | connectTimeout: TIMEOUT_MS 295 | }); 296 | 297 | client.close(); 298 | t.end(); 299 | }); 300 | -------------------------------------------------------------------------------- /test/message.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Mark Cavage. All rights reserved. 2 | 3 | var fast = require('../lib'); 4 | var test = require('tape').test; 5 | 6 | 7 | 8 | ///--- Tests 9 | 10 | test('serialize ok', function (t) { 11 | var encoder = new fast.MessageEncoder(); 12 | 13 | encoder.on('readable', function onReadable() { 14 | var buf = encoder.read(); 15 | t.ok(buf); 16 | t.equal(buf[0], 0x01); // v 17 | t.equal(buf[1], 0x01); // t 18 | t.equal(buf[2], 0x01); //s 19 | t.equal(buf.readUInt32BE(3), 123); // id 20 | t.ok(buf.readInt32BE(7)); // crc 21 | t.equal(buf.readUInt32BE(11), 17); // len 22 | t.deepEqual(JSON.parse(buf.slice(15, 32).toString()), { 23 | hello: 'world' 24 | }); 25 | t.end(); 26 | }); 27 | encoder.send({ 28 | msgid: 123, 29 | data: { 30 | hello: 'world' 31 | }, 32 | status: 0x01, 33 | type: 0x01, 34 | version: 0x01 35 | }); 36 | }); 37 | 38 | 39 | test('deserialize ok', function (t) { 40 | var encoder = new fast.MessageEncoder(); 41 | var decoder = new fast.MessageDecoder(); 42 | 43 | 44 | var msg1 = { 45 | msgid: 123, 46 | data: { 47 | hello: 'world' 48 | }, 49 | status: 0x01, 50 | type: 0x01, 51 | version: 0x01 52 | }; 53 | 54 | decoder.on('message', function (msg2) { 55 | t.ok(msg2); 56 | t.equal(msg2.msgid, msg1.msgid); 57 | t.equal(msg2.status, msg1.status); 58 | t.equal(msg2.type, msg1.type); 59 | t.equal(msg2.version, msg1.version); 60 | t.deepEqual(msg2.data, msg1.data); 61 | t.end(); 62 | }); 63 | 64 | encoder.pipe(decoder); 65 | encoder.send(msg1); 66 | }); 67 | -------------------------------------------------------------------------------- /test/rpc.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Mark Cavage. All rights reserved. 2 | 3 | var fast = require('../lib'); 4 | var test = require('tape').test; 5 | 6 | 7 | 8 | ////--- Globals 9 | 10 | var PORT = process.env.TEST_PORT || 12345; 11 | 12 | var client; 13 | var server; 14 | 15 | 16 | 17 | ///--- Tests 18 | 19 | test('createServer', function (t) { 20 | server = fast.createServer(); 21 | t.ok(server); 22 | t.end(); 23 | }); 24 | 25 | 26 | test('listen', function (t) { 27 | server.listen(PORT, function () { 28 | t.end(); 29 | }); 30 | }); 31 | 32 | 33 | test('createClient', function (t) { 34 | client = fast.createClient({ 35 | host: 'localhost', 36 | port: PORT 37 | }); 38 | client.on('connect', function () { 39 | t.end(); 40 | }); 41 | }); 42 | 43 | 44 | test('echo RPC handler', function (t) { 45 | server.rpc('echo', function (message, res) { 46 | res.end(message); 47 | }); 48 | var req = client.rpc('echo', 'hello world'); 49 | t.ok(req); 50 | req.on('message', function (msg) { 51 | t.equal(msg, 'hello world'); 52 | }); 53 | req.on('end', function () { 54 | t.end(); 55 | }); 56 | }); 57 | 58 | 59 | test('error RPC handler', function (t) { 60 | server.rpc('err', function (res) { 61 | var e = new Error('suck it, mr. client'); 62 | e.context = { 63 | foo: 'bar' 64 | }; 65 | res.write(e); 66 | }); 67 | var req = client.rpc('err'); 68 | t.ok(req); 69 | req.on('error', function (err) { 70 | t.ok(err); 71 | t.equal(err.message, 'suck it, mr. client'); 72 | t.ok(err.context); 73 | if (err.context) 74 | t.equal(err.context.foo, 'bar'); 75 | t.end(); 76 | }); 77 | }); 78 | 79 | 80 | test('cancelled RPC', function (t) { 81 | server.rpc('cancelMe', function (message, res) { 82 | var timer = setTimeout(function () { 83 | t.fail('not canceled'); 84 | res.end({woe: 'is me'}); 85 | }, 500); 86 | res.on('cancel', function () { 87 | t.pass('canceled'); 88 | clearTimeout(timer); 89 | }); 90 | }); 91 | var req = client.rpc('cancelMe', 'test'); 92 | t.ok(req); 93 | req.once('error', function (err) { 94 | t.ok(err); 95 | t.equal(err.name, 'RPCCanceled'); 96 | }); 97 | setTimeout(req.cancel.bind(req), 200); 98 | setTimeout(t.end.bind(t), 1000); 99 | }); 100 | 101 | 102 | test('cancel on disconnect', function (t) { 103 | t.plan(4); 104 | var port = PORT+1; 105 | var cServer = fast.createServer(); 106 | var cClient; 107 | cServer.rpc('toCancel', function (arg, res) { 108 | res.on('cancel', function () { 109 | t.pass('rpc cancel'); 110 | cClient.close(); 111 | cServer.close(); 112 | }); 113 | }); 114 | t.ok(cServer); 115 | cServer.listen(port, function () { 116 | cClient = fast.createClient({ 117 | host: 'localhost', 118 | port: port 119 | }); 120 | t.ok(cClient); 121 | cClient.once('connect', function () { 122 | t.pass('connected'); 123 | var req = cClient.rpc('toCancel', 'test'); 124 | setTimeout(function () { 125 | // simulate disconnect 126 | cClient.fast_conn.destroy(); 127 | }, 100); 128 | req.once('error', function () {}); 129 | }); 130 | }); 131 | }); 132 | 133 | 134 | test('streaming RPC handler', function (t) { 135 | server.rpc('stream', function (res) { 136 | for (var i = 1; i <= 10; i++) 137 | res.write({i: i}); 138 | res.end(); 139 | }); 140 | var req = client.rpc('stream'); 141 | var seen = 0; 142 | t.ok(req); 143 | req.on('message', function (obj) { 144 | t.ok(obj); 145 | t.ok(obj.i); 146 | seen++; 147 | }); 148 | req.on('end', function () { 149 | t.equal(seen, 10); 150 | t.end(); 151 | }); 152 | }); 153 | 154 | 155 | test('RPC suppress messages after error', function (t) { 156 | t.plan(2); 157 | server.rpc('sup_err', function (res) { 158 | t.pass('RPC called'); 159 | res.write(new Error('bummer')); 160 | res.write({foo: 'bar'}); 161 | res.end(); 162 | }); 163 | var req = client.rpc('sup_err'); 164 | req.on('error', t.ok.bind(t)); 165 | req.on('end', t.fail.bind(t, 'end not suppressed')); 166 | req.on('message', t.fail.bind(t, 'msg not suppressed')); 167 | }); 168 | 169 | 170 | test('RPC suppress messages after end', function (t) { 171 | t.plan(2); 172 | server.rpc('sup_end', function (res) { 173 | t.pass('RPC called'); 174 | res.end(); 175 | res.write(new Error('bummer')); 176 | res.write({foo: 'bar'}); 177 | }); 178 | var req = client.rpc('sup_end'); 179 | req.on('end', t.pass.bind(t)); 180 | req.on('error', t.fail.bind(t, 'error not suppressed')); 181 | req.on('message', t.fail.bind(t, 'msg not suppressed')); 182 | }); 183 | 184 | 185 | test('undefined RPC - checkDefined', function (t) { 186 | var port = PORT+1; 187 | var cServer = fast.createServer({ 188 | checkDefined: true 189 | }); 190 | t.ok(cServer); 191 | cServer.listen(port, function () { 192 | var cClient = fast.createClient({ 193 | host: 'localhost', 194 | port: port 195 | }); 196 | t.ok(cClient); 197 | cClient.on('connect', function () { 198 | t.pass('connected'); 199 | var req = cClient.rpc('notdefined', 'test'); 200 | req.once('error', function (err) { 201 | t.ok(err); 202 | t.equal(err.name, 'RPCNotDefinedError'); 203 | cClient.close(); 204 | cServer.close(); 205 | t.end(); 206 | }); 207 | }); 208 | }); 209 | }); 210 | 211 | 212 | test('teardown', function (t) { 213 | client.on('close', function () { 214 | t.pass('client closed'); 215 | server.on('close', function () { 216 | t.pass('server closed'); 217 | t.end(); 218 | }); 219 | server.close(); 220 | }); 221 | client.close(); 222 | }); 223 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Joyent, Inc. All rights reserved. 2 | 3 | 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | function runTests(directory) { 8 | fs.readdir(directory, function (err, files) { 9 | files.filter(function (f) { 10 | return (/\.test\.js$/.test(f)); 11 | }).map(function (f) { 12 | return (path.join(directory, f)); 13 | }).forEach(require); 14 | }); 15 | } 16 | 17 | ///--- Run All Tests 18 | 19 | (function main() { 20 | runTests(__dirname); 21 | })(); 22 | -------------------------------------------------------------------------------- /tools/jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define module 119 | +define process 120 | +define require 121 | +define setInterval 122 | +define setTimeout 123 | +define setImmediate 124 | +define Buffer 125 | +define JSON 126 | +define Math 127 | 128 | ### JavaScript Version 129 | # To change the default JavaScript version: 130 | #+default-type text/javascript;version=1.5 131 | #+default-type text/javascript;e4x=1 132 | 133 | ### Files 134 | # Specify which files to lint 135 | # Use "+recurse" to enable recursion (disabled by default). 136 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 137 | # or "+process Folder\Path\*.htm". 138 | # 139 | 140 | -------------------------------------------------------------------------------- /tools/jsstyle.conf: -------------------------------------------------------------------------------- 1 | indent=4 2 | doxygen 3 | unparenthesized-return=1 4 | blank-after-start-comment=0 5 | -------------------------------------------------------------------------------- /tools/mk/Makefile.defs: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.defs: common defines. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This makefile defines some useful defines. Include it at the top of 13 | # your Makefile. 14 | # 15 | # Definitions in this Makefile: 16 | # 17 | # TOP The absolute path to the project directory. The top dir. 18 | # BRANCH The current git branch. 19 | # TIMESTAMP The timestamp for the build. This can be set via 20 | # the TIMESTAMP envvar (used by MG-based builds). 21 | # STAMP A build stamp to use in built package names. 22 | # 23 | 24 | TOP := $(shell pwd) 25 | 26 | # 27 | # Mountain Gorilla-spec'd versioning. 28 | # See "Package Versioning" in MG's README.md: 29 | # 30 | # 31 | # Need GNU awk for multi-char arg to "-F". 32 | _AWK := $(shell (which gawk >/dev/null && echo gawk) \ 33 | || (which nawk >/dev/null && echo nawk) \ 34 | || echo awk) 35 | BRANCH := $(shell git symbolic-ref HEAD | $(_AWK) -F/ '{print $$3}') 36 | ifeq ($(TIMESTAMP),) 37 | TIMESTAMP := $(shell date -u "+%Y%m%dT%H%M%SZ") 38 | endif 39 | _GITDESCRIBE := g$(shell git describe --all --long --dirty | $(_AWK) -F'-g' '{print $$NF}') 40 | STAMP := $(BRANCH)-$(TIMESTAMP)-$(_GITDESCRIBE) 41 | -------------------------------------------------------------------------------- /tools/mk/Makefile.deps: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.deps: Makefile for including common tools as dependencies 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This file is separate from Makefile.targ so that teams can choose 13 | # independently whether to use the common targets in Makefile.targ and the 14 | # common tools here. 15 | # 16 | 17 | # 18 | # javascriptlint 19 | # 20 | JSL_EXEC ?= deps/javascriptlint/build/install/jsl 21 | JSL ?= python $(JSL_EXEC) 22 | 23 | $(JSL_EXEC): | deps/javascriptlint/.git 24 | cd deps/javascriptlint && make install 25 | 26 | # 27 | # jsstyle 28 | # 29 | JSSTYLE_EXEC ?= deps/jsstyle/jsstyle 30 | JSSTYLE ?= $(JSSTYLE_EXEC) 31 | 32 | $(JSSTYLE_EXEC): | deps/jsstyle/.git 33 | 34 | # 35 | # restdown 36 | # 37 | RESTDOWN_EXEC ?= deps/restdown/bin/restdown 38 | RESTDOWN ?= python2.6 $(RESTDOWN_EXEC) 39 | $(RESTDOWN_EXEC): | deps/restdown/.git 40 | -------------------------------------------------------------------------------- /tools/mk/Makefile.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.targ: common targets. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This Makefile defines several useful targets and rules. You can use it by 13 | # including it from a Makefile that specifies some of the variables below. 14 | # 15 | # Targets defined in this Makefile: 16 | # 17 | # check Checks JavaScript files for lint and style 18 | # Checks bash scripts for syntax 19 | # Checks SMF manifests for validity against the SMF DTD 20 | # 21 | # clean Removes built files 22 | # 23 | # docs Builds restdown documentation in docs/ 24 | # 25 | # prepush Depends on "check" and "test" 26 | # 27 | # test Does nothing (you should override this) 28 | # 29 | # xref Generates cscope (source cross-reference index) 30 | # 31 | # For details on what these targets are supposed to do, see the Joyent 32 | # Engineering Guide. 33 | # 34 | # To make use of these targets, you'll need to set some of these variables. Any 35 | # variables left unset will simply not be used. 36 | # 37 | # BASH_FILES Bash scripts to check for syntax 38 | # (paths relative to top-level Makefile) 39 | # 40 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 41 | # that files generated by targets in this Makefile are 42 | # automatically included in CLEAN_FILES. These include 43 | # restdown-generated HTML and JSON files. 44 | # 45 | # DOC_FILES Restdown (documentation source) files. These are 46 | # assumed to be contained in "docs/", and must NOT 47 | # contain the "docs/" prefix. 48 | # 49 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 50 | # JSL_CONF_WEB (paths relative to top-level Makefile) 51 | # 52 | # Node.js and Web configuration files are separate 53 | # because you'll usually want different global variable 54 | # configurations. If no file is specified, none is given 55 | # to jsl, which causes it to use a default configuration, 56 | # which probably isn't what you want. 57 | # 58 | # JSL_FILES_NODE JavaScript files to check with Node config file. 59 | # JSL_FILES_WEB JavaScript files to check with Web config file. 60 | # 61 | # You can also override these variables: 62 | # 63 | # BASH Path to bash (default: bash) 64 | # 65 | # CSCOPE_DIRS Directories to search for source files for the cscope 66 | # index. (default: ".") 67 | # 68 | # JSL Path to JavaScriptLint (default: "jsl") 69 | # 70 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 71 | # JSL_FLAGS_WEB 72 | # JSL_FLAGS 73 | # 74 | # JSSTYLE Path to jsstyle (default: jsstyle) 75 | # 76 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 77 | # 78 | 79 | # 80 | # Defaults for the various tools we use. 81 | # 82 | BASH ?= bash 83 | BASHSTYLE ?= tools/bashstyle 84 | CP ?= cp 85 | CSCOPE ?= cscope 86 | CSCOPE_DIRS ?= . 87 | JSL ?= jsl 88 | JSSTYLE ?= jsstyle 89 | MKDIR ?= mkdir -p 90 | MV ?= mv 91 | RESTDOWN_FLAGS ?= 92 | RMTREE ?= rm -rf 93 | JSL_FLAGS ?= --nologo --nosummary 94 | 95 | ifeq ($(shell uname -s),SunOS) 96 | TAR ?= gtar 97 | else 98 | TAR ?= tar 99 | endif 100 | 101 | 102 | # 103 | # Defaults for other fixed values. 104 | # 105 | BUILD = build 106 | DISTCLEAN_FILES += $(BUILD) 107 | DOC_BUILD = $(BUILD)/docs/public 108 | 109 | # 110 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 111 | # 112 | ifneq ($(origin JSL_CONF_NODE), undefined) 113 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 114 | endif 115 | 116 | ifneq ($(origin JSL_CONF_WEB), undefined) 117 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 118 | endif 119 | 120 | # 121 | # Targets. For descriptions on what these are supposed to do, see the 122 | # Joyent Engineering Guide. 123 | # 124 | 125 | # 126 | # Instruct make to keep around temporary files. We have rules below that 127 | # automatically update git submodules as needed, but they employ a deps/*/.git 128 | # temporary file. Without this directive, make tries to remove these .git 129 | # directories after the build has completed. 130 | # 131 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 132 | 133 | # 134 | # This rule enables other rules that use files from a git submodule to have 135 | # those files depend on deps/module/.git and have "make" automatically check 136 | # out the submodule as needed. 137 | # 138 | deps/%/.git: 139 | git submodule update --init deps/$* 140 | 141 | # 142 | # These recipes make heavy use of dynamically-created phony targets. The parent 143 | # Makefile defines a list of input files like BASH_FILES. We then say that each 144 | # of these files depends on a fake target called filename.bashchk, and then we 145 | # define a pattern rule for those targets that runs bash in check-syntax-only 146 | # mode. This mechanism has the nice properties that if you specify zero files, 147 | # the rule becomes a noop (unlike a single rule to check all bash files, which 148 | # would invoke bash with zero files), and you can check individual files from 149 | # the command line with "make filename.bashchk". 150 | # 151 | .PHONY: check-bash 152 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 153 | 154 | %.bashchk: % 155 | $(BASH) -n $^ 156 | 157 | %.bashstyle: % 158 | $(BASHSTYLE) $^ 159 | 160 | # 161 | # The above approach can be slow when there are many files to check because it 162 | # requires that "make" invoke the check tool once for each file, rather than 163 | # passing in several files at once. For the JavaScript check targets, we define 164 | # a variable for the target itself *only if* the list of input files is 165 | # non-empty. This avoids invoking the tool if there are no files to check. 166 | # 167 | JSL_NODE_TARGET = $(if $(JSL_FILES_NODE), check-jsl-node) 168 | .PHONY: check-jsl-node 169 | check-jsl-node: $(JSL_EXEC) 170 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $(JSL_FILES_NODE) 171 | 172 | JSL_WEB_TARGET = $(if $(JSL_FILES_WEB), check-jsl-web) 173 | .PHONY: check-jsl-web 174 | check-jsl-web: $(JSL_EXEC) 175 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $(JSL_FILES_WEB) 176 | 177 | .PHONY: check-jsl 178 | check-jsl: $(JSL_NODE_TARGET) $(JSL_WEB_TARGET) 179 | 180 | JSSTYLE_TARGET = $(if $(JSSTYLE_FILES), check-jsstyle) 181 | .PHONY: check-jsstyle 182 | check-jsstyle: $(JSSTYLE_EXEC) 183 | $(JSSTYLE) $(JSSTYLE_FLAGS) $(JSSTYLE_FILES) 184 | 185 | .PHONY: check 186 | check: check-jsl $(JSSTYLE_TARGET) check-bash 187 | @echo check ok 188 | 189 | .PHONY: clean 190 | clean:: 191 | -$(RMTREE) $(CLEAN_FILES) 192 | 193 | .PHONY: distclean 194 | distclean:: clean 195 | -$(RMTREE) $(DISTCLEAN_FILES) 196 | 197 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 198 | CLEAN_FILES += $(CSCOPE_FILES) 199 | 200 | .PHONY: xref 201 | xref: cscope.files 202 | $(CSCOPE) -bqR 203 | 204 | .PHONY: cscope.files 205 | cscope.files: 206 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 207 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 208 | 209 | # 210 | # The "docs" target is complicated because we do several things here: 211 | # 212 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 213 | # 214 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 215 | # functions as a complete copy of the documentation that could be 216 | # mirrored or served over HTTP. 217 | # 218 | # (3) Then copy any directories and media from docs/media into 219 | # $(DOC_BUILD)/media. This allows projects to include their own media, 220 | # including files that will override same-named files provided by 221 | # restdown. 222 | # 223 | # Step (3) is the surprisingly complex part: in order to do this, we need to 224 | # identify the subdirectories in docs/media, recreate them in 225 | # $(DOC_BUILD)/media, then do the same with the files. 226 | # 227 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 228 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 229 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 230 | 231 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 232 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 233 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 234 | 235 | # 236 | # Like the other targets, "docs" just depends on the final files we want to 237 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 238 | # to get there. 239 | # 240 | .PHONY: docs 241 | docs: \ 242 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \ 243 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \ 244 | $(DOC_MEDIA_FILES_BUILD) 245 | 246 | # 247 | # We keep the intermediate files so that the next build can see whether the 248 | # files in DOC_BUILD are up to date. 249 | # 250 | .PRECIOUS: \ 251 | $(DOC_FILES:%.restdown=docs/%.html) \ 252 | $(DOC_FILES:%.restdown=docs/%json) 253 | 254 | # 255 | # We do clean those intermediate files, as well as all of DOC_BUILD. 256 | # 257 | CLEAN_FILES += \ 258 | $(DOC_BUILD) \ 259 | $(DOC_FILES:%.restdown=docs/%.html) \ 260 | $(DOC_FILES:%.restdown=docs/%.json) 261 | 262 | # 263 | # Before installing the files, we must make sure the directories exist. The | 264 | # syntax tells make that the dependency need only exist, not be up to date. 265 | # Otherwise, it might try to rebuild spuriously because the directory itself 266 | # appears out of date. 267 | # 268 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 269 | 270 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 271 | $(CP) $< $@ 272 | 273 | docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC) 274 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 275 | 276 | $(DOC_BUILD): 277 | $(MKDIR) $@ 278 | 279 | $(DOC_MEDIA_DIRS_BUILD): 280 | $(MKDIR) $@ 281 | 282 | # 283 | # The default "test" target does nothing. This should usually be overridden by 284 | # the parent Makefile. It's included here so we can define "prepush" without 285 | # requiring the repo to define "test". 286 | # 287 | .PHONY: test 288 | test: 289 | 290 | .PHONY: prepush 291 | prepush: check test 292 | --------------------------------------------------------------------------------