UNPKG

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