UNPKG

60.6 kBJavaScriptView Raw
1/* eslint-disable no-console */
2(function() {
3 // begin enclosed
4
5 var browser = false;
6 var Logger;
7 var crypto;
8 var Primus;
9
10 var PROTOCOL = 'happn_{{protocol}}';
11 var HAPPN_VERSION = '{{version}}';
12 var STATUS;
13
14 if (typeof window !== 'undefined' && typeof document !== 'undefined') browser = true;
15
16 // allow require when module is defined (needed for NW.js)
17 if (typeof module !== 'undefined') module.exports = HappnClient;
18
19 if (!browser) {
20 Logger = require('happn-logger');
21 PROTOCOL = 'happn_' + require('../package.json').protocol; //we can access our package
22 HAPPN_VERSION = require('../package.json').version; //we can access our package
23 Primus = require('happn-primus-wrapper');
24 } else {
25 window.HappnClient = HappnClient;
26 Primus = window.Primus;
27 // Object.assign polyfill for IE11 (from mozilla)
28 if (typeof Object.assign !== 'function') {
29 Object.defineProperty(Object, 'assign', {
30 value: function assign(target) {
31 'use strict';
32 if (target === null || target === undefined) {
33 throw new TypeError('Cannot convert undefined or null to object');
34 }
35 var to = Object(target);
36 for (var index = 1; index < arguments.length; index++) {
37 var nextSource = arguments[index];
38
39 if (nextSource !== null && nextSource !== undefined) {
40 for (var nextKey in nextSource) {
41 // Avoid bugs when hasOwnProperty is shadowed
42 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
43 to[nextKey] = nextSource[nextKey];
44 }
45 }
46 }
47 }
48 return to;
49 },
50 writable: true,
51 configurable: true
52 });
53 }
54 }
55
56 var promisify = function(fn) {
57 // MIT License, - thanks to paulmillr.com
58 if (typeof fn !== 'function') throw new TypeError('micro-promisify must receive a function');
59 return Object.defineProperties(
60 function() {
61 var _this = this;
62 for (var args = new Array(arguments.length), i = 0; i < args.length; ++i)
63 args[i] = arguments[i]; // git.io/vk55A
64 return new Promise(function(resolve, reject) {
65 args.push(function(error, result) {
66 error == null ? resolve(result) : reject(error);
67 });
68 fn.apply(_this, args);
69 });
70 },
71 {
72 length: { value: Math.max(0, fn.length - 1) },
73 name: { value: fn.name }
74 }
75 );
76 };
77
78 var maybePromisify = function(originalFunction, opts) {
79 return function() {
80 var args = Array.prototype.slice.call(arguments);
81 var _this = this;
82
83 if (opts && opts.unshift) args.unshift(opts.unshift);
84 if (args[args.length - 1] == null) args.splice(args.length - 1);
85
86 // No promisify if last passed arg is function (ie callback)
87
88 if (typeof args[args.length - 1] === 'function') {
89 return originalFunction.apply(this, args);
90 }
91
92 return new Promise(function(resolve, reject) {
93 // push false callback into arguments
94 args.push(function(error, result, more) {
95 if (error) return reject(error);
96 if (more) {
97 var args = Array.prototype.slice.call(arguments);
98 args.shift(); // toss undefined error
99 return resolve(args); // resolve array of args passed to callback
100 }
101 return resolve(result);
102 });
103 try {
104 return originalFunction.apply(_this, args);
105 } catch (error) {
106 return reject(error);
107 }
108 });
109 };
110 };
111
112 function HappnClient() {
113 if (!browser) {
114 this.CONSTANTS = require('.').constants;
115 this.utils = require('./services/utils/shared');
116 }
117
118 //DO NOT DELETE
119 //{{constants}}
120
121 //DO NOT DELETE
122 //{{utils}}
123
124 STATUS = this.CONSTANTS.CLIENT_STATE;
125 }
126
127 HappnClient.__instance = function(options) {
128 return new HappnClient().client(options);
129 };
130
131 HappnClient.create = maybePromisify(function(connection, options, callback) {
132 if (typeof connection === 'function') {
133 callback = connection;
134 options = {};
135 connection = null;
136 }
137
138 if (typeof options === 'function') {
139 callback = options;
140 options = connection ? connection : {};
141 connection = null;
142 }
143
144 if (!options) options = connection;
145
146 var client = new HappnClient().client(options);
147
148 if (options.testMode) HappnClient.lastClient = client;
149
150 return client.initialize(function(err, createdClient) {
151 if (!err) return callback(null, createdClient);
152
153 if (client.state.clientType !== 'eventemitter')
154 return client.disconnect(function() {
155 callback(err);
156 });
157
158 client.socket.disconnect();
159 callback(err);
160 });
161 });
162
163 HappnClient.prototype.client = function(options) {
164 options = options || {};
165
166 if (options.Logger && options.Logger.createLogger) {
167 this.log = options.Logger.createLogger('HappnClient');
168 } else if (Logger) {
169 if (!Logger.configured) Logger.configure(options.utils);
170
171 this.log = Logger.createLogger('HappnClient');
172 } else {
173 this.log = {
174 $$TRACE: function(msg, obj) {
175 if (obj) return console.info('HappnClient', msg, obj);
176 console.info('HappnClient', msg);
177 },
178 $$DEBUG: function(msg, obj) {
179 if (obj) return console.info('HappnClient', msg, obj);
180 console.info('HappnClient', msg);
181 },
182 trace: function(msg, obj) {
183 if (obj) return console.info('HappnClient', msg, obj);
184 console.info('HappnClient', msg);
185 },
186 debug: function(msg, obj) {
187 if (obj) return console.info('HappnClient', msg, obj);
188 console.info('HappnClient', msg);
189 },
190 info: function(msg, obj) {
191 if (obj) return console.info('HappnClient', msg, obj);
192 console.info('HappnClient', msg);
193 },
194 warn: function(msg, obj) {
195 if (obj) return console.warn('HappnClient', msg, obj);
196 console.info('HappnClient', msg);
197 },
198 error: function(msg, obj) {
199 if (obj) return console.error('HappnClient', msg, obj);
200 console.info('HappnClient', msg);
201 },
202 fatal: function(msg, obj) {
203 if (obj) return console.error('HappnClient', msg, obj);
204 console.info('HappnClient', msg);
205 }
206 };
207 }
208
209 this.log.$$TRACE('new client()');
210 this.__initializeState(); //local properties
211 this.__prepareInstanceOptions(options);
212 this.__initializeEvents(); //client events (connect/disconnect etc.)
213
214 return this;
215 };
216
217 HappnClient.prototype.initialize = maybePromisify(function(callback) {
218 var _this = this;
219
220 //ensure session scope is not on the prototype
221 _this.session = null;
222
223 if (browser) {
224 return _this.getResources(function(e) {
225 if (e) return callback(e);
226 _this.authenticate(function(e) {
227 if (e) return callback(e);
228 _this.status = STATUS.ACTIVE;
229 callback(null, _this);
230 });
231 });
232 }
233
234 _this.authenticate(function(e) {
235 if (e) return callback(e);
236 _this.status = STATUS.ACTIVE;
237 callback(null, _this);
238 });
239 });
240
241 HappnClient.prototype.__prepareSecurityOptions = function(options) {
242 if (options.keyPair && options.keyPair.publicKey) options.publicKey = options.keyPair.publicKey;
243
244 if (options.keyPair && options.keyPair.privateKey)
245 options.privateKey = options.keyPair.privateKey;
246 };
247
248 HappnClient.prototype.__prepareSocketOptions = function(options) {
249 //backward compatibility config options
250 if (!options.socket) options.socket = {};
251 if (!options.socket.reconnect) options.socket.reconnect = {};
252 if (options.reconnect) options.socket.reconnect = options.reconnect; //override, above config is very convoluted
253 if (!options.socket.reconnect.retries) options.socket.reconnect.retries = Infinity;
254 if (!options.socket.reconnect.max) options.socket.reconnect.max = 180e3; //3 minutes
255
256 if (options.connectTimeout != null) {
257 options.socket.timeout = options.connectTimeout;
258 } else {
259 options.socket.timeout = options.socket.timeout != null ? options.socket.timeout : 30e3; //30 seconds
260 }
261
262 options.socket.pingTimeout =
263 'pingTimeout' in options.socket ? options.socket.pingTimeout : 45e3; //45 second default
264
265 options.socket.strategy =
266 options.socket.reconnect.strategy || options.socket.strategy || 'disconnect,online';
267 };
268
269 HappnClient.prototype.__prepareConnectionOptions = function(options, defaults) {
270 var setDefaults = function(propertyName) {
271 if (!options[propertyName] && defaults[propertyName] != null)
272 options[propertyName] = defaults[propertyName];
273 };
274
275 if (defaults) {
276 setDefaults('host');
277 setDefaults('port');
278 setDefaults('url');
279 setDefaults('protocol');
280 setDefaults('allowSelfSignedCerts');
281 setDefaults('username');
282 setDefaults('password');
283 setDefaults('publicKey');
284 setDefaults('privateKey');
285 setDefaults('token');
286 }
287
288 if (!options.host) options.host = '127.0.0.1';
289
290 if (!options.port) options.port = 55000;
291
292 if (!options.url) {
293 options.protocol = options.protocol || 'http';
294
295 if (options.protocol === 'http' && parseInt(options.port) === 80) {
296 options.url = options.protocol + '://' + options.host;
297 } else if (options.protocol === 'https' && parseInt(options.port) === 443) {
298 options.url = options.protocol + '://' + options.host;
299 } else {
300 options.url = options.protocol + '://' + options.host + ':' + options.port;
301 }
302 }
303
304 return options;
305 };
306
307 HappnClient.prototype.__prepareInstanceOptions = function(options) {
308 var preparedOptions;
309
310 if (options.config) {
311 //we are going to standardise here, so no more config.config
312 preparedOptions = options.config;
313
314 for (var optionProperty in options) {
315 if (optionProperty !== 'config' && !preparedOptions[optionProperty])
316 preparedOptions[optionProperty] = options[optionProperty];
317 }
318 } else preparedOptions = options;
319
320 if (!preparedOptions.callTimeout) preparedOptions.callTimeout = 60000; //1 minute
321
322 //this is for local client connections
323 if (preparedOptions.context)
324 Object.defineProperty(this, 'context', {
325 value: preparedOptions.context
326 });
327
328 //how we override methods
329 if (preparedOptions.plugin) {
330 for (var overrideName in preparedOptions.plugin) {
331 // eslint-disable-next-line no-prototype-builtins
332 if (preparedOptions.plugin.hasOwnProperty(overrideName)) {
333 if (preparedOptions.plugin[overrideName].bind)
334 this[overrideName] = preparedOptions.plugin[overrideName].bind(this);
335 else this[overrideName] = preparedOptions.plugin[overrideName];
336 }
337 }
338 }
339
340 preparedOptions = this.__prepareConnectionOptions(preparedOptions);
341
342 this.__prepareSecurityOptions(preparedOptions);
343
344 if (preparedOptions.allowSelfSignedCerts) process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
345
346 this.__prepareSocketOptions(preparedOptions);
347
348 var info = preparedOptions.info != null ? preparedOptions.info : {};
349
350 if (typeof info !== 'object')
351 info = {
352 data: info
353 };
354
355 preparedOptions.info = info;
356 preparedOptions.info._browser = preparedOptions.info._browser || browser;
357
358 if (preparedOptions.loginRetry == null) preparedOptions.loginRetry = 4; // will attempt to login to the same address 4 times
359 if (preparedOptions.loginRetryInterval == null) preparedOptions.loginRetryInterval = 5000; // five seconds apart
360
361 if (preparedOptions.loginTimeout == null)
362 preparedOptions.loginTimeout = preparedOptions.callTimeout; // will wait a minute before failing the login
363
364 if (preparedOptions.defaultVariableDepth == null) preparedOptions.defaultVariableDepth = 5;
365
366 this.options = preparedOptions;
367 };
368
369 HappnClient.prototype.__updateOptions = function(possibility) {
370 var _this = this;
371
372 var syncOption = function(propertyName) {
373 if (possibility[propertyName] != null)
374 _this.options[propertyName] = possibility[propertyName];
375 };
376
377 syncOption('url');
378 syncOption('host');
379 syncOption('port');
380 syncOption('protocol');
381 syncOption('allowSelfSignedCerts');
382 syncOption('username');
383 syncOption('password');
384 syncOption('publicKey');
385 syncOption('privateKey');
386 syncOption('token');
387 };
388
389 HappnClient.prototype.__getConnection = function(callback) {
390 var _this = this;
391 this.__connectionCleanup(function(e) {
392 if (e) return callback(e);
393 _this.options.socket.manual = true; //because we want to explicitly call open()
394 _this.__connectSocket(callback);
395 });
396 };
397
398 HappnClient.prototype.__connectSocket = function(callback) {
399 var socket;
400
401 var _this = this;
402
403 _this.status = STATUS.CONNECTING;
404
405 if (browser) socket = new Primus(_this.options.url, _this.options.socket);
406 else {
407 var Socket = Primus.createSocket({
408 transformer: _this.options.transformer,
409 parser: _this.options.parser,
410 manual: true
411 });
412
413 socket = new Socket(_this.options.url, _this.options.socket);
414 }
415
416 socket.on('timeout', function() {
417 if (_this.status === STATUS.CONNECTING) {
418 _this.status = STATUS.CONNECT_ERROR;
419 return callback(new Error('connection timed out'));
420 }
421 _this.handle_error(new Error('connection timed out'));
422 });
423
424 socket.on('open', function waitForConnection() {
425 if (_this.status === STATUS.CONNECTING) {
426 _this.status = STATUS.ACTIVE;
427 _this.serverDisconnected = false;
428 socket.removeListener('open', waitForConnection);
429 callback(null, socket);
430 }
431 });
432
433 socket.on('error', function(e) {
434 if (_this.status === STATUS.CONNECTING) {
435 // ERROR before connected,
436 // ECONNREFUSED etc. out as errors on callback
437 _this.status = STATUS.CONNECT_ERROR;
438 _this.__endAndDestroySocket(socket, function(destroyErr) {
439 if (destroyErr)
440 _this.log.warn(
441 'socket.end failed in client connection failure: ' + destroyErr.toString()
442 );
443 callback(e.error || e);
444 });
445 }
446 _this.handle_error(e.error || e);
447 });
448
449 socket.open();
450 };
451
452 HappnClient.prototype.__initializeState = function() {
453 this.state = {};
454
455 this.state.events = {};
456 this.state.refCount = {};
457 this.state.listenerRefs = {};
458 this.state.requestEvents = {};
459 this.state.currentEventId = 0;
460 this.state.currentListenerId = 0;
461 this.state.errors = [];
462 this.state.clientType = 'socket';
463 this.state.systemMessageHandlers = [];
464 this.status = STATUS.UNINITIALIZED;
465 this.state.ackHandlers = {};
466 this.state.eventHandlers = {};
467 };
468
469 HappnClient.prototype.__initializeEvents = function() {
470 var _this = this;
471
472 _this.onEvent = function(eventName, eventHandler) {
473 if (!eventName) throw new Error('event name cannot be blank or null');
474
475 if (typeof eventHandler !== 'function') throw new Error('event handler must be a function');
476
477 if (!_this.state.eventHandlers[eventName]) _this.state.eventHandlers[eventName] = [];
478
479 _this.state.eventHandlers[eventName].push(eventHandler);
480
481 return eventName + '|' + (_this.state.eventHandlers[eventName].length - 1);
482 };
483
484 _this.offEvent = function(handlerId) {
485 var eventName = handlerId.split('|')[0];
486
487 var eventIndex = parseInt(handlerId.split('|')[1]);
488
489 _this.state.eventHandlers[eventName][eventIndex] = null;
490 };
491
492 _this.emit = function(eventName, eventData) {
493 if (_this.state.eventHandlers[eventName]) {
494 _this.state.eventHandlers[eventName].forEach(function(handler) {
495 if (!handler) return;
496 handler.call(handler, eventData);
497 });
498 }
499 };
500 };
501
502 HappnClient.prototype.getScript = function(url, callback) {
503 if (!browser) return callback(new Error('only for browser'));
504
505 var script = document.createElement('script');
506 script.src = url;
507 var head = document.getElementsByTagName('head')[0];
508 var done = false;
509
510 // Attach handlers for all browsers
511 script.onload = script.onreadystatechange = function() {
512 if (
513 !done &&
514 (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')
515 ) {
516 done = true;
517 script.onload = script.onreadystatechange = null;
518 head.removeChild(script);
519 callback();
520 }
521 };
522
523 head.appendChild(script);
524 };
525
526 HappnClient.prototype.getResources = function(callback) {
527 if (typeof Primus !== 'undefined') return callback();
528
529 this.getScript(this.options.url + '/browser_primus.js', function(e) {
530 if (e) return callback(e);
531
532 if (typeof Primus === 'undefined') {
533 if (window && window.Primus) Primus = window.Primus;
534 else if (document && document.Primus) Primus = document.Primus;
535 else return callback(new Error('unable to fetch Primus library'));
536
537 callback();
538 }
539 });
540 };
541
542 HappnClient.prototype.stop = maybePromisify(function(callback) {
543 this.__connectionCleanup(callback);
544 });
545
546 HappnClient.prototype.__encryptLogin = function(parameters, publicKey) {
547 return {
548 encrypted: crypto.asymmetricEncrypt(
549 publicKey,
550 this.options.privateKey,
551 JSON.stringify(parameters)
552 ),
553 publicKey: parameters.publicKey,
554 loginType: parameters.loginType != null ? parameters.loginType : 'password'
555 };
556 };
557
558 HappnClient.prototype.__decryptLogin = function(loginResult) {
559 return JSON.parse(
560 crypto.asymmetricDecrypt(
561 this.serverInfo.publicKey,
562 this.options.privateKey,
563 loginResult.encrypted
564 )
565 );
566 };
567
568 HappnClient.prototype.__encryptPayload = function(message) {
569 var payload = crypto.symmetricEncryptObjectiv(
570 message,
571 this.session.secret,
572 this.utils.computeiv(this.session.secret)
573 );
574
575 return {
576 sessionId: message.sessionId,
577 eventId: message.eventId,
578 encrypted: payload
579 };
580 };
581
582 HappnClient.prototype.__decryptPayload = function(message) {
583 var self = this;
584
585 var payload = crypto.symmetricDecryptObjectiv(
586 message,
587 self.session.secret,
588 self.utils.computeiv(self.session.secret)
589 );
590
591 return payload;
592 };
593
594 HappnClient.prototype.__ensureCryptoLibrary = maybePromisify(function(callback) {
595 if (crypto) return callback();
596
597 if (browser) {
598 this.getScript(this.options.url + '/browser_crypto.js', function(e) {
599 if (e) return callback(e);
600 crypto = new window.Crypto();
601 callback();
602 });
603 } else {
604 Crypto = require('happn-util-crypto');
605 crypto = new Crypto();
606 callback();
607 }
608 });
609
610 HappnClient.prototype.__writeCookie = function(session, sessionDocument) {
611 sessionDocument.cookie = this.__getCookie(session);
612 };
613
614 HappnClient.prototype.__getCookie = function(session) {
615 var cookie = (session.cookieName || 'happn_token') + '=' + session.token + '; path=/;';
616 if (session.cookieDomain) cookie += ' domain=' + session.cookieDomain + ';';
617 if (this.options.protocol === 'https') cookie += ' Secure;';
618 return cookie;
619 };
620
621 HappnClient.prototype.__expireCookie = function(session, sessionDocument) {
622 session.token = '';
623 var cookie = this.__getCookie(session);
624 cookie += '; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
625 sessionDocument.cookie = cookie;
626 };
627
628 HappnClient.prototype.__attachSession = function(result) {
629 delete result._meta;
630 this.session = result;
631 if (browser) this.__writeCookie(result, document);
632 };
633
634 HappnClient.prototype.__payloadToError = function(payload) {
635 var err = new Error(payload.toString());
636 if (payload.message) err.message = payload.message;
637 return err;
638 };
639
640 HappnClient.prototype.__doLogin = function(loginParameters, callback) {
641 var _this = this;
642
643 var login = function(cb) {
644 _this.__performSystemRequest(
645 'login',
646 loginParameters,
647 {
648 timeout: _this.options.loginTimeout
649 },
650 function(e, result) {
651 if (e) return cb(e);
652 if (result._meta.status === 'ok') {
653 _this.__attachSession(result);
654 cb();
655 } else cb(_this.__payloadToError(result.payload));
656 }
657 );
658 };
659
660 if (!_this.options.loginRetry) return login(callback);
661
662 if (!_this.options.loginRetryInterval || typeof _this.options.loginRetryInterval !== 'number')
663 _this.options.loginRetryInterval = 5000; //just in case, someone made it 0 or -1 or blah
664
665 var currentAttempt = 0;
666
667 var loggedIn = false;
668
669 _this.utils.whilst(
670 function() {
671 return currentAttempt < _this.options.loginRetry && loggedIn === false;
672 },
673 function(attempt, next) {
674 currentAttempt++;
675
676 login(function(e) {
677 if (e) {
678 if ([403, 401].indexOf(e.code) > -1) return next(e); //access was denied
679 if (currentAttempt === _this.options.loginRetry) return next(e);
680 return setTimeout(next, _this.options.loginRetryInterval);
681 }
682
683 loggedIn = true;
684
685 return next();
686 });
687 },
688 callback
689 );
690 };
691
692 HappnClient.prototype.__signNonce = function(nonce) {
693 return crypto.sign(nonce, this.options.privateKey);
694 };
695
696 HappnClient.prototype.__prepareLogin = function(loginParameters, callback) {
697 var _this = this;
698
699 var prepareCallback = function(prepared) {
700 if (_this.serverInfo.encryptPayloads)
701 prepared = _this.__encryptLogin(prepared, _this.serverInfo.publicKey);
702 callback(null, prepared);
703 };
704
705 if (loginParameters.loginType === 'digest') {
706 _this.__performSystemRequest(
707 'request-nonce',
708 {
709 publicKey: loginParameters.publicKey
710 },
711 null,
712 function(e, response) {
713 if (e) return callback(e);
714
715 loginParameters.digest = _this.__signNonce(response.nonce);
716 prepareCallback(loginParameters);
717 }
718 );
719 } else prepareCallback(loginParameters);
720 };
721
722 HappnClient.prototype.__prepareAndDoLogin = function(loginParameters, callback) {
723 var _this = this;
724 _this.__prepareLogin(loginParameters, function(e, preparedParameters) {
725 if (e) return callback(e);
726 _this.__doLogin(preparedParameters, callback);
727 });
728 };
729
730 HappnClient.prototype.__sessionConfigureAndDescribe = function() {
731 var _this = this;
732 return new Promise(function(resolve, reject) {
733 _this.__performSystemRequest(
734 'configure-session',
735 {
736 protocol: PROTOCOL,
737 version: HAPPN_VERSION,
738 browser: browser
739 },
740 null,
741 function(e) {
742 if (e) return reject(e);
743 _this.__performSystemRequest('describe', null, null, function(e, serverInfo) {
744 if (e) return reject(e);
745 resolve(serverInfo);
746 });
747 }
748 );
749 });
750 };
751
752 function getCookie(name) {
753 var value = '; ' + document.cookie;
754 var parts = value.split('; ' + name + '=');
755 if (parts.length === 2)
756 return parts
757 .pop()
758 .split(';')
759 .shift();
760 }
761
762 HappnClient.prototype.login = maybePromisify(function(callback) {
763 var _this = this;
764
765 var loginParameters = {
766 username: this.options.username,
767 info: this.options.info,
768 protocol: PROTOCOL
769 };
770
771 loginParameters.info._browser = loginParameters.info._browser || browser;
772 loginParameters.info._local = _this.socket._local ? true : false;
773
774 if (this.options.password) loginParameters.password = this.options.password;
775 if (this.options.publicKey) loginParameters.publicKey = this.options.publicKey;
776 if (this.options.token) loginParameters.token = this.options.token;
777
778 if (PROTOCOL === 'happn_{{protocol}}') PROTOCOL = 'happn'; //if this file is being used without a replace on the protocol
779
780 _this
781 .__sessionConfigureAndDescribe()
782 .then(function(serverInfo) {
783 _this.serverInfo = serverInfo;
784 if (!_this.serverInfo.secure) return _this.__doLogin(loginParameters, callback);
785
786 if (_this.options.useCookie) {
787 if (browser) {
788 loginParameters.token = getCookie(serverInfo.cookieName);
789 } else {
790 return callback(new Error('Logging in with cookie only valid in browser'));
791 }
792 }
793
794 if (!loginParameters.token && !loginParameters.username)
795 return callback(new Error('happn server is secure, please specify a username or token'));
796
797 if (!loginParameters.password && !loginParameters.token) {
798 if (!loginParameters.publicKey)
799 return callback(new Error('happn server is secure, please specify a password'));
800 loginParameters.loginType = 'digest';
801 }
802
803 if (!_this.serverInfo.encryptPayloads && loginParameters.loginType !== 'digest')
804 return _this.__doLogin(loginParameters, callback);
805
806 _this.__ensureCryptoLibrary(function(e) {
807 if (e) return callback(e);
808
809 if (!_this.options.privateKey || !_this.options.publicKey) {
810 if (loginParameters.loginType === 'digest')
811 return callback(
812 new Error('login type is digest, but no privateKey and publicKey specified')
813 );
814
815 //We generate one
816 var keyPair = crypto.createKeyPair();
817
818 _this.options.publicKey = keyPair.publicKey;
819 _this.options.privateKey = keyPair.privateKey;
820 }
821
822 loginParameters.publicKey = _this.options.publicKey;
823 _this.__prepareAndDoLogin(loginParameters, callback);
824 });
825 })
826 .catch(function(e) {
827 _this.emit('connect-error', e);
828 callback(e);
829 });
830 });
831
832 HappnClient.prototype.authenticate = maybePromisify(function(callback) {
833 var _this = this;
834 if (_this.socket) {
835 // handle_reconnection also call through here to 're-authenticate'.
836 // this is that happening. Don't make new socket.
837 // if the login fails, a setTimeout 3000 and re-authenticate, retry happens
838 // see this.__retryReconnect
839 _this.login(callback);
840 return;
841 }
842
843 _this.__getConnection(function(e, socket) {
844 if (e) return callback(e);
845
846 _this.socket = socket;
847
848 _this.socket.on('data', _this.handle_publication.bind(_this));
849 _this.socket.on('reconnected', _this.reconnect.bind(_this));
850 _this.socket.on('end', _this.handle_end.bind(_this));
851 _this.socket.on('close', _this.handle_end.bind(_this));
852 _this.socket.on('reconnect timeout', _this.handle_reconnect_timeout.bind(_this));
853 _this.socket.on('reconnect scheduled', _this.handle_reconnect_scheduled.bind(_this));
854
855 // login is called before socket connection established...
856 // seems ok (streams must be paused till open)
857 _this.login(callback);
858 });
859 });
860
861 HappnClient.prototype.handle_end = function() {
862 this.status = STATUS.DISCONNECTED;
863 if (this.session) return this.emit('connection-ended', this.session.id);
864 this.emit('connection-ended');
865 };
866
867 HappnClient.prototype.handle_reconnect_timeout = function(err, opts) {
868 this.status = STATUS.DISCONNECTED;
869
870 this.emit('reconnect-timeout', {
871 err: err,
872 opts: opts
873 });
874 };
875
876 HappnClient.prototype.handle_reconnect_scheduled = function(opts) {
877 this.status = STATUS.RECONNECTING;
878 this.emit('reconnect-scheduled', opts);
879 };
880
881 HappnClient.prototype.getEventId = function() {
882 return (this.state.currentEventId += 1);
883 };
884
885 HappnClient.prototype.__requestCallback = function(
886 message,
887 callback,
888 options,
889 eventId,
890 path,
891 action
892 ) {
893 var _this = this;
894
895 var callbackHandler = {
896 eventId: message.eventId
897 };
898
899 callbackHandler.handleResponse = function(e, response) {
900 clearTimeout(callbackHandler.timedout);
901 delete _this.state.requestEvents[callbackHandler.eventId];
902 return callback(e, response);
903 };
904
905 callbackHandler.timedout = setTimeout(function() {
906 delete _this.state.requestEvents[callbackHandler.eventId];
907 var errorMessage = 'api request timed out';
908 if (path) errorMessage += ' path: ' + path;
909 if (action) errorMessage += ' action: ' + action;
910 return callback(new Error(errorMessage));
911 }, options.timeout);
912
913 //we add our event handler to a queue, with the embedded timeout
914 _this.state.requestEvents[eventId] = callbackHandler;
915 };
916
917 HappnClient.prototype.__asyncErrorCallback = function(error, callback) {
918 if (!callback) {
919 throw error;
920 }
921 setTimeout(function() {
922 callback(error);
923 }, 0);
924 };
925
926 HappnClient.prototype.__performDataRequest = function(path, action, data, options, callback) {
927 if (this.status !== STATUS.ACTIVE) {
928 var errorMessage = 'client not active';
929
930 if (this.status === STATUS.CONNECT_ERROR) errorMessage = 'client in an error state';
931 if (this.status === STATUS.UNINITIALIZED) errorMessage = 'client not initialized yet';
932 if (this.status === STATUS.DISCONNECTED) errorMessage = 'client is disconnected';
933
934 var errorDetail = 'action: ' + action + ', path: ' + path;
935
936 var error = new Error(errorMessage);
937 error.detail = errorDetail;
938
939 return this.__asyncErrorCallback(error, callback);
940 }
941
942 var message = {
943 action: action,
944 eventId: this.getEventId(),
945 path: path,
946 data: data,
947 sessionId: this.session.id
948 };
949
950 if (!options) options = {};
951 else message.options = options; //else skip sending up the options
952
953 if (['set', 'remove'].indexOf(action) >= 0) {
954 if (
955 options.consistency === this.CONSTANTS.CONSISTENCY.DEFERRED ||
956 options.consistency === this.CONSTANTS.CONSISTENCY.ACKNOWLEDGED
957 )
958 this.__attachPublishedAck(options, message);
959 }
960
961 if (!options.timeout) options.timeout = this.options.callTimeout;
962 if (this.serverInfo.encryptPayloads) message = this.__encryptPayload(message);
963 if (callback) this.__requestCallback(message, callback, options, message.eventId, path, action); // if null we are firing and forgetting
964
965 this.socket.write(message);
966 };
967
968 HappnClient.prototype.__performSystemRequest = function(action, data, options, callback) {
969 var message = {
970 action: action,
971 eventId: this.getEventId()
972 };
973
974 if (data !== undefined) message.data = data;
975
976 if (this.session) message.sessionId = this.session.id;
977
978 if (!options) options = {};
979 //skip sending up the options
980 else message.options = options;
981
982 if (!options.timeout) options.timeout = this.options.callTimeout; //this is not used on the server side
983
984 this.__requestCallback(message, callback, options, message.eventId); // if null we are firing and forgetting
985
986 this.socket.write(message);
987 };
988
989 HappnClient.prototype.getChannel = function(path, action) {
990 this.utils.checkPath(path);
991 return '/' + action.toUpperCase() + '@' + path;
992 };
993
994 HappnClient.prototype.get = maybePromisify(function(path, parameters, handler) {
995 if (typeof parameters === 'function') {
996 handler = parameters;
997 parameters = {};
998 }
999 this.__performDataRequest(path, 'get', null, parameters, handler);
1000 });
1001
1002 HappnClient.prototype.count = maybePromisify(function(path, parameters, handler) {
1003 if (typeof parameters === 'function') {
1004 handler = parameters;
1005 parameters = {};
1006 }
1007 this.__performDataRequest(path, 'count', null, parameters, handler);
1008 });
1009
1010 HappnClient.prototype.getPaths = maybePromisify(function(path, opts, handler) {
1011 if (typeof opts === 'function') {
1012 handler = opts;
1013 opts = {};
1014 }
1015
1016 opts.options = {
1017 path_only: true
1018 };
1019
1020 this.get(path, opts, handler);
1021 });
1022
1023 HappnClient.prototype.increment = maybePromisify(function(path, gauge, increment, opts, handler) {
1024 if (typeof opts === 'function') {
1025 handler = opts;
1026 opts = {};
1027 }
1028
1029 if (typeof increment === 'function') {
1030 handler = increment;
1031 increment = gauge;
1032 gauge = 'counter';
1033 opts = {};
1034 }
1035
1036 if (typeof gauge === 'function') {
1037 handler = gauge;
1038 increment = 1;
1039 gauge = 'counter';
1040 opts = {};
1041 }
1042
1043 if (isNaN(increment)) return handler(new Error('increment must be a number'));
1044
1045 opts.increment = increment;
1046 this.set(path, gauge, opts, handler);
1047 });
1048
1049 HappnClient.prototype.publish = maybePromisify(function(path, data, options, handler) {
1050 if (typeof options === 'function') {
1051 handler = options;
1052 options = {};
1053 }
1054
1055 if (data === null) options.nullValue = true; //carry across the wire
1056
1057 options.noStore = true;
1058 options.noDataResponse = true;
1059
1060 try {
1061 //in a try/catch to catch checkPath failure
1062 this.utils.checkPath(path, 'set');
1063 this.__performDataRequest(path, 'set', data, options, handler);
1064 } catch (e) {
1065 return handler(e);
1066 }
1067 });
1068
1069 HappnClient.prototype.set = maybePromisify(function(path, data, options, handler) {
1070 if (typeof options === 'function') {
1071 handler = options;
1072 options = {};
1073 }
1074
1075 if (data === null) options.nullValue = true; //carry across the wire
1076
1077 try {
1078 //in a try/catch to catch checkPath failure
1079 this.utils.checkPath(path, 'set');
1080 this.__performDataRequest(path, 'set', data, options, handler);
1081 } catch (e) {
1082 return handler(e);
1083 }
1084 });
1085
1086 HappnClient.prototype.setSibling = maybePromisify(function(path, data, opts, handler) {
1087 if (typeof opts === 'function') {
1088 handler = opts;
1089 opts = {};
1090 }
1091
1092 opts.set_type = 'sibling';
1093 this.set(path, data, opts, handler);
1094 });
1095
1096 HappnClient.prototype.remove = maybePromisify(function(path, parameters, handler) {
1097 if (typeof parameters === 'function') {
1098 handler = parameters;
1099 parameters = {};
1100 }
1101
1102 return this.__performDataRequest(path, 'remove', null, parameters, handler);
1103 });
1104
1105 HappnClient.prototype.__updateListenerRef = function(listener, remoteRef) {
1106 if (listener.initialEmit || listener.initialCallback)
1107 this.state.listenerRefs[listener.id] = remoteRef;
1108 else this.state.listenerRefs[listener.eventKey] = remoteRef;
1109 };
1110
1111 HappnClient.prototype.__clearListenerRef = function(listener) {
1112 if (listener.initialEmit || listener.initialCallback)
1113 return delete this.state.listenerRefs[listener.id];
1114 delete this.state.listenerRefs[listener.eventKey];
1115 };
1116
1117 HappnClient.prototype.__getListenerRef = function(listener) {
1118 if (listener.initialEmit || listener.initialCallback)
1119 return this.state.listenerRefs[listener.id];
1120 return this.state.listenerRefs[listener.eventKey];
1121 };
1122
1123 HappnClient.prototype.__reattachChangedListeners = function(permissions) {
1124 if (!permissions || Object.keys(permissions).length === 0) return;
1125 let listenerPermPaths = Object.keys(permissions).filter(
1126 key => permissions[key].actions.includes('*') || permissions[key].actions.includes('on')
1127 );
1128 this.state.refCount = this.state.refCount || {};
1129 let errors = [];
1130 if (listenerPermPaths) {
1131 listenerPermPaths.forEach(path => {
1132 let listeners = this.state.events[path];
1133 if (listeners) {
1134 listeners.forEach(listener => {
1135 this.state.refCount[listener.eventKey] = 0;
1136 if (!this.state.events[path]) return; // We have deleted this key.
1137 if (this.state.refCount[listener.eventKey]) {
1138 //we are already listening on this key
1139 this.state.refCount[listener.eventKey]++;
1140 return;
1141 }
1142 let parameters = {};
1143 if (listener.meta) parameters.meta = listener.meta;
1144 this._offPath(path, e => {
1145 if (e)
1146 errors.push(
1147 new Error('failed detaching listener to path, on re-establishment: ' + path, e)
1148 );
1149
1150 this._remoteOn(path, parameters, (e, response) => {
1151 if (e) {
1152 if ([403, 401].indexOf(e.code) > -1) {
1153 //permissions may have changed regarding this path
1154 delete this.state.events[path];
1155 return;
1156 }
1157 errors.push(new Error('failed re-establishing listener to path: ' + path, e));
1158 return;
1159 }
1160 this.state.refCount[listener.eventKey] = 1;
1161 this.__updateListenerRef(listener, response.id);
1162 });
1163 });
1164 });
1165 } else {
1166 this._remoteOn(path, {}, (e, response) => {
1167 if (e) {
1168 if ([403, 401].indexOf(e.code) > -1) {
1169 //permissions may have changed regarding this path
1170 delete this.state.events[path];
1171 this.__clearSecurityDirectorySubscriptions(path);
1172 }
1173 } else {
1174 this._remoteOff(path, response.id, () => {});
1175 }
1176 });
1177 }
1178 });
1179 }
1180 };
1181
1182 HappnClient.prototype.__reattachListeners = function(callback) {
1183 var _this = this;
1184
1185 _this.utils.async(
1186 Object.keys(_this.state.events),
1187 function(eventPath, index, nextEvent) {
1188 var listeners = _this.state.events[eventPath];
1189 _this.state.refCount = {};
1190
1191 // re-establish each listener individually to preserve original meta and listener id
1192 _this.utils.async(
1193 listeners,
1194 function(listener, index, nextListener) {
1195 if (_this.state.refCount[listener.eventKey]) {
1196 //we are already listening on this key
1197 _this.state.refCount[listener.eventKey]++;
1198 return nextListener();
1199 }
1200
1201 // we don't pass any additional parameters like initialValueEmit and initialValueCallback
1202 var parameters = {};
1203
1204 if (listener.meta) parameters.meta = listener.meta;
1205
1206 _this._offPath(eventPath, function(e) {
1207 if (e)
1208 return nextListener(
1209 new Error(
1210 'failed detaching listener to path, on re-establishment: ' + eventPath,
1211 e
1212 )
1213 );
1214
1215 _this._remoteOn(eventPath, parameters, function(e, response) {
1216 if (e) {
1217 if ([403, 401].indexOf(e.code) > -1) {
1218 //permissions may have changed regarding this path
1219 delete _this.state.events[eventPath];
1220 return nextListener();
1221 }
1222 return nextListener(
1223 new Error('failed re-establishing listener to path: ' + eventPath, e)
1224 );
1225 }
1226
1227 //update our ref count so we dont subscribe again
1228 _this.state.refCount[listener.eventKey] = 1;
1229 //create our mapping between the listener id and
1230 _this.__updateListenerRef(listener, response.id);
1231
1232 nextListener();
1233 });
1234 });
1235 },
1236 nextEvent
1237 );
1238 },
1239 callback
1240 );
1241 };
1242
1243 HappnClient.prototype.__retryReconnect = function(options, reason, e) {
1244 var _this = this;
1245 _this.emit('reconnect-error', {
1246 reason: reason,
1247 error: e
1248 });
1249 if (_this.status === STATUS.DISCONNECTED) {
1250 clearTimeout(_this.__retryReconnectTimeout);
1251 return;
1252 }
1253 _this.__retryReconnectTimeout = setTimeout(function() {
1254 _this.reconnect.call(_this, options);
1255 }, 3000);
1256 };
1257
1258 HappnClient.prototype.reconnect = function(options) {
1259 var _this = this;
1260
1261 _this.status = STATUS.RECONNECT_ACTIVE;
1262 _this.emit('reconnect', options);
1263 _this.authenticate(function(e) {
1264 if (e) {
1265 _this.handle_error(e);
1266 return _this.__retryReconnect(options, 'authentication-failed', e);
1267 }
1268 //we are authenticated and ready for data requests
1269 _this.status = STATUS.ACTIVE;
1270 _this.__reattachListeners(function(e) {
1271 if (e) {
1272 _this.handle_error(e);
1273 return _this.__retryReconnect(options, 'reattach-listeners-failed', e);
1274 }
1275 _this.emit('reconnect-successful', options);
1276 });
1277 });
1278 };
1279
1280 HappnClient.prototype.handle_error = function(err) {
1281 var errLog = {
1282 timestamp: Date.now(),
1283 error: err
1284 };
1285
1286 if (this.state.errors.length === 100) this.state.errors.shift();
1287 this.state.errors.push(errLog);
1288
1289 this.emit('error', err);
1290 this.log.error('unhandled error', err);
1291 };
1292
1293 HappnClient.prototype.__attachPublishedAck = function(options, message) {
1294 var _this = this;
1295
1296 if (typeof options.onPublished !== 'function')
1297 throw new Error('onPublished handler in options is missing');
1298
1299 var publishedTimeout = options.onPublishedTimeout || 60000; //default is one minute
1300
1301 var ackHandler = {
1302 id: message.sessionId + '-' + message.eventId,
1303 onPublished: options.onPublished,
1304
1305 handle: function(e, results) {
1306 clearTimeout(ackHandler.timeout);
1307 delete _this.state.ackHandlers[ackHandler.id];
1308 ackHandler.onPublished(e, results);
1309 },
1310 timedout: function() {
1311 ackHandler.handle(new Error('publish timed out'));
1312 }
1313 };
1314
1315 ackHandler.timeout = setTimeout(ackHandler.timedout, publishedTimeout);
1316 _this.state.ackHandlers[ackHandler.id] = ackHandler;
1317 };
1318
1319 HappnClient.prototype.handle_ack = function(message) {
1320 if (this.state.ackHandlers[message.id]) {
1321 if (message.status === 'error')
1322 return this.state.ackHandlers[message.id].handle(new Error(message.error), message.result);
1323 this.state.ackHandlers[message.id].handle(null, message.result);
1324 }
1325 };
1326
1327 HappnClient.prototype.handle_publication = function(message) {
1328 if (message.encrypted) {
1329 if (message._meta && message._meta.type === 'login') message = this.__decryptLogin(message);
1330 else message = this.__decryptPayload(message.encrypted);
1331 }
1332
1333 if (message._meta && message._meta.type === 'data')
1334 return this.handle_data(message._meta.channel, message);
1335
1336 if (message._meta && message._meta.type === 'system')
1337 return this.__handleSystemMessage(message);
1338
1339 if (message._meta && message._meta.type === 'ack') return this.handle_ack(message);
1340
1341 if (Array.isArray(message)) return this.handle_response_array(null, message, message.pop());
1342
1343 if (message._meta.status === 'error') {
1344 var error = message._meta.error;
1345 var e = new Error();
1346 e.name = error.name || error.message || error;
1347 Object.keys(error).forEach(function(key) {
1348 if (!e[key]) e[key] = error[key];
1349 });
1350 return this.handle_response(e, message);
1351 }
1352
1353 var decoded;
1354 if (message.data) {
1355 var meta = message._meta;
1356 if (Array.isArray(message.data)) decoded = message.data.slice();
1357 else decoded = Object.assign({}, message.data);
1358 decoded._meta = meta;
1359 } else decoded = message;
1360
1361 if (message.data === null) decoded._meta.nullData = true;
1362 this.handle_response(null, decoded);
1363 };
1364
1365 HappnClient.prototype.handle_response_array = function(e, response, meta) {
1366 var responseHandler = this.state.requestEvents[meta.eventId];
1367 if (responseHandler) responseHandler.handleResponse(e, response);
1368 };
1369
1370 HappnClient.prototype.handle_response = function(e, response) {
1371 var responseHandler = this.state.requestEvents[response._meta.eventId];
1372 if (responseHandler) {
1373 if (response._meta.nullData) return responseHandler.handleResponse(e, null);
1374 responseHandler.handleResponse(e, response);
1375 }
1376 };
1377
1378 HappnClient.prototype.__acknowledge = function(message, callback) {
1379 if (message._meta.consistency !== this.CONSTANTS.CONSISTENCY.ACKNOWLEDGED)
1380 return callback(message);
1381
1382 this.__performDataRequest(message.path, 'ack', message._meta.publicationId, null, function(e) {
1383 if (e) {
1384 message._meta.acknowledged = false;
1385 message._meta.acknowledgedError = e;
1386 } else message._meta.acknowledged = true;
1387
1388 callback(message);
1389 });
1390 };
1391
1392 HappnClient.prototype.delegate_handover = function(data, meta, delegate) {
1393 if (delegate.variableDepth && delegate.depth < meta.depth) return;
1394 delegate.runcount++;
1395 if (delegate.count > 0 && delegate.runcount > delegate.count) return;
1396 //swallow any successive publicatiions
1397 if (delegate.count === delegate.runcount) {
1398 var _this = this;
1399 return _this._offListener(delegate.id, function(e) {
1400 if (e) return _this.handle_error(e);
1401 delegate.handler.call(_this, JSON.parse(data), meta);
1402 });
1403 }
1404
1405 delegate.handler.call(this, JSON.parse(data), meta);
1406 };
1407
1408 HappnClient.prototype.handle_data = function(path, message) {
1409 var _this = this;
1410
1411 _this.__acknowledge(message, function(acknowledged) {
1412 if (!_this.state.events[path]) return;
1413
1414 if (acknowledged._meta.acknowledgedError)
1415 _this.log.error(
1416 'acknowledgement failure: ',
1417 acknowledged._meta.acknowledgedError.toString(),
1418 acknowledged._meta.acknowledgedError
1419 );
1420
1421 var intermediateData = JSON.stringify(acknowledged.data);
1422
1423 function doHandover(delegate) {
1424 _this.delegate_handover(intermediateData, acknowledged._meta, delegate);
1425 }
1426 //slice necessary so order is not messed with for count based subscriptions
1427 _this.state.events[path].slice().forEach(doHandover);
1428 });
1429 };
1430
1431 HappnClient.prototype.__clearSubscriptionsOnPath = function(eventListenerPath) {
1432 var _this = this;
1433
1434 var listeners = this.state.events[eventListenerPath];
1435 listeners.forEach(function(listener) {
1436 _this.__clearListenerState(eventListenerPath, listener);
1437 });
1438 };
1439
1440 HappnClient.prototype.__clearSecurityDirectorySubscriptions = function(path) {
1441 var _this = this;
1442
1443 Object.keys(this.state.events).forEach(function(eventListenerPath) {
1444 if (path === eventListenerPath.substring(eventListenerPath.indexOf('@') + 1)) {
1445 _this.__clearSubscriptionsOnPath(eventListenerPath);
1446 }
1447 });
1448 };
1449
1450 HappnClient.prototype.__updateSecurityDirectory = function(message) {
1451 var _this = this;
1452 if (message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_REMOVED) {
1453 if (['*', 'on'].indexOf(message.data.changedData.action) > -1) {
1454 let permissions = {};
1455 permissions[message.data.changedData.path] = {
1456 actions: [message.data.changedData.action]
1457 };
1458 return _this.__reattachChangedListeners(permissions);
1459 }
1460 }
1461
1462 if (message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.UPSERT_GROUP) {
1463 Object.keys(message.data.changedData.permissions).forEach(function(permissionPath) {
1464 var permission = message.data.changedData.permissions[permissionPath];
1465 if (
1466 permission.prohibit &&
1467 (permission.prohibit.indexOf('on') > -1 || permission.prohibit.indexOf('*') > -1)
1468 )
1469 _this.__clearSecurityDirectorySubscriptions(permissionPath);
1470 });
1471 }
1472
1473 if (message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.UNLINK_GROUP) {
1474 return _this.__reattachChangedListeners(message.data.changedData.permissions);
1475 }
1476
1477 if (
1478 message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.UPSERT_USER &&
1479 message.data.changedData.permissions //backward compatible older servers that dont have user permissions
1480 ) {
1481 Object.keys(message.data.changedData.permissions).forEach(function(permissionPath) {
1482 var permission = message.data.changedData.permissions[permissionPath];
1483 if (
1484 permission.prohibit &&
1485 (permission.prohibit.indexOf('on') > -1 || permission.prohibit.indexOf('*') > -1)
1486 )
1487 _this.__clearSecurityDirectorySubscriptions(permissionPath);
1488 });
1489 }
1490 };
1491
1492 HappnClient.prototype.__handleServerSideDisconnect = function(message) {
1493 this.emit('session-ended', message.data);
1494 };
1495
1496 HappnClient.prototype.__handleSystemMessage = function(message) {
1497 if (message.eventKey === 'server-side-disconnect') {
1498 this.status = STATUS.DISCONNECTED;
1499 this.__handleServerSideDisconnect(message);
1500 }
1501 if (message.eventKey === 'security-data-changed') this.__updateSecurityDirectory(message);
1502
1503 this.state.systemMessageHandlers.every(function(messageHandler) {
1504 return messageHandler.apply(messageHandler, [message.eventKey, message.data]);
1505 });
1506 };
1507
1508 HappnClient.prototype.offSystemMessage = function(index) {
1509 this.state.systemMessageHandlers.splice(index, 1);
1510 };
1511
1512 HappnClient.prototype.onSystemMessage = function(handler) {
1513 this.state.systemMessageHandlers.push(handler);
1514 return this.state.systemMessageHandlers.length - 1;
1515 };
1516
1517 HappnClient.prototype._remoteOn = function(path, parameters, callback) {
1518 this.__performDataRequest(path, 'on', null, parameters, callback);
1519 };
1520
1521 HappnClient.prototype.__clearListenerState = function(path, listener) {
1522 this.state.events[path].splice(this.state.events[path].indexOf(listener), 1);
1523 if (this.state.events[path].length === 0) delete this.state.events[path];
1524 this.state.refCount[listener.eventKey]--;
1525 if (this.state.refCount[listener.eventKey] === 0) delete this.state.refCount[listener.eventKey];
1526 this.__clearListenerRef(listener);
1527 };
1528
1529 HappnClient.prototype.__confirmRemoteOn = function(path, parameters, listener, callback) {
1530 var _this = this;
1531
1532 _this._remoteOn(path, parameters, function(e, response) {
1533 if (e || response.status === 'error') {
1534 _this.__clearListenerState(path, listener);
1535 //TODO: do we want to return something that is not an error (response.payload)
1536 if (response && response.status === 'error') return callback(response.payload);
1537 return callback(e);
1538 }
1539
1540 if (listener.initialEmit) {
1541 //emit data as events immediately
1542 response.forEach(function(message) {
1543 listener.handler(message);
1544 });
1545 _this.__updateListenerRef(listener, response._meta.referenceId);
1546 } else if (listener.initialCallback) {
1547 //emit the data in the callback
1548 _this.__updateListenerRef(listener, response._meta.referenceId);
1549 } else {
1550 _this.__updateListenerRef(listener, response.id);
1551 }
1552
1553 if (parameters.onPublished) return callback(null, listener.id);
1554 if (listener.initialCallback) return callback(null, listener.id, response);
1555
1556 callback(null, listener.id);
1557 });
1558 };
1559
1560 HappnClient.prototype.__getListener = function(handler, parameters, path, variableDepth) {
1561 return {
1562 handler: handler,
1563 count: parameters.count,
1564 eventKey: JSON.stringify({
1565 path: path,
1566 event_type: parameters.event_type,
1567 count: parameters.count,
1568 initialEmit: parameters.initialEmit,
1569 initialCallback: parameters.initialCallback,
1570 meta: parameters.meta,
1571 depth: parameters.depth
1572 }),
1573 runcount: 0,
1574 meta: parameters.meta,
1575 id: this.state.currentListenerId++,
1576 initialEmit: parameters.initialEmit,
1577 initialCallback: parameters.initialCallback,
1578 depth: parameters.depth,
1579 variableDepth: variableDepth
1580 };
1581 };
1582
1583 HappnClient.prototype.once = promisify(function(path, parameters, handler, callback) {
1584 if (typeof parameters === 'function') {
1585 callback = handler;
1586 handler = parameters;
1587 parameters = {};
1588 }
1589 parameters.count = 1;
1590 return this.on(path, parameters, handler, callback);
1591 });
1592
1593 HappnClient.prototype.on = promisify(function(path, parameters, handler, callback) {
1594 if (typeof parameters === 'function') {
1595 callback = handler;
1596 handler = parameters;
1597 parameters = {};
1598 }
1599
1600 var variableDepth =
1601 typeof path === 'string' && (path === '**' || path.substring(path.length - 3) === '/**');
1602
1603 if (!parameters) parameters = {};
1604 if (!parameters.event_type || parameters.event_type === '*') parameters.event_type = 'all';
1605 if (!parameters.count) parameters.count = 0;
1606 if (variableDepth && !parameters.depth) parameters.depth = this.options.defaultVariableDepth; //5 by default
1607
1608 if (!callback) {
1609 if (typeof parameters.onPublished !== 'function')
1610 throw new Error('you cannot subscribe without passing in a subscription callback');
1611 if (typeof handler !== 'function')
1612 throw new Error('callback cannot be null when using the onPublished event handler');
1613 callback = handler;
1614 handler = parameters.onPublished;
1615 }
1616
1617 path = this.getChannel(path, parameters.event_type);
1618 var listener = this.__getListener(handler, parameters, path, variableDepth);
1619
1620 if (!this.state.events[path]) this.state.events[path] = [];
1621 if (!this.state.refCount[listener.eventKey]) this.state.refCount[listener.eventKey] = 0;
1622
1623 this.state.events[path].push(listener);
1624 this.state.refCount[listener.eventKey]++;
1625
1626 if (
1627 !(
1628 this.state.refCount[listener.eventKey] === 1 ||
1629 listener.initialCallback ||
1630 listener.initialEmit
1631 )
1632 )
1633 return callback(null, listener.id);
1634
1635 this.__confirmRemoteOn(path, parameters, listener, callback);
1636 });
1637
1638 HappnClient.prototype.onAll = promisify(function(handler, callback) {
1639 return this.on('*', {}, handler, callback);
1640 });
1641
1642 HappnClient.prototype._remoteOff = function(channel, listenerRef, callback) {
1643 if (typeof listenerRef === 'function') {
1644 callback = listenerRef;
1645 listenerRef = 0;
1646 }
1647
1648 this.__performDataRequest(
1649 channel,
1650 'off',
1651 null,
1652 {
1653 referenceId: listenerRef
1654 },
1655 function(e, response) {
1656 if (e) return callback(e);
1657 if (response.status === 'error') return callback(response.payload);
1658 callback();
1659 }
1660 );
1661 };
1662
1663 HappnClient.prototype._offListener = function(handle, callback) {
1664 var _this = this;
1665
1666 if (!_this.state.events || _this.state.events.length === 0) return callback();
1667 var listenerFound = false;
1668
1669 Object.keys(_this.state.events).every(function(channel) {
1670 var listeners = _this.state.events[channel];
1671
1672 //use every here so we can exit early
1673 return listeners.every(function(listener) {
1674 if (listener.id !== handle) return true;
1675
1676 listenerFound = true;
1677
1678 var listenerRef = _this.__getListenerRef(listener);
1679 _this.__clearListenerState(channel, listener);
1680
1681 //now unsubscribe if we are off
1682 if (
1683 !_this.state.refCount[listener.eventKey] ||
1684 listener.initialEmit ||
1685 listener.initialCallback
1686 )
1687 _this._remoteOff(channel, listenerRef, callback);
1688 else callback();
1689
1690 return false;
1691 });
1692 });
1693 //in case a listener with that index does not exist
1694 if (!listenerFound) return callback();
1695 };
1696
1697 HappnClient.prototype._offPath = function(path, callback) {
1698 var _this = this;
1699 var unsubscriptions = [];
1700 var channels = [];
1701
1702 Object.keys(_this.state.events).forEach(function(channel) {
1703 var channelParts = channel.split('@');
1704 var channelPath = channelParts.slice(1, channelParts.length).join('@');
1705
1706 if (_this.utils.wildcardMatch(path, channelPath)) {
1707 channels.push(channel);
1708 _this.state.events[channel].forEach(function(listener) {
1709 unsubscriptions.push(listener);
1710 });
1711 }
1712 });
1713
1714 if (unsubscriptions.length === 0) return callback();
1715
1716 _this.utils.async(
1717 unsubscriptions,
1718 function(listener, index, next) {
1719 _this._offListener(listener.id, next);
1720 },
1721 function(e) {
1722 if (e) return callback(e);
1723 channels.forEach(function(channel) {
1724 delete _this.state.events[channel];
1725 });
1726 callback();
1727 }
1728 );
1729 };
1730
1731 HappnClient.prototype.offAll = maybePromisify(function(callback) {
1732 var _this = this;
1733
1734 return _this._remoteOff('*', function(e) {
1735 if (e) return callback(e);
1736
1737 _this.state.events = {};
1738 _this.state.refCount = {};
1739 _this.state.listenerRefs = {};
1740
1741 callback();
1742 });
1743 });
1744
1745 HappnClient.prototype.off = maybePromisify(function(handle, callback) {
1746 if (handle == null) return callback(new Error('handle cannot be null'));
1747
1748 if (typeof handle !== 'number') return callback(new Error('handle must be a number'));
1749
1750 this._offListener(handle, callback);
1751 });
1752
1753 HappnClient.prototype.offPath = maybePromisify(function(path, callback) {
1754 if (typeof path === 'function') {
1755 callback = path;
1756 path = '*';
1757 }
1758
1759 return this._offPath(path, callback);
1760 });
1761
1762 HappnClient.prototype.clearTimeouts = function() {
1763 var _this = this;
1764 clearTimeout(_this.__retryReconnectTimeout);
1765 Object.keys(_this.state.ackHandlers).forEach(function(handlerKey) {
1766 clearTimeout(_this.state.ackHandlers[handlerKey].timedout);
1767 });
1768 Object.keys(_this.state.requestEvents).forEach(function(handlerKey) {
1769 clearTimeout(_this.state.requestEvents[handlerKey].timedout);
1770 });
1771 };
1772
1773 HappnClient.prototype.revokeToken = function(callback) {
1774 return this.__performSystemRequest('revoke-token', null, null, callback);
1775 };
1776
1777 HappnClient.prototype.__destroySocket = function(socket, callback) {
1778 //possible socket end needs to do its thing, we destroy in the next tick
1779 var _this = this;
1780 setTimeout(function() {
1781 var destroyError;
1782 try {
1783 socket.destroy();
1784 } catch (e) {
1785 _this.log.warn('socket.destroy failed in client: ' + e.toString());
1786 destroyError = e;
1787 }
1788 callback(destroyError);
1789 }, 0);
1790 };
1791
1792 HappnClient.prototype.__endAndDestroySocket = function(socket, callback) {
1793 if (socket._local) {
1794 //this is an eventemitter connection
1795 setTimeout(function() {
1796 socket.end();
1797 callback();
1798 }, 0);
1799 return;
1800 }
1801 if (socket.readyState !== 1) {
1802 //socket is already disconnecting or disconnected, close event wont be fired
1803 socket.end();
1804 return this.__destroySocket(socket, callback);
1805 }
1806 //socket is currently open, close event will be fired
1807 var _this = this;
1808 socket
1809 .once('close', function() {
1810 _this.__destroySocket(socket, callback);
1811 })
1812 .end();
1813 };
1814
1815 HappnClient.prototype.__connectionCleanup = function(options, callback) {
1816 if (typeof options === 'function') {
1817 callback = options;
1818 options = null;
1819 }
1820
1821 if (!this.socket) return callback();
1822 if (!options) options = {};
1823 var _this = this;
1824
1825 if (browser) {
1826 if (options.deleteCookie) {
1827 this.__expireCookie(this.session, document);
1828 }
1829 }
1830
1831 if (options.revokeToken || options.revokeSession)
1832 return _this.revokeToken(function(e) {
1833 if (e)
1834 _this.log.warn(
1835 '__connectionCleanup failed in client, revoke-token failed: ' + e.toString()
1836 );
1837 _this.__endAndDestroySocket(_this.socket, callback);
1838 });
1839
1840 if (options.disconnectChildSessions)
1841 return _this.__performSystemRequest('disconnect-child-sessions', null, null, function(e) {
1842 if (e)
1843 _this.log.warn(
1844 '__connectionCleanup failed in client, disconnect-child-sessions failed: ' +
1845 e.toString()
1846 );
1847 _this.__endAndDestroySocket(_this.socket, callback);
1848 });
1849 _this.__endAndDestroySocket(_this.socket, callback);
1850 };
1851
1852 HappnClient.prototype.disconnect = maybePromisify(function(options, callback) {
1853 if (typeof options === 'function') {
1854 callback = options;
1855 options = null;
1856 }
1857
1858 if (!options) options = {};
1859 if (!callback) callback = function() {};
1860
1861 this.clearTimeouts();
1862 var _this = this;
1863 this.__connectionCleanup(options, function(e) {
1864 if (e) return callback(e);
1865 _this.socket = null;
1866 _this.state.systemMessageHandlers = [];
1867 _this.state.eventHandlers = {};
1868 _this.state.events = {};
1869 _this.state.refCount = {};
1870 _this.state.listenerRefs = {};
1871 _this.status = STATUS.DISCONNECTED;
1872 callback();
1873 });
1874 });
1875})(); // end enclosed