>1,c=-7,f=n?i-1:0,l=n?-1:1,p=t[e+f];for(f+=l,o=p&(1<<-c)-1,p>>=-c,c+=h;c>0;o=256*o+t[e+f],f+=l,c-=8);for(s=o&(1<<-c)-1,o>>=-c,c+=r;c>0;s=256*s+t[e+f],f+=l,c-=8);if(0===o)o=1-a;else{if(o===u)return s?NaN:(p?-1:1)*(1/0);s+=Math.pow(2,r),o-=a}return(p?-1:1)*s*Math.pow(2,o-r)},e.write=function(t,e,n,r,i,o){var s,h,u,a=8*o-i-1,c=(1<>1,l=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:o-1,d=r?1:-1,g=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(h=isNaN(e)?1:0,s=c):(s=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-s))<1&&(s--,u*=2),e+=s+f>=1?l/u:l*Math.pow(2,1-f),e*u>=2&&(s++,u/=2),s+f>=c?(h=0,s=c):s+f>=1?(h=(e*u-1)*Math.pow(2,i),s+=f):(h=e*Math.pow(2,f-1)*Math.pow(2,i),s=0));i>=8;t[n+p]=255&h,p+=d,h/=256,i-=8);for(s=s<0;t[n+p]=255&s,p+=d,s/=256,a-=8);t[n+p-d]|=128*g}},function(t,e){var n={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==n.call(t)}},function(t,e,n){(function(t,r){function i(t,e){this._id=t,this._clearFn=e}var o=n(7).nextTick,s=Function.prototype.apply,h=Array.prototype.slice,u={},a=0;e.setTimeout=function(){return new i(s.call(setTimeout,window,arguments),clearTimeout)},e.setInterval=function(){return new i(s.call(setInterval,window,arguments),clearInterval)},e.clearTimeout=e.clearInterval=function(t){t.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(window,this._id)},e.enroll=function(t,e){clearTimeout(t._idleTimeoutId),t._idleTimeout=e},e.unenroll=function(t){clearTimeout(t._idleTimeoutId),t._idleTimeout=-1},e._unrefActive=e.active=function(t){clearTimeout(t._idleTimeoutId);var e=t._idleTimeout;e>=0&&(t._idleTimeoutId=setTimeout(function(){t._onTimeout&&t._onTimeout()},e))},e.setImmediate="function"==typeof t?t:function(t){var n=a++,r=!(arguments.length<2)&&h.call(arguments,1);return u[n]=!0,o(function(){u[n]&&(r?t.apply(null,r):t.call(null),e.clearImmediate(n))}),n},e.clearImmediate="function"==typeof r?r:function(t){delete u[t]}}).call(e,n(6).setImmediate,n(6).clearImmediate)},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function i(t){if(c===setTimeout)return setTimeout(t,0);if((c===n||!c)&&setTimeout)return c=setTimeout,setTimeout(t,0);try{return c(t,0)}catch(e){try{return c.call(null,t,0)}catch(e){return c.call(this,t,0)}}}function o(t){if(f===clearTimeout)return clearTimeout(t);if((f===r||!f)&&clearTimeout)return f=clearTimeout,clearTimeout(t);try{return f(t)}catch(e){try{return f.call(null,t)}catch(e){return f.call(this,t)}}}function s(){g&&p&&(g=!1,p.length?d=p.concat(d):v=-1,d.length&&h())}function h(){if(!g){var t=i(s);g=!0;for(var e=d.length;e;){for(p=d,d=[];++v >>0?1:0)|0,this._b=this._b+n+(this._bl>>>0(Math.pow(2,32)-1)*h))throw new TypeError("keylen exceeds maximum length");d.copy(a,0,0,h);for(var g=1;g 0) {
8 | crypto.getRandomValues(buf);
9 | }
10 | return buf;
11 | };
12 |
--------------------------------------------------------------------------------
/lib/nats.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Nats
3 | * Copyright(c) 2012-2016 Apcera Inc. All rights reserved.
4 | * Copyright(c) 2011-2014 Derek Collison (derek.collison@gmail.com)
5 | * MIT Licensed
6 | */
7 |
8 | /* jslint node: true */
9 | 'use strict';
10 |
11 | /**
12 | * Module Dependencies
13 | */
14 |
15 | var net = require('net'),
16 | tls = require('tls'),
17 | url = require('url'),
18 | util = require('util'),
19 | events = require('events'),
20 | nuid = require('nuid');
21 |
22 | /**
23 | * Constants
24 | */
25 |
26 | var VERSION = '0.6.8',
27 |
28 | DEFAULT_PORT = 4222,
29 | DEFAULT_PRE = 'nats://localhost:',
30 | DEFAULT_URI = DEFAULT_PRE + DEFAULT_PORT,
31 |
32 | MAX_CONTROL_LINE_SIZE = 512,
33 |
34 | // Parser state
35 | AWAITING_CONTROL = 0,
36 | AWAITING_MSG_PAYLOAD = 1,
37 |
38 | // Reconnect Parameters, 2 sec wait, 10 tries
39 | DEFAULT_RECONNECT_TIME_WAIT = 2*1000,
40 | DEFAULT_MAX_RECONNECT_ATTEMPTS = 10,
41 |
42 | // Protocol
43 | //CONTROL_LINE = /^(.*)\r\n/, // TODO: remove / never used
44 |
45 | MSG = /^MSG\s+([^\s\r\n]+)\s+([^\s\r\n]+)\s+(([^\s\r\n]+)[^\S\r\n]+)?(\d+)\r\n/i,
46 | OK = /^\+OK\s*\r\n/i,
47 | ERR = /^-ERR\s+('.+')?\r\n/i,
48 | PING = /^PING\r\n/i,
49 | PONG = /^PONG\r\n/i,
50 | INFO = /^INFO\s+([^\r\n]+)\r\n/i,
51 | SUBRE = /^SUB\s+([^\r\n]+)\r\n/i,
52 |
53 | CR_LF = '\r\n',
54 | CR_LF_LEN = CR_LF.length,
55 | EMPTY = '',
56 | SPC = ' ',
57 |
58 | // Protocol
59 | //PUB = 'PUB', // TODO: remove / never used
60 | SUB = 'SUB',
61 | UNSUB = 'UNSUB',
62 | CONNECT = 'CONNECT',
63 |
64 | // Responses
65 | PING_REQUEST = 'PING' + CR_LF,
66 | PONG_RESPONSE = 'PONG' + CR_LF,
67 |
68 | // Errors
69 | BAD_SUBJECT = 'Subject must be supplied',
70 | BAD_MSG = 'Message can\'t be a function',
71 | BAD_REPLY = 'Reply can\'t be a function',
72 | CONN_CLOSED = 'Connection closed',
73 | BAD_JSON_MSG = 'Message should be a JSON object',
74 | BAD_AUTHENTICATION = 'User and Token can not both be provided',
75 |
76 | // Pedantic Mode support
77 | //Q_SUB = /^([^\.\*>\s]+|>$|\*)(\.([^\.\*>\s]+|>$|\*))*$/, // TODO: remove / never used
78 | //Q_SUB_NO_WC = /^([^\.\*>\s]+)(\.([^\.\*>\s]+))*$/, // TODO: remove / never used
79 |
80 | FLUSH_THRESHOLD = 65536;
81 |
82 | /**
83 | * Library Version
84 | */
85 |
86 | exports.version = VERSION;
87 |
88 | /**
89 | * Create a properly formatted inbox subject.
90 | *
91 | * @api public
92 | */
93 |
94 | var createInbox = exports.createInbox = function() {
95 | return ("_INBOX." + nuid.next());
96 | };
97 |
98 | /**
99 | * Initialize a client with the appropriate options.
100 | *
101 | * @param {Mixed} opts
102 | * @api public
103 | */
104 |
105 | function Client(opts) {
106 | events.EventEmitter.call(this);
107 | this.parseOptions(opts);
108 | this.initState();
109 | this.createConnection();
110 | }
111 |
112 | /**
113 | * Connect to a nats-server and return the client.
114 | * Argument can be a url, or an object with a 'url'
115 | * property and additional options.
116 | *
117 | * @params {Mixed} opts
118 | *
119 | * @api public
120 | */
121 |
122 | exports.connect = function(opts) {
123 | return new Client(opts);
124 | };
125 |
126 | /**
127 | * Connected clients are event emitters.
128 | */
129 |
130 | util.inherits(Client, events.EventEmitter);
131 |
132 | /**
133 | * Allow createInbox to be called on a client.
134 | *
135 | * @api public
136 | */
137 |
138 | Client.prototype.createInbox = createInbox;
139 |
140 | Client.prototype.assignOption = function(opts, prop, assign) {
141 | if (assign === undefined) {
142 | assign = prop;
143 | }
144 | if (opts[prop] !== undefined) {
145 | this.options[assign] = opts[prop];
146 | }
147 | };
148 |
149 | function shuffle(array) {
150 | for (var i = array.length - 1; i > 0; i--) {
151 | var j = Math.floor(Math.random() * (i + 1));
152 | var temp = array[i];
153 | array[i] = array[j];
154 | array[j] = temp;
155 | }
156 | return array;
157 | }
158 |
159 | /**
160 | * Parse the conctructor/connect options.
161 | *
162 | * @param {Mixed} opts
163 | * @api private
164 | */
165 |
166 | Client.prototype.parseOptions = function(opts) {
167 | var options = this.options = {
168 | 'verbose' : false,
169 | 'pedantic' : false,
170 | 'reconnect' : true,
171 | 'maxReconnectAttempts' : DEFAULT_MAX_RECONNECT_ATTEMPTS,
172 | 'reconnectTimeWait' : DEFAULT_RECONNECT_TIME_WAIT,
173 | 'encoding' : 'utf8',
174 | 'tls' : false,
175 | 'waitOnFirstConnect' : false,
176 | };
177 |
178 | if (undefined === opts) {
179 | options.url = DEFAULT_URI;
180 | } else if ('number' === typeof opts) {
181 | options.url = DEFAULT_PRE + opts;
182 | } else if ('string' === typeof opts) {
183 | options.url = opts;
184 | } else if ('object' === typeof opts) {
185 | if (opts.port !== undefined) {
186 | options.url = DEFAULT_PRE + opts.port;
187 | }
188 | // Pull out various options here
189 | this.assignOption(opts, 'url');
190 | this.assignOption(opts, 'uri', 'url');
191 | this.assignOption(opts, 'user');
192 | this.assignOption(opts, 'pass');
193 | this.assignOption(opts, 'token');
194 | this.assignOption(opts, 'password', 'pass');
195 | this.assignOption(opts, 'verbose');
196 | this.assignOption(opts, 'pedantic');
197 | this.assignOption(opts, 'reconnect');
198 | this.assignOption(opts, 'maxReconnectAttempts');
199 | this.assignOption(opts, 'reconnectTimeWait');
200 | this.assignOption(opts, 'servers');
201 | this.assignOption(opts, 'urls', 'servers');
202 | this.assignOption(opts, 'noRandomize');
203 | this.assignOption(opts, 'NoRandomize', 'noRandomize');
204 | this.assignOption(opts, 'dontRandomize', 'noRandomize');
205 | this.assignOption(opts, 'encoding');
206 | this.assignOption(opts, 'tls');
207 | this.assignOption(opts, 'secure', 'tls');
208 | this.assignOption(opts, 'name');
209 | this.assignOption(opts, 'client', 'name');
210 | this.assignOption(opts, 'yieldTime');
211 | this.assignOption(opts, 'waitOnFirstConnect');
212 | this.assignOption(opts, 'json');
213 | }
214 |
215 | var client = this;
216 |
217 | // Set user/pass as needed if in options.
218 | client.user = options.user;
219 | client.pass = options.pass;
220 |
221 | // Set token as needed if in options.
222 | client.token = options.token;
223 |
224 | // Authentication - make sure authentication is valid.
225 | if (client.user && client.token) {
226 | throw(new Error(BAD_AUTHENTICATION));
227 | }
228 |
229 | // Encoding - make sure its valid.
230 | if (Buffer.isEncoding(options.encoding)) {
231 | client.encoding = options.encoding;
232 | } else {
233 | throw new Error('Invalid Encoding:' + options.encoding);
234 | }
235 | // For cluster support
236 | client.servers = [];
237 |
238 | if (Array.isArray(options.servers)) {
239 | options.servers.forEach(function(server) {
240 | client.servers.push(new Server(url.parse(server)));
241 | });
242 | } else {
243 | if (undefined === options.url) {
244 | options.url = DEFAULT_URI;
245 | }
246 | client.servers.push(new Server(url.parse(options.url)));
247 | }
248 |
249 | // Randomize if needed
250 | if (options.noRandomize !== true) {
251 | shuffle(client.servers);
252 | }
253 | };
254 |
255 | /**
256 | * Create a new server.
257 | *
258 | * @api private
259 | */
260 |
261 | function Server(url) {
262 | this.url = url;
263 | this.didConnect = false;
264 | this.reconnects = 0;
265 | }
266 |
267 | /**
268 | * Properly select the next server.
269 | * We rotate the server list as we go,
270 | * we also pull auth from urls as needed, or
271 | * if they were set in options use that as override.
272 | *
273 | * @api private
274 | */
275 |
276 | Client.prototype.selectServer = function() {
277 | var client = this;
278 | var server = client.servers.shift();
279 |
280 | // Place in client context.
281 | client.currentServer = server;
282 | client.url = server.url;
283 | if ('auth' in server.url && !!server.url.auth) {
284 | var auth = server.url.auth.split(':');
285 | if (auth.length !== 1) {
286 | if (client.options.user === undefined) {
287 | client.user = auth[0];
288 | }
289 | if (client.options.pass === undefined) {
290 | client.pass = auth[1];
291 | }
292 | } else {
293 | if (client.options.token === undefined) {
294 | client.token = auth[0];
295 | }
296 | }
297 | }
298 | client.servers.push(server);
299 | };
300 |
301 | /**
302 | * Check for TLS configuration mismatch.
303 | *
304 | * @api private
305 | */
306 |
307 | Client.prototype.checkTLSMismatch = function() {
308 | if (this.info.tls_required === true &&
309 | this.options.tls === false) {
310 | this.emit('error', 'Server requires a secure connection.');
311 | this.closeStream();
312 | return true;
313 | }
314 |
315 | if (this.info.tls_required === false &&
316 | this.options.tls !== false) {
317 | this.emit('error', 'Server does not support a secure connection.');
318 | this.closeStream();
319 | return true;
320 | }
321 |
322 | if (this.info.tls_verify === true &&
323 | this.options.tls.cert === undefined) {
324 | this.emit('error', 'Server requires a client certificate.');
325 | this.closeStream();
326 | return true;
327 | }
328 | return false;
329 | };
330 |
331 | /**
332 | * Callback for first flush/connect.
333 | *
334 | * @api private
335 | */
336 |
337 | Client.prototype.connectCB = function() {
338 | var wasReconnecting = this.reconnecting;
339 | var event = (wasReconnecting === true) ? 'reconnect' : 'connect';
340 | this.reconnecting = false;
341 | this.reconnects = 0;
342 | this.wasConnected = true;
343 | this.currentServer.didConnect = true;
344 |
345 | this.emit(event, this);
346 |
347 | this.flushPending();
348 | };
349 |
350 |
351 | /**
352 | * Properly setup a stream event handlers.
353 | *
354 | * @api private
355 | */
356 |
357 | Client.prototype.setupHandlers = function() {
358 | var client = this;
359 | var stream = client.stream;
360 |
361 | if (undefined === stream) {
362 | return;
363 | }
364 |
365 | stream.on('connect', function() {
366 | client.connected = true;
367 | });
368 |
369 | stream.on('close', function(hadError) {
370 | client.closeStream();
371 | client.emit('disconnect');
372 | if (client.closed === true ||
373 | client.options.reconnect === false ||
374 | ((client.reconnects >= client.options.maxReconnectAttempts) && client.options.maxReconnectAttempts !== -1)) {
375 | client.emit('close');
376 | } else {
377 | client.scheduleReconnect();
378 | }
379 | });
380 |
381 | stream.on('error', function(exception) {
382 | // If we were connected just return, close event will process
383 | if (client.wasConnected === true && client.currentServer.didConnect === true) {
384 | return;
385 | }
386 |
387 | // if the current server did not connect at all, and we in
388 | // general have not connected to any server, remove it from
389 | // this list. Unless overidden
390 | if (client.wasConnected === false && client.currentServer.didConnect === false) {
391 | // We can override this behavior with waitOnFirstConnect, which will
392 | // treat it like a reconnect scenario.
393 | if (client.options.waitOnFirstConnect) {
394 | // Pretend to move us into a reconnect state.
395 | client.currentServer.didConnect = true;
396 | } else {
397 | client.servers.splice(client.servers.length-1, 1);
398 | }
399 | }
400 |
401 | // Only bubble up error if we never had connected
402 | // to the server and we only have one.
403 | if (client.wasConnected === false && client.servers.length === 0) {
404 | client.emit('error', 'Could not connect to server: ' + exception);
405 | }
406 | client.closeStream();
407 | });
408 |
409 | stream.on('data', function (data) {
410 | // If inbound exists, concat them together. We try to avoid this for split
411 | // messages, so this should only really happen for a split control line.
412 | // Long term answer is hand rolled parser and not regexp.
413 | if (client.inbound) {
414 | client.inbound = Buffer.concat([client.inbound, data]);
415 | } else {
416 | client.inbound = data;
417 | }
418 |
419 | // Process the inbound queue.
420 | client.processInbound();
421 | });
422 | };
423 |
424 | /**
425 | * Send the connect command. This needs to happen after receiving the first
426 | * INFO message and after TLS is established if necessary.
427 | *
428 | * @api private
429 | */
430 |
431 | Client.prototype.sendConnect = function() {
432 | // Queue the connect command.
433 | var cs = {
434 | 'lang' : 'node',
435 | 'version' : VERSION,
436 | 'verbose' : this.options.verbose,
437 | 'pedantic': this.options.pedantic
438 | };
439 | if (this.user !== undefined) {
440 | cs.user = this.user;
441 | cs.pass = this.pass;
442 | }
443 | if (this.token !== undefined) {
444 | cs.auth_token = this.token;
445 | }
446 | if (this.options.name !== undefined) {
447 | cs.name = this.options.name;
448 | }
449 |
450 | // If we enqueued requests before we received INFO from the server, or we
451 | // reconnected, there be other data pending, write this immediately instead
452 | // of adding it to the queue.
453 | this.stream.write(CONNECT + SPC + JSON.stringify(cs) + CR_LF);
454 | };
455 |
456 | /**
457 | * Properly setup a stream connection with proper events.
458 | *
459 | * @api private
460 | */
461 |
462 | Client.prototype.createConnection = function() {
463 | // Commands may have been queued during reconnect. Discard everything except:
464 | // 1) ping requests with a pong callback
465 | // 2) publish requests
466 | //
467 | // Rationale: CONNECT and SUBs are written directly upon connecting, any PONG
468 | // response is no longer relevant, and any UNSUB will be accounted for when we
469 | // sync our SUBs. Without this, users of the client may miss state transitions
470 | // via callbacks, would have to track the client's internal connection state,
471 | // and may have to double buffer messages (which we are already doing) if they
472 | // wanted to ensure their messages reach the server.
473 | var pong = [];
474 | var pend = [];
475 | var pSize = 0;
476 | var client = this;
477 | if (client.pending !== null) {
478 | var pongIndex = 0;
479 | client.pending.forEach(function(cmd) {
480 | var cmdLen = Buffer.isBuffer(cmd) ? cmd.length : Buffer.byteLength(cmd);
481 | if (cmd === PING_REQUEST && client.pongs !== null && pongIndex < client.pongs.length) {
482 | // filter out any useless ping requests (no pong callback, nop flush)
483 | var p = client.pongs[pongIndex++];
484 | if (p !== undefined) {
485 | pend.push(cmd);
486 | pSize += cmdLen;
487 | pong.push(p);
488 | }
489 | } else if (cmd.length > 3 && cmd[0] == 'P' && cmd[1] == 'U' && cmd[2] == 'B') {
490 | pend.push(cmd);
491 | pSize += cmdLen;
492 | }
493 | });
494 | }
495 | this.pongs = pong;
496 | this.pending = pend;
497 | this.pSize = pSize;
498 |
499 | this.pstate = AWAITING_CONTROL;
500 |
501 | // Clear info processing.
502 | this.info = null;
503 | this.infoReceived = false;
504 |
505 | // Select a server to connect to.
506 | this.selectServer();
507 | // Create the stream.
508 | this.stream = net.createConnection(this.url);
509 | // Setup the proper handlers.
510 | this.setupHandlers();
511 | };
512 |
513 | /**
514 | * Initialize client state.
515 | *
516 | * @api private
517 | */
518 |
519 | Client.prototype.initState = function() {
520 | this.ssid = 1;
521 | this.subs = {};
522 | this.reconnects = 0;
523 | this.connected = false;
524 | this.wasConnected = false;
525 | this.reconnecting = false;
526 | this.server = null;
527 | this.pending = [];
528 | };
529 |
530 | /**
531 | * Close the connection to the server.
532 | *
533 | * @api public
534 | */
535 |
536 | Client.prototype.close = function() {
537 | this.closed = true;
538 | this.removeAllListeners();
539 | this.closeStream();
540 | this.ssid = -1;
541 | this.subs = null;
542 | this.pstate = -1;
543 | this.pongs = null;
544 | this.pending = null;
545 | this.pSize = 0;
546 | };
547 |
548 | /**
549 | * Close down the stream and clear state.
550 | *
551 | * @api private
552 | */
553 |
554 | Client.prototype.closeStream = function() {
555 | if (this.stream !== null) {
556 | this.stream.end();
557 | this.stream.destroy();
558 | this.stream = null;
559 | }
560 | if (this.connected === true || this.closed === true) {
561 | this.pongs = null;
562 | this.pending = null;
563 | this.pSize = 0;
564 | this.connected = false;
565 | }
566 | this.inbound = null;
567 | };
568 |
569 | /**
570 | * Flush all pending data to the server.
571 | *
572 | * @api private
573 | */
574 |
575 | Client.prototype.flushPending = function() {
576 | if (this.connected === false ||
577 | this.pending === null ||
578 | this.pending.length === 0 ||
579 | this.infoReceived !== true) {
580 | return;
581 | }
582 |
583 | var client = this;
584 | var write = function(data) {
585 | client.pending = [];
586 | client.pSize = 0;
587 | return client.stream.write(data);
588 | };
589 | if (!this.pBufs) {
590 | // All strings, fastest for now.
591 | return write(this.pending.join(EMPTY));
592 | } else {
593 | // We have some or all Buffers. Figure out if we can optimize.
594 | var allBufs = true;
595 | for (var i=0; i < this.pending.length; i++){
596 | if (!Buffer.isBuffer(this.pending[i])) {
597 | allBufs = false;
598 | break;
599 | }
600 | }
601 | // If all buffers, concat together and write once.
602 | if (allBufs) {
603 | return write(Buffer.concat(this.pending, this.pSize));
604 | } else {
605 | // We have a mix, so write each one individually.
606 | var pending = this.pending;
607 | this.pending = [];
608 | this.pSize = 0;
609 | var result = true;
610 | for (i=0; i < pending.length; i++){
611 | result = this.stream.write(pending[i]) && result;
612 | }
613 | return result;
614 | }
615 | }
616 | };
617 |
618 | /**
619 | * Strips all SUBS commands from pending during initial connection completed since
620 | * we send the subscriptions as a separate operation.
621 | *
622 | * @api private
623 | */
624 |
625 | Client.prototype.stripPendingSubs = function() {
626 | var pending = this.pending;
627 | this.pending = [];
628 | this.pSize = 0;
629 | for (var i=0; i < pending.length; i++){
630 | if (!SUBRE.test(pending[i])) {
631 | // Re-queue the command.
632 | this.sendCommand(pending[i]);
633 | }
634 | }
635 | };
636 |
637 | /**
638 | * Send commands to the server or queue them up if connection pending.
639 | *
640 | * @api private
641 | */
642 |
643 | Client.prototype.sendCommand = function(cmd) {
644 | // Buffer to cut down on system calls, increase throughput.
645 | // When receive gets faster, should make this Buffer based..
646 |
647 | if (this.closed || this.pending === null) { return; }
648 |
649 | this.pending.push(cmd);
650 | if (!Buffer.isBuffer(cmd)) {
651 | this.pSize += Buffer.byteLength(cmd);
652 | } else {
653 | this.pSize += cmd.length;
654 | this.pBufs = true;
655 | }
656 |
657 | if (this.connected === true) {
658 | // First one let's setup flush..
659 | if (this.pending.length === 1) {
660 | var self = this;
661 | setImmediate(function() {
662 | self.flushPending();
663 | });
664 | } else if (this.pSize > FLUSH_THRESHOLD) {
665 | // Flush in place when threshold reached..
666 | this.flushPending();
667 | }
668 | }
669 | };
670 |
671 | /**
672 | * Sends existing subscriptions to new server after reconnect.
673 | *
674 | * @api private
675 | */
676 |
677 | Client.prototype.sendSubscriptions = function() {
678 | var protos = "";
679 | for (var sid in this.subs) {
680 | if (this.subs.hasOwnProperty(sid)) {
681 | var sub = this.subs[sid];
682 | var proto;
683 | if (sub.qgroup) {
684 | proto = [SUB, sub.subject, sub.qgroup, sid + CR_LF];
685 | } else {
686 | proto = [SUB, sub.subject, sid + CR_LF];
687 | }
688 | protos += proto.join(SPC);
689 | }
690 | }
691 | if (protos.length > 0) {
692 | this.stream.write(protos);
693 | }
694 | };
695 |
696 | /**
697 | * Process the inbound data queue.
698 | *
699 | * @api private
700 | */
701 |
702 | Client.prototype.processInbound = function() {
703 | var client = this;
704 |
705 | // Hold any regex matches.
706 | var m;
707 |
708 | // For optional yield
709 | var start;
710 |
711 | // unpause if needed.
712 | // FIXME(dlc) client.stream.isPaused() causes 0.10 to fail
713 | client.stream.resume();
714 |
715 | /* jshint -W083 */
716 |
717 | if (client.options.yieldTime !== undefined) {
718 | start = Date.now();
719 | }
720 |
721 | while (!client.closed && client.inbound && client.inbound.length > 0) {
722 | switch (client.pstate) {
723 |
724 | case AWAITING_CONTROL:
725 | // Regex only works on strings, so convert once to be more efficient.
726 | // Long term answer is a hand rolled parser, not regex.
727 | var buf = client.inbound.toString('binary', 0, MAX_CONTROL_LINE_SIZE);
728 | if ((m = MSG.exec(buf)) !== null) {
729 | client.payload = {
730 | subj : m[1],
731 | sid : parseInt(m[2], 10),
732 | reply : m[4],
733 | size : parseInt(m[5], 10)
734 | };
735 | client.payload.psize = client.payload.size + CR_LF_LEN;
736 | client.pstate = AWAITING_MSG_PAYLOAD;
737 | } else if ((m = OK.exec(buf)) !== null) {
738 | // Ignore for now..
739 | } else if ((m = ERR.exec(buf)) !== null) {
740 | client.emit('error', m[1]);
741 | } else if ((m = PONG.exec(buf)) !== null) {
742 | var cb = client.pongs && client.pongs.shift();
743 | if (cb) { cb(); } // FIXME: Should we check for exceptions?
744 | } else if ((m = PING.exec(buf)) !== null) {
745 | client.sendCommand(PONG_RESPONSE);
746 | } else if ((m = INFO.exec(buf)) !== null) {
747 | client.info = JSON.parse(m[1]);
748 | // Check on TLS mismatch.
749 | if (client.checkTLSMismatch() === true) {
750 | return;
751 | }
752 | // Process first INFO
753 | if (client.infoReceived === false) {
754 | // Switch over to TLS as needed.
755 | if (client.options.tls !== false &&
756 | client.stream.encrypted !== true) {
757 | var tlsOpts = {socket: client.stream};
758 | if ('object' === typeof client.options.tls) {
759 | for (var key in client.options.tls) {
760 | tlsOpts[key] = client.options.tls[key];
761 | }
762 | }
763 | client.stream = tls.connect(tlsOpts, function() {
764 | client.flushPending();
765 | });
766 | client.setupHandlers();
767 | }
768 |
769 | // Send the connect message and subscriptions immediately
770 | client.sendConnect();
771 | client.sendSubscriptions();
772 |
773 | client.pongs.unshift(function() { client.connectCB(); });
774 | client.stream.write(PING_REQUEST);
775 |
776 | // Mark as received
777 | client.infoReceived = true;
778 | client.stripPendingSubs();
779 | client.flushPending();
780 | }
781 | } else {
782 | // FIXME, check line length for something weird.
783 | // Nothing here yet, return
784 | return;
785 | }
786 | break;
787 |
788 | case AWAITING_MSG_PAYLOAD:
789 |
790 | // If we do not have the complete message, hold onto the chunks
791 | // and assemble when we have all we need. This optimizes for
792 | // when we parse a large buffer down to a small number of bytes,
793 | // then we receive a large chunk. This avoids a big copy with a
794 | // simple concat above.
795 | if (client.inbound.length < client.payload.psize) {
796 | if (undefined === client.payload.chunks) {
797 | client.payload.chunks = [];
798 | }
799 | client.payload.chunks.push(client.inbound);
800 | client.payload.psize -= client.inbound.length;
801 | client.inbound = null;
802 | return;
803 | }
804 |
805 | // If we are here we have the complete message.
806 | // Check to see if we have existing chunks
807 | if (client.payload.chunks) {
808 | client.payload.chunks.push(client.inbound.slice(0, client.payload.psize));
809 | var mbuf = Buffer.concat(client.payload.chunks, client.payload.size+CR_LF_LEN);
810 | client.payload.msg = mbuf.toString(client.encoding, 0, client.payload.size);
811 | } else {
812 | client.payload.msg = client.inbound.toString(client.encoding, 0, client.payload.size);
813 | }
814 |
815 | // Eat the size of the inbound that represents the message.
816 | if (client.inbound.length === client.payload.psize) {
817 | client.inbound = null;
818 | } else {
819 | client.inbound = client.inbound.slice(client.payload.psize);
820 | }
821 |
822 | // process the message
823 | client.processMsg();
824 |
825 | // Reset
826 | client.pstate = AWAITING_CONTROL;
827 | client.payload = null;
828 |
829 | // Check to see if we have an option to yield for other events after yieldTime.
830 | if (start !== undefined) {
831 | if ((Date.now() - start) > client.options.yieldTime) {
832 | client.stream.pause();
833 | setImmediate(client.processInbound.bind(this));
834 | return;
835 | }
836 | }
837 | break;
838 | }
839 |
840 | // This is applicable for a regex match to eat the bytes we used from a control line.
841 | if (m && !this.closed) {
842 | // Chop inbound
843 | var psize = m[0].length;
844 | if (psize >= client.inbound.length) {
845 | client.inbound = null;
846 | } else {
847 | client.inbound = client.inbound.slice(psize);
848 | }
849 | }
850 | m = null;
851 | }
852 | };
853 |
854 | /**
855 | * Process a delivered message and deliver to appropriate subscriber.
856 | *
857 | * @api private
858 | */
859 |
860 | Client.prototype.processMsg = function() {
861 | var sub = this.subs[this.payload.sid];
862 | if (sub !== undefined) {
863 | sub.received += 1;
864 | // Check for a timeout, and cancel if received >= expected
865 | if (sub.timeout) {
866 | if (sub.received >= sub.expected) {
867 | clearTimeout(sub.timeout);
868 | sub.timeout = null;
869 | }
870 | }
871 | // Check for auto-unsubscribe
872 | if (sub.max !== undefined) {
873 | if (sub.received === sub.max) {
874 | delete this.subs[this.payload.sid];
875 | this.emit('unsubscribe', this.payload.sid, sub.subject);
876 | } else if (sub.received > sub.max) {
877 | this.unsubscribe(this.payload.sid);
878 | sub.callback = null;
879 | }
880 | }
881 |
882 | if (sub.callback) {
883 | var msg = this.payload.msg;
884 | if (this.options.json) {
885 | try {
886 | msg = JSON.parse(new Buffer(this.payload.msg, this.options.encoding).toString());
887 | } catch (e) {
888 | msg = e;
889 | }
890 | }
891 | sub.callback(msg, this.payload.reply, this.payload.subj, this.payload.sid);
892 | }
893 | }
894 | };
895 |
896 | /**
897 | * Push a new cluster server.
898 | *
899 | * @param {String} uri
900 | * @api public
901 | */
902 |
903 | Client.prototype.addServer = function(uri) {
904 | this.servers.push(new Server(url.parse(uri)));
905 |
906 | if (this.options.noRandomize !== true) {
907 | shuffle(this.servers);
908 | }
909 | };
910 |
911 | /**
912 | * Flush outbound queue to server and call optional callback when server has processed
913 | * all data.
914 | *
915 | * @param {Function} opt_callback
916 | * @api public
917 | */
918 |
919 | Client.prototype.flush = function(opt_callback) {
920 | if (this.closed) {
921 | if (typeof opt_callback === 'function') {
922 | opt_callback(new Error(CONN_CLOSED));
923 | return;
924 | } else {
925 | throw(new Error(CONN_CLOSED));
926 | }
927 | }
928 | if (this.pongs) {
929 | this.pongs.push(opt_callback);
930 | this.sendCommand(PING_REQUEST);
931 | this.flushPending();
932 | }
933 | };
934 |
935 | /**
936 | * Publish a message to the given subject, with optional reply and callback.
937 | *
938 | * @param {String} subject
939 | * @param {String} opt_msg
940 | * @param {String} opt_reply
941 | * @param {Function} opt_callback
942 | * @api public
943 | */
944 |
945 | Client.prototype.publish = function(subject, msg, opt_reply, opt_callback) {
946 | // They only supplied a callback function.
947 | if (typeof subject === 'function') {
948 | opt_callback = subject;
949 | subject = undefined;
950 | }
951 | if (!msg) { msg = EMPTY; }
952 | if (!subject) {
953 | if (opt_callback) {
954 | opt_callback(new Error(BAD_SUBJECT));
955 | } else {
956 | throw(new Error(BAD_SUBJECT));
957 | }
958 | }
959 | if (typeof msg === 'function') {
960 | if (opt_callback || opt_reply) {
961 | opt_callback(new Error(BAD_MSG));
962 | return;
963 | }
964 | opt_callback = msg;
965 | msg = EMPTY;
966 | opt_reply = undefined;
967 | }
968 | if (typeof opt_reply === 'function') {
969 | if (opt_callback) {
970 | opt_callback(new Error(BAD_REPLY));
971 | return;
972 | }
973 | opt_callback = opt_reply;
974 | opt_reply = undefined;
975 | }
976 |
977 | // Hold PUB SUB [REPLY]
978 | var psub;
979 | if (opt_reply === undefined) {
980 | psub = 'PUB ' + subject + SPC;
981 | } else {
982 | psub = 'PUB ' + subject + SPC + opt_reply + SPC;
983 | }
984 |
985 | if ('ArrayBuffer' in window && ArrayBuffer.isView(msg)) {
986 | msg = Buffer.from(msg);
987 | }
988 |
989 | // Need to treat sending buffers different.
990 | if (!Buffer.isBuffer(msg)) {
991 | var str = msg;
992 | if (this.options.json) {
993 | if (typeof msg !== 'object' || Array.isArray(msg)) {
994 | throw(new Error(BAD_JSON_MSG));
995 | }
996 | try {
997 | str = JSON.stringify(msg);
998 | } catch (e) {
999 | throw(new Error(BAD_JSON_MSG));
1000 | }
1001 | }
1002 | this.sendCommand(psub + Buffer.byteLength(str) + CR_LF + str + CR_LF);
1003 | } else {
1004 | var b = new Buffer(psub.length + msg.length + (2 * CR_LF_LEN) + msg.length.toString().length);
1005 | var len = b.write(psub + msg.length + CR_LF);
1006 | msg.copy(b, len);
1007 | b.write(CR_LF, len + msg.length);
1008 | this.sendCommand(b);
1009 | }
1010 |
1011 | if (opt_callback !== undefined) {
1012 | this.flush(opt_callback);
1013 | } else if (this.closed) {
1014 | throw(new Error(CONN_CLOSED));
1015 | }
1016 | };
1017 |
1018 | /**
1019 | * Subscribe to a given subject, with optional options and callback. opts can be
1020 | * ommitted, even with a callback. The Subscriber Id is returned.
1021 | *
1022 | * @param {String} subject
1023 | * @param {Object} opts
1024 | * @param {Function} callback
1025 | * @return {Mixed}
1026 | * @api public
1027 | */
1028 |
1029 | Client.prototype.subscribe = function(subject, opts, callback) {
1030 | if (this.closed) {
1031 | throw(new Error(CONN_CLOSED));
1032 | }
1033 | var qgroup, max;
1034 | if (typeof opts === 'function') {
1035 | callback = opts;
1036 | opts = undefined;
1037 | } else if (opts && typeof opts === 'object') {
1038 | // FIXME, check exists, error otherwise..
1039 | qgroup = opts.queue;
1040 | max = opts.max;
1041 | }
1042 | this.ssid += 1;
1043 | this.subs[this.ssid] = { 'subject':subject, 'callback':callback, 'received':0 };
1044 |
1045 | var proto;
1046 | if (typeof qgroup === 'string') {
1047 | this.subs[this.ssid].qgroup = qgroup;
1048 | proto = [SUB, subject, qgroup, this.ssid + CR_LF];
1049 | } else {
1050 | proto = [SUB, subject, this.ssid + CR_LF];
1051 | }
1052 |
1053 | this.sendCommand(proto.join(SPC));
1054 | this.emit('subscribe', this.ssid, subject, opts);
1055 |
1056 | if (max) {
1057 | this.unsubscribe(this.ssid, max);
1058 | }
1059 | return this.ssid;
1060 | };
1061 |
1062 | /**
1063 | * Unsubscribe to a given Subscriber Id, with optional max parameter.
1064 | *
1065 | * @param {Mixed} sid
1066 | * @param {Number} opt_max
1067 | * @api public
1068 | */
1069 |
1070 | Client.prototype.unsubscribe = function(sid, opt_max) {
1071 | if (!sid || this.closed) { return; }
1072 |
1073 | var proto;
1074 | if (opt_max) {
1075 | proto = [UNSUB, sid, opt_max + CR_LF];
1076 | } else {
1077 | proto = [UNSUB, sid + CR_LF];
1078 | }
1079 | this.sendCommand(proto.join(SPC));
1080 |
1081 | var sub = this.subs[sid];
1082 | if (sub === undefined) {
1083 | return;
1084 | }
1085 | sub.max = opt_max;
1086 | if (sub.max === undefined || (sub.received >= sub.max)) {
1087 | delete this.subs[sid];
1088 | this.emit('unsubscribe', sid, sub.subject);
1089 | }
1090 | };
1091 |
1092 | /**
1093 | * Set a timeout on a subscription.
1094 | *
1095 | * @param {Mixed} sid
1096 | * @param {Number} timeout
1097 | * @param {Number} expected
1098 | * @api public
1099 | */
1100 |
1101 | Client.prototype.timeout = function(sid, timeout, expected, callback) {
1102 | if (!sid) { return; }
1103 | var sub = this.subs[sid];
1104 | if (sub === null) { return; }
1105 | sub.expected = expected;
1106 | sub.timeout = setTimeout(function() { callback(sid); }, timeout);
1107 | };
1108 |
1109 | /**
1110 | * Publish a message with an implicit inbox listener as the reply. Message is optional.
1111 | * This should be treated as a subscription. You can optionally indicate how many
1112 | * messages you only want to receive using opt_options = {max:N}. Otherwise you
1113 | * will need to unsubscribe to stop the message stream.
1114 | * The Subscriber Id is returned.
1115 | *
1116 | * @param {String} subject
1117 | * @param {String} opt_msg
1118 | * @param {Object} opt_options
1119 | * @param {Function} callback
1120 | * @return {Mixed}
1121 | * @api public
1122 | */
1123 |
1124 | Client.prototype.request = function(subject, opt_msg, opt_options, callback) {
1125 | if (typeof opt_msg === 'function') {
1126 | callback = opt_msg;
1127 | opt_msg = EMPTY;
1128 | opt_options = null;
1129 | }
1130 | if (typeof opt_options === 'function') {
1131 | callback = opt_options;
1132 | opt_options = null;
1133 | }
1134 | var inbox = createInbox();
1135 | var s = this.subscribe(inbox, opt_options, function(msg, reply) {
1136 | callback(msg, reply);
1137 | });
1138 | this.publish(subject, opt_msg, inbox);
1139 | return s;
1140 | };
1141 |
1142 | /**
1143 | * Report number of outstanding subscriptions on this connection.
1144 | *
1145 | * @return {Number}
1146 | * @api public
1147 | */
1148 |
1149 | Client.prototype.numSubscriptions = function() {
1150 | return Object.keys(this.subs).length;
1151 | };
1152 |
1153 | /**
1154 | * Reconnect to the server.
1155 | *
1156 | * @api private
1157 | */
1158 |
1159 | Client.prototype.reconnect = function() {
1160 | if (this.closed) { return; }
1161 | this.reconnects += 1;
1162 | this.createConnection();
1163 | if (this.currentServer.didConnect === true) {
1164 | this.emit('reconnecting');
1165 | }
1166 | };
1167 |
1168 | /**
1169 | * Setup a timer event to attempt reconnect.
1170 | *
1171 | * @api private
1172 | */
1173 |
1174 | Client.prototype.scheduleReconnect = function() {
1175 | var client = this;
1176 | // Just return if no more servers
1177 | if (client.servers.length === 0) {
1178 | return;
1179 | }
1180 | // Don't set reconnecting state if we are just trying
1181 | // for the first time.
1182 | if (client.wasConnected === true) {
1183 | client.reconnecting = true;
1184 | }
1185 | // Only stall if we have connected before.
1186 | var wait = 0;
1187 | if (client.servers[0].didConnect === true) {
1188 | wait = this.options.reconnectTimeWait;
1189 | }
1190 | setTimeout(function() { client.reconnect(); }, wait);
1191 | };
1192 |
--------------------------------------------------------------------------------
/lib/net.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var util = require('util');
4 | var EventEmitter = require('events').EventEmitter;
5 |
6 | function WebSocketProxy(url) {
7 | var self = this;
8 | EventEmitter.call(this);
9 | this.sock = new WebSocket(url);
10 | this.sock.addEventListener('open', function(e) {
11 | self.emit('connect');
12 | });
13 | this.sock.addEventListener('message', function(e) {
14 | self.emit('data', new Buffer(e.data));
15 | });
16 | this.sock.addEventListener('error', function(e) {
17 | self.emit('error', e);
18 | });
19 | this.sock.addEventListener('close', function(e) {
20 | self.emit('close');
21 | });
22 | }
23 | util.inherits(WebSocketProxy, EventEmitter);
24 |
25 | WebSocketProxy.prototype.end = function() {
26 | this.destroy();
27 | }
28 |
29 | WebSocketProxy.prototype.destroy = function() {
30 | if (
31 | this.sock.readyState === WebSocket.CONNECTING ||
32 | this.sock.readyState === WebSocket.OPEN
33 | ) {
34 | this.sock.close();
35 | }
36 | }
37 |
38 | WebSocketProxy.prototype.write = function(data) {
39 | if (this.sock.readyState === WebSocket.OPEN) {
40 | this.sock.send(data);
41 | }
42 | }
43 |
44 | WebSocketProxy.prototype.pause = function() {
45 | console.warn('WebSocketProxy stream pause/resume is not supported yet.');
46 | }
47 |
48 | WebSocketProxy.prototype.resume = function() {}
49 |
50 | exports.createConnection = function(url) {
51 | // The url is rebuilt to avoid including the auth credentials.
52 | return new WebSocketProxy(url.format({
53 | protocol: url.protocol,
54 | slashes: url.slashes,
55 | host: url.host,
56 | hostname: url.hostname,
57 | port: url.port,
58 | pathname: url.pathname,
59 | search: url.search,
60 | path: url.path,
61 | query: url.query,
62 | hash: url.hash
63 | }));
64 | }
65 |
--------------------------------------------------------------------------------
/lib/tls.js:
--------------------------------------------------------------------------------
1 | exports.connect = function(opts, cb) {
2 | throw "TLS is not supported in the browser. Use WSS instead.";
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "websocket-nats",
3 | "version": "0.3.3",
4 | "description": "An in-browser websocket client for [NATS](http://nats.io/), a lightweight, high-performance cloud native messaging system.",
5 | "keywords": [
6 | "websocket",
7 | "websockets",
8 | "ws",
9 | "nats",
10 | "browser"
11 | ],
12 | "license": "MIT",
13 | "author": {
14 | "name": "Josh Glendenning",
15 | "email": "josh@isobit.io"
16 | },
17 | "main": "index.js",
18 | "repository": "isobit/websocket-nats",
19 | "scripts": {
20 | "build": "webpack && npm run build:prod && npm run build:dev",
21 | "build:prod": "NODE_ENV=prod webpack",
22 | "build:dev": "NODE_ENV=dev webpack",
23 | "version": "npm run build && git add -A dist",
24 | "push": "git push && git push --tags && npm publish"
25 | },
26 | "browser": {
27 | "crypto": "./lib/crypto.js",
28 | "net": "./lib/net.js",
29 | "tls": "./lib/tls.js"
30 | },
31 | "devDependencies": {
32 | "webpack": "^1.13.0"
33 | },
34 | "dependencies": {
35 | "nuid": "^0.6.8"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/update-lib.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | curl 'https://raw.githubusercontent.com/nats-io/node-nats/master/lib/nats.js' > /tmp/nats.js
3 | vimdiff lib/nats.js /tmp/nats.js
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | entry: './entry.js',
6 | output: {
7 | path: path.resolve(__dirname, './dist'),
8 | publicPath: '/dist/',
9 | filename: 'websocket-nats.js'
10 | }
11 | };
12 |
13 | if (process.env.NODE_ENV === 'dev') {
14 | module.exports.output.filename = 'websocket-nats.dev.js';
15 | module.exports.devtool = '#eval-source-map';
16 | }
17 |
18 | if (process.env.NODE_ENV === 'prod') {
19 | module.exports.output.filename = 'websocket-nats.min.js';
20 | module.exports.devtool = '#source-map';
21 | module.exports.plugins = (module.exports.plugins || []).concat([
22 | new webpack.optimize.UglifyJsPlugin({minimize: true})
23 | ]);
24 | }
25 |
--------------------------------------------------------------------------------