1 |
|
2 | (function() {
|
3 |
|
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 |
|
17 | if (typeof module !== 'undefined') module.exports = HappnClient;
|
18 |
|
19 | if (!browser) {
|
20 | Logger = require('happn-logger');
|
21 | PROTOCOL = 'happn_' + require('../package.json').protocol;
|
22 | HAPPN_VERSION = require('../package.json').version;
|
23 | Primus = require('happn-primus-wrapper');
|
24 | } else {
|
25 | window.HappnClient = HappnClient;
|
26 | Primus = window.Primus;
|
27 |
|
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 |
|
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 |
|
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];
|
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 |
|
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 |
|
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();
|
99 | return resolve(args);
|
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 |
|
119 |
|
120 |
|
121 |
|
122 |
|
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();
|
211 | this.__prepareInstanceOptions(options);
|
212 | this.__initializeEvents();
|
213 |
|
214 | return this;
|
215 | };
|
216 |
|
217 | HappnClient.prototype.initialize = maybePromisify(function(callback) {
|
218 | var _this = this;
|
219 |
|
220 |
|
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 |
|
250 | if (!options.socket) options.socket = {};
|
251 | if (!options.socket.reconnect) options.socket.reconnect = {};
|
252 | if (options.reconnect) options.socket.reconnect = options.reconnect;
|
253 | if (!options.socket.reconnect.retries) options.socket.reconnect.retries = Infinity;
|
254 | if (!options.socket.reconnect.max) options.socket.reconnect.max = 180e3;
|
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;
|
260 | }
|
261 |
|
262 | options.socket.pingTimeout =
|
263 | 'pingTimeout' in options.socket ? options.socket.pingTimeout : 45e3;
|
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 |
|
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;
|
321 |
|
322 |
|
323 | if (preparedOptions.context)
|
324 | Object.defineProperty(this, 'context', {
|
325 | value: preparedOptions.context
|
326 | });
|
327 |
|
328 |
|
329 | if (preparedOptions.plugin) {
|
330 | for (var overrideName in preparedOptions.plugin) {
|
331 |
|
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;
|
359 | if (preparedOptions.loginRetryInterval == null) preparedOptions.loginRetryInterval = 5000;
|
360 |
|
361 | if (preparedOptions.loginTimeout == null)
|
362 | preparedOptions.loginTimeout = preparedOptions.callTimeout;
|
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;
|
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 |
|
436 |
|
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 |
|
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;
|
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);
|
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';
|
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 |
|
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 |
|
836 |
|
837 |
|
838 |
|
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 |
|
856 |
|
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 |
|
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;
|
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);
|
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 |
|
980 | else message.options = options;
|
981 |
|
982 | if (!options.timeout) options.timeout = this.options.callTimeout;
|
983 |
|
984 | this.__requestCallback(message, callback, options, message.eventId);
|
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;
|
1056 |
|
1057 | options.noStore = true;
|
1058 | options.noDataResponse = true;
|
1059 |
|
1060 | try {
|
1061 |
|
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;
|
1076 |
|
1077 | try {
|
1078 |
|
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;
|
1137 | if (this.state.refCount[listener.eventKey]) {
|
1138 |
|
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 |
|
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 |
|
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 |
|
1192 | _this.utils.async(
|
1193 | listeners,
|
1194 | function(listener, index, nextListener) {
|
1195 | if (_this.state.refCount[listener.eventKey]) {
|
1196 |
|
1197 | _this.state.refCount[listener.eventKey]++;
|
1198 | return nextListener();
|
1199 | }
|
1200 |
|
1201 |
|
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 |
|
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 |
|
1228 | _this.state.refCount[listener.eventKey] = 1;
|
1229 |
|
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 |
|
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;
|
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 |
|
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 |
|
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
|
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 |
|
1536 | if (response && response.status === 'error') return callback(response.payload);
|
1537 | return callback(e);
|
1538 | }
|
1539 |
|
1540 | if (listener.initialEmit) {
|
1541 |
|
1542 | response.forEach(function(message) {
|
1543 | listener.handler(message);
|
1544 | });
|
1545 | _this.__updateListenerRef(listener, response._meta.referenceId);
|
1546 | } else if (listener.initialCallback) {
|
1547 |
|
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;
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
1795 | setTimeout(function() {
|
1796 | socket.end();
|
1797 | callback();
|
1798 | }, 0);
|
1799 | return;
|
1800 | }
|
1801 | if (socket.readyState !== 1) {
|
1802 |
|
1803 | socket.end();
|
1804 | return this.__destroySocket(socket, callback);
|
1805 | }
|
1806 |
|
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 | })();
|