UNPKG

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