UNPKG

27.7 kBJavaScriptView Raw
1import { __awaiter, __generator, __decorate } from 'tslib';
2import { EventEmitter, NgZone, Output, Input, Component, NgModule } from '@angular/core';
3import { BehaviorSubject, Observable, Subject, interval } from 'rxjs';
4import { filter, share, tap, takeUntil } from 'rxjs/operators';
5import { Brolog } from 'brolog';
6import { StateSwitch } from 'state-switch';
7
8/**
9 * This file will be overwrite when we publish NPM module
10 * by scripts/generate_version.ts
11 */
12var VERSION = '0.0.0';
13
14var ReadyState;
15(function (ReadyState) {
16 ReadyState[ReadyState["CLOSED"] = WebSocket.CLOSED] = "CLOSED";
17 ReadyState[ReadyState["CLOSING"] = WebSocket.CLOSING] = "CLOSING";
18 ReadyState[ReadyState["CONNECTING"] = WebSocket.CONNECTING] = "CONNECTING";
19 ReadyState[ReadyState["OPEN"] = WebSocket.OPEN] = "OPEN";
20})(ReadyState || (ReadyState = {}));
21var IoService = /** @class */ (function () {
22 function IoService() {
23 this.autoReconnect = true;
24 this.log = Brolog.instance();
25 this.CONNECT_TIMEOUT = 10 * 1000; // 10 seconds
26 this.ENDPOINT = 'wss://api.chatie.io/v0/websocket/token/';
27 this.PROTOCOL = 'web|0.0.1';
28 this.sendBuffer = [];
29 this.log.verbose('IoService', 'constructor()');
30 }
31 Object.defineProperty(IoService.prototype, "readyState", {
32 get: function () {
33 return this._readyState.asObservable();
34 },
35 enumerable: true,
36 configurable: true
37 });
38 IoService.prototype.init = function () {
39 return __awaiter(this, void 0, void 0, function () {
40 var e_1;
41 var _this = this;
42 return __generator(this, function (_a) {
43 switch (_a.label) {
44 case 0:
45 this.log.verbose('IoService', 'init()');
46 if (this.state) {
47 throw new Error('re-init');
48 }
49 this.snapshot = {
50 readyState: ReadyState.CLOSED,
51 event: null,
52 };
53 this._readyState = new BehaviorSubject(ReadyState.CLOSED);
54 this.state = new StateSwitch('IoService', this.log);
55 this.state.setLog(this.log);
56 _a.label = 1;
57 case 1:
58 _a.trys.push([1, 4, , 5]);
59 return [4 /*yield*/, this.initStateDealer()];
60 case 2:
61 _a.sent();
62 return [4 /*yield*/, this.initRxSocket()];
63 case 3:
64 _a.sent();
65 return [3 /*break*/, 5];
66 case 4:
67 e_1 = _a.sent();
68 this.log.silly('IoService', 'init() exception: %s', e_1.message);
69 throw e_1;
70 case 5:
71 this.readyState.subscribe(function (s) {
72 _this.log.silly('IoService', 'init() readyState.subscribe(%s)', ReadyState[s]);
73 _this.snapshot.readyState = s;
74 });
75 // IMPORTANT: subscribe to event and make it HOT!
76 this.event.subscribe(function (s) {
77 _this.log.silly('IoService', 'init() event.subscribe({name:%s})', s.name);
78 _this.snapshot.event = s;
79 });
80 return [2 /*return*/];
81 }
82 });
83 });
84 };
85 IoService.prototype.token = function (newToken) {
86 this.log.silly('IoService', 'token(%s)', newToken);
87 if (newToken) {
88 this._token = newToken;
89 return;
90 }
91 return this._token;
92 };
93 IoService.prototype.start = function () {
94 return __awaiter(this, void 0, void 0, function () {
95 var e_2;
96 return __generator(this, function (_a) {
97 switch (_a.label) {
98 case 0:
99 this.log.verbose('IoService', 'start() with token:%s', this._token);
100 if (!this._token) {
101 throw new Error('start() without token');
102 }
103 if (this.state.on()) {
104 throw new Error('state is already ON');
105 }
106 if (this.state.pending()) {
107 throw new Error('state is pending');
108 }
109 this.state.on('pending');
110 this.autoReconnect = true;
111 _a.label = 1;
112 case 1:
113 _a.trys.push([1, 3, , 4]);
114 return [4 /*yield*/, this.connectRxSocket()];
115 case 2:
116 _a.sent();
117 this.state.on(true);
118 return [3 /*break*/, 4];
119 case 3:
120 e_2 = _a.sent();
121 this.log.warn('IoService', 'start() failed:%s', e_2.message);
122 this.state.off(true);
123 return [3 /*break*/, 4];
124 case 4: return [2 /*return*/];
125 }
126 });
127 });
128 };
129 IoService.prototype.stop = function () {
130 return __awaiter(this, void 0, void 0, function () {
131 return __generator(this, function (_a) {
132 switch (_a.label) {
133 case 0:
134 this.log.verbose('IoService', 'stop()');
135 if (this.state.off()) {
136 this.log.warn('IoService', 'stop() state is already off');
137 if (this.state.pending()) {
138 throw new Error('state pending() is true');
139 }
140 return [2 /*return*/];
141 }
142 this.state.off('pending');
143 this.autoReconnect = false;
144 if (!this._websocket) {
145 throw new Error('no websocket');
146 }
147 return [4 /*yield*/, this.socketClose(1000, 'IoService.stop()')];
148 case 1:
149 _a.sent();
150 this.state.off(true);
151 return [2 /*return*/];
152 }
153 });
154 });
155 };
156 IoService.prototype.restart = function () {
157 return __awaiter(this, void 0, void 0, function () {
158 var e_3;
159 return __generator(this, function (_a) {
160 switch (_a.label) {
161 case 0:
162 this.log.verbose('IoService', 'restart()');
163 _a.label = 1;
164 case 1:
165 _a.trys.push([1, 4, , 5]);
166 return [4 /*yield*/, this.stop()];
167 case 2:
168 _a.sent();
169 return [4 /*yield*/, this.start()];
170 case 3:
171 _a.sent();
172 return [3 /*break*/, 5];
173 case 4:
174 e_3 = _a.sent();
175 this.log.error('IoService', 'restart() error:%s', e_3.message);
176 throw e_3;
177 case 5: return [2 /*return*/];
178 }
179 });
180 });
181 };
182 IoService.prototype.initStateDealer = function () {
183 var _this = this;
184 this.log.verbose('IoService', 'initStateDealer()');
185 this.readyState.pipe(filter(function (s) { return s === ReadyState.OPEN; }))
186 .subscribe(function (open) { return _this.stateOnOpen(); });
187 };
188 /**
189 * Creates a subject from the specified observer and observable.
190 * - https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md
191 * Create an Rx.Subject using Subject.create that allows onNext without subscription
192 * A socket implementation (example, don't use)
193 * - http://stackoverflow.com/a/34862286/1123955
194 */
195 IoService.prototype.initRxSocket = function () {
196 return __awaiter(this, void 0, void 0, function () {
197 var observable;
198 var _this = this;
199 return __generator(this, function (_a) {
200 this.log.verbose('IoService', 'initRxSocket()');
201 if (this.event) {
202 throw new Error('re-init is not permitted');
203 }
204 // 1. Mobile Originated. moObserver.next() means mobile is sending
205 this.moObserver = {
206 next: this.socketSend.bind(this),
207 error: this.socketClose.bind(this),
208 complete: this.socketClose.bind(this),
209 };
210 observable = Observable.create(function (observer) {
211 _this.log.verbose('IoService', 'initRxSocket() Observable.create()');
212 _this.mtObserver = observer;
213 return _this.socketClose.bind(_this);
214 });
215 // 3. Subject for MO & MT Observers
216 this.event = Subject.create(this.moObserver, observable.pipe(share()));
217 return [2 /*return*/];
218 });
219 });
220 };
221 IoService.prototype.connectRxSocket = function () {
222 return __awaiter(this, void 0, void 0, function () {
223 var onOpenPromise;
224 var _this = this;
225 return __generator(this, function (_a) {
226 this.log.verbose('IoService', 'connectRxSocket()');
227 // FIXME: check & close the old one
228 if (this._websocket) {
229 throw new Error('already has a websocket');
230 }
231 // if (this.state.target() !== 'open'
232 // || this.state.current() !== 'open'
233 // || this.state.stable()
234 if (this.state.off()) {
235 throw new Error('switch state is off');
236 }
237 else if (!this.state.pending()) {
238 throw new Error('switch state is already ON');
239 }
240 this._websocket = new WebSocket(this.endPoint(), this.PROTOCOL);
241 this.socketUpdateState();
242 onOpenPromise = new Promise(function (resolve, reject) {
243 _this.log.verbose('IoService', 'connectRxSocket() Promise()');
244 var id = setTimeout(function () {
245 _this._websocket = null;
246 var e = new Error('rxSocket connect timeout after '
247 + Math.round(_this.CONNECT_TIMEOUT / 1000));
248 reject(e);
249 }, _this.CONNECT_TIMEOUT); // timeout for connect websocket
250 _this._websocket.onopen = function (e) {
251 _this.log.verbose('IoService', 'connectRxSocket() Promise() WebSocket.onOpen() resolve()');
252 _this.socketUpdateState();
253 clearTimeout(id);
254 resolve();
255 };
256 });
257 // Handle the payload
258 this._websocket.onmessage = this.socketOnMessage.bind(this);
259 // Deal the event
260 this._websocket.onerror = this.socketOnError.bind(this);
261 this._websocket.onclose = this.socketOnClose.bind(this);
262 return [2 /*return*/, onOpenPromise];
263 });
264 });
265 };
266 IoService.prototype.endPoint = function () {
267 var url = this.ENDPOINT + this._token;
268 this.log.verbose('IoService', 'endPoint() => %s', url);
269 return url;
270 };
271 /******************************************************************
272 *
273 * State Event Listeners
274 *
275 */
276 IoService.prototype.stateOnOpen = function () {
277 this.log.verbose('IoService', 'stateOnOpen()');
278 this.socketSendBuffer();
279 this.rpcUpdate('from stateOnOpen()');
280 };
281 /******************************************************************
282 *
283 * Io RPC Methods
284 *
285 */
286 IoService.prototype.rpcDing = function (payload) {
287 return __awaiter(this, void 0, void 0, function () {
288 var e;
289 return __generator(this, function (_a) {
290 this.log.verbose('IoService', 'ding(%s)', payload);
291 e = {
292 name: 'ding',
293 payload: payload,
294 };
295 this.event.next(e);
296 return [2 /*return*/];
297 });
298 });
299 };
300 IoService.prototype.rpcUpdate = function (payload) {
301 return __awaiter(this, void 0, void 0, function () {
302 return __generator(this, function (_a) {
303 this.event.next({
304 name: 'update',
305 payload: payload,
306 });
307 return [2 /*return*/];
308 });
309 });
310 };
311 /******************************************************************
312 *
313 * Socket Actions
314 *
315 */
316 IoService.prototype.socketClose = function (code, reason) {
317 return __awaiter(this, void 0, void 0, function () {
318 var future;
319 var _this = this;
320 return __generator(this, function (_a) {
321 switch (_a.label) {
322 case 0:
323 this.log.verbose('IoService', 'socketClose()');
324 if (!this._websocket) {
325 throw new Error('no websocket');
326 }
327 this._websocket.close(code, reason);
328 this.socketUpdateState();
329 future = new Promise(function (resolve) {
330 _this.readyState.pipe(filter(function (s) { return s === ReadyState.CLOSED; }))
331 .subscribe(resolve);
332 });
333 return [4 /*yield*/, future];
334 case 1:
335 _a.sent();
336 return [2 /*return*/];
337 }
338 });
339 });
340 };
341 IoService.prototype.socketSend = function (ioEvent) {
342 this.log.silly('IoService', 'socketSend({name:%s, payload:%s})', ioEvent.name, ioEvent.payload);
343 if (!this._websocket) {
344 this.log.silly('IoService', 'socketSend() no _websocket');
345 }
346 var strEvt = JSON.stringify(ioEvent);
347 this.sendBuffer.push(strEvt);
348 // XXX can move this to onOpen?
349 this.socketSendBuffer();
350 };
351 IoService.prototype.socketSendBuffer = function () {
352 this.log.silly('IoService', 'socketSendBuffer() length:%s', this.sendBuffer.length);
353 if (!this._websocket) {
354 throw new Error('socketSendBuffer(): no _websocket');
355 }
356 if (this._websocket.readyState !== WebSocket.OPEN) {
357 this.log.warn('IoService', 'socketSendBuffer() readyState is not OPEN, send job delayed.');
358 return;
359 }
360 while (this.sendBuffer.length) {
361 var buf = this.sendBuffer.shift();
362 this.log.silly('IoService', 'socketSendBuffer() sending(%s)', buf);
363 this._websocket.send(buf);
364 }
365 };
366 IoService.prototype.socketUpdateState = function () {
367 this.log.verbose('IoService', 'socketUpdateState() is %s', ReadyState[this._websocket.readyState]);
368 if (!this._websocket) {
369 this.log.error('IoService', 'socketUpdateState() no _websocket');
370 return;
371 }
372 this._readyState.next(this._websocket.readyState);
373 };
374 /******************************************************************
375 *
376 * Socket Events Listener
377 *
378 */
379 IoService.prototype.socketOnMessage = function (message) {
380 this.log.verbose('IoService', 'onMessage({data: %s})', message.data);
381 var data = message.data; // WebSocket data
382 var ioEvent = {
383 name: 'raw',
384 payload: data,
385 }; // this is default io event for unknown format message
386 try {
387 var obj = JSON.parse(data);
388 ioEvent.name = obj.name;
389 ioEvent.payload = obj.payload;
390 }
391 catch (e) {
392 this.log.warn('IoService', 'onMessage parse message fail. save as RAW');
393 }
394 this.mtObserver.next(ioEvent);
395 };
396 IoService.prototype.socketOnError = function (event) {
397 this.log.silly('IoService', 'socketOnError(%s)', event);
398 // this._websocket = null
399 };
400 /**
401 * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
402 * code: 1006 CLOSE_ABNORMAL
403 * - Reserved. Used to indicate that a connection was closed abnormally
404 * (that is, with no close frame being sent) when a status code is expected.
405 */
406 IoService.prototype.socketOnClose = function (closeEvent) {
407 var _this = this;
408 this.log.verbose('IoService', 'socketOnClose({code:%s, reason:%s, returnValue:%s})', closeEvent.code, closeEvent.reason, closeEvent.returnValue);
409 this.socketUpdateState();
410 /**
411 * reconnect inside onClose
412 */
413 if (this.autoReconnect) {
414 this.state.on('pending');
415 setTimeout(function () { return __awaiter(_this, void 0, void 0, function () {
416 var e_4;
417 return __generator(this, function (_a) {
418 switch (_a.label) {
419 case 0:
420 _a.trys.push([0, 2, , 3]);
421 return [4 /*yield*/, this.connectRxSocket()];
422 case 1:
423 _a.sent();
424 this.state.on(true);
425 return [3 /*break*/, 3];
426 case 2:
427 e_4 = _a.sent();
428 this.log.warn('IoService', 'socketOnClose() autoReconnect() exception: %s', e_4);
429 this.state.off(true);
430 return [3 /*break*/, 3];
431 case 3: return [2 /*return*/];
432 }
433 });
434 }); }, 1000);
435 }
436 else {
437 this.state.off(true);
438 }
439 this._websocket = null;
440 if (!closeEvent.wasClean) {
441 this.log.warn('IoService', 'socketOnClose() event.wasClean FALSE');
442 // TODO emit error
443 }
444 };
445 return IoService;
446}());
447
448var WechatyComponent = /** @class */ (function () {
449 function WechatyComponent(log, ngZone) {
450 this.log = log;
451 this.ngZone = ngZone;
452 this.message = new EventEmitter();
453 this.scan = new EventEmitter();
454 this.login = new EventEmitter();
455 this.logout = new EventEmitter();
456 this.error = new EventEmitter();
457 this.heartbeat = new EventEmitter();
458 this.timerSub = null;
459 this.counter = 0;
460 this.timestamp = new Date();
461 this.log.verbose('WechatyComponent', 'constructor() v%s', VERSION);
462 }
463 Object.defineProperty(WechatyComponent.prototype, "token", {
464 get: function () { return this._token; },
465 set: function (_newToken) {
466 this.log.verbose('WechatyComponent', 'set token(%s)', _newToken);
467 var newToken = (_newToken || '').trim();
468 if (this._token === newToken) {
469 this.log.silly('WechatyComponent', 'set token(%s) not new', newToken);
470 return;
471 }
472 this._token = newToken;
473 if (!this.ioService) {
474 this.log.silly('WechatyComponent', 'set token() skip token init value');
475 this.log.silly('WechatyComponent', 'set token() because ioService will do it inside ngOnInit()');
476 return;
477 }
478 this.log.silly('WechatyComponent', 'set token(%s) reloading ioService now...', newToken);
479 this.ioService.token(this.token);
480 this.ioService.restart(); // async
481 },
482 enumerable: true,
483 configurable: true
484 });
485 WechatyComponent.prototype.ngOnInit = function () {
486 return __awaiter(this, void 0, void 0, function () {
487 return __generator(this, function (_a) {
488 switch (_a.label) {
489 case 0:
490 this.log.verbose('WechatyComponent', 'ngOninit() with token: ' + this.token);
491 this.ioService = new IoService();
492 return [4 /*yield*/, this.ioService.init()];
493 case 1:
494 _a.sent();
495 this.ioService.event.subscribe(this.onIo.bind(this));
496 this.log.silly('WechatyComponent', 'ngOninit() ioService.event.subscribe()-ed');
497 if (!this.token) return [3 /*break*/, 3];
498 this.ioService.token(this.token);
499 return [4 /*yield*/, this.ioService.start()];
500 case 2:
501 _a.sent();
502 _a.label = 3;
503 case 3: return [2 /*return*/];
504 }
505 });
506 });
507 };
508 WechatyComponent.prototype.ngOnDestroy = function () {
509 this.log.verbose('WechatyComponent', 'ngOnDestroy()');
510 this.endTimer();
511 if (this.ioService) {
512 this.ioService.stop();
513 // this.ioService = null
514 }
515 };
516 WechatyComponent.prototype.onIo = function (e) {
517 this.log.silly('WechatyComponent', 'onIo#%d(%s)', this.counter++, e.name);
518 this.timestamp = new Date();
519 switch (e.name) {
520 case 'scan':
521 this.scan.emit(e.payload);
522 break;
523 case 'login':
524 this.login.emit(e.payload);
525 break;
526 case 'logout':
527 this.logout.emit(e.payload);
528 break;
529 case 'message':
530 this.message.emit(e.payload);
531 break;
532 case 'error':
533 this.error.emit(e.payload);
534 break;
535 case 'ding':
536 case 'dong':
537 case 'raw':
538 this.heartbeat.emit(e.name + '[' + e.payload + ']');
539 break;
540 case 'heartbeat':
541 this.heartbeat.emit(e.payload);
542 break;
543 case 'sys':
544 this.log.silly('WechatyComponent', 'onIo(%s): %s', e.name, e.payload);
545 break;
546 default:
547 this.log.warn('WechatyComponent', 'onIo() unknown event name: %s[%s]', e.name, e.payload);
548 break;
549 }
550 };
551 WechatyComponent.prototype.reset = function (reason) {
552 this.log.verbose('WechatyComponent', 'reset(%s)', reason);
553 var resetEvent = {
554 name: 'reset',
555 payload: reason,
556 };
557 if (!this.ioService) {
558 throw new Error('no ioService');
559 }
560 this.ioService.event.next(resetEvent);
561 };
562 WechatyComponent.prototype.shutdown = function (reason) {
563 this.log.verbose('WechatyComponent', 'shutdown(%s)', reason);
564 var shutdownEvent = {
565 name: 'shutdown',
566 payload: reason,
567 };
568 if (!this.ioService) {
569 throw new Error('no ioService');
570 }
571 this.ioService.event.next(shutdownEvent);
572 };
573 WechatyComponent.prototype.startTimer = function () {
574 var _this = this;
575 this.log.verbose('WechatyComponent', 'startTimer()');
576 this.ender = new Subject();
577 // https://github.com/angular/protractor/issues/3349#issuecomment-232253059
578 // https://github.com/juliemr/ngconf-2016-zones/blob/master/src/app/main.ts#L38
579 this.ngZone.runOutsideAngular(function () {
580 _this.timer = interval(3000).pipe(tap(function (i) { _this.log.verbose('do', ' %d', i); }), takeUntil(_this.ender), share());
581 // .publish()
582 });
583 this.timerSub = this.timer.subscribe(function (t) {
584 _this.counter = t;
585 if (!_this.ioService) {
586 throw new Error('no ioService');
587 }
588 _this.ioService.rpcDing(_this.counter);
589 // this.message.emit('#' + this.token + ':' + dong)
590 });
591 };
592 WechatyComponent.prototype.endTimer = function () {
593 this.log.verbose('WechatyComponent', 'endTimer()');
594 if (this.timerSub) {
595 this.timerSub.unsubscribe();
596 this.timerSub = null;
597 }
598 // this.timer = null
599 if (this.ender) {
600 this.ender.next(null);
601 // this.ender = null
602 }
603 };
604 WechatyComponent.prototype.logoff = function (reason) {
605 this.log.silly('WechatyComponent', 'logoff(%s)', reason);
606 var quitEvent = {
607 name: 'logout',
608 payload: reason,
609 };
610 this.ioService.event.next(quitEvent);
611 };
612 Object.defineProperty(WechatyComponent.prototype, "readyState", {
613 get: function () {
614 return this.ioService.readyState;
615 },
616 enumerable: true,
617 configurable: true
618 });
619 WechatyComponent.ctorParameters = function () { return [
620 { type: Brolog },
621 { type: NgZone }
622 ]; };
623 __decorate([
624 Output()
625 ], WechatyComponent.prototype, "message", void 0);
626 __decorate([
627 Output()
628 ], WechatyComponent.prototype, "scan", void 0);
629 __decorate([
630 Output()
631 ], WechatyComponent.prototype, "login", void 0);
632 __decorate([
633 Output()
634 ], WechatyComponent.prototype, "logout", void 0);
635 __decorate([
636 Output()
637 ], WechatyComponent.prototype, "error", void 0);
638 __decorate([
639 Output()
640 ], WechatyComponent.prototype, "heartbeat", void 0);
641 __decorate([
642 Input()
643 ], WechatyComponent.prototype, "token", null);
644 WechatyComponent = __decorate([
645 Component({
646 // tslint:disable-next-line:component-selector
647 selector: 'wechaty',
648 /**
649 * http://localhost:4200/app.component.html 404 (Not Found)
650 * zone.js:344 Unhandled Promise rejection: Failed to load app.component.html
651 * https://github.com/angular/angular-cli/issues/2592#issuecomment-266635266
652 * https://github.com/angular/angular-cli/issues/2293
653 *
654 * console.log from angular:
655 * If you're using Webpack you should inline the template and the styles,
656 * see https://goo.gl/X2J8zc.
657 */
658 template: '<ng-content></ng-content>'
659 })
660 ], WechatyComponent);
661 return WechatyComponent;
662}());
663
664var WechatyModule = /** @class */ (function () {
665 function WechatyModule() {
666 }
667 WechatyModule = __decorate([
668 NgModule({
669 id: 'wechaty',
670 declarations: [
671 WechatyComponent,
672 ],
673 exports: [
674 WechatyComponent,
675 ],
676 })
677 ], WechatyModule);
678 return WechatyModule;
679}());
680
681/**
682 * Generated bundle index. Do not edit.
683 */
684
685export { VERSION, WechatyComponent, WechatyModule, WechatyComponent as ɵa };
686//# sourceMappingURL=chatie-angular.js.map