UNPKG

21.3 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.2';
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 this.log.verbose('IoService', 'socketUpdateState() is %s', ReadyState[this._websocket.readyState]);
295 if (!this._websocket) {
296 this.log.error('IoService', 'socketUpdateState() no _websocket');
297 return;
298 }
299 this._readyState.next(this._websocket.readyState);
300 }
301 /******************************************************************
302 *
303 * Socket Events Listener
304 *
305 */
306 socketOnMessage(message) {
307 this.log.verbose('IoService', 'onMessage({data: %s})', message.data);
308 const data = message.data; // WebSocket data
309 const ioEvent = {
310 name: 'raw',
311 payload: data,
312 }; // this is default io event for unknown format message
313 try {
314 const obj = JSON.parse(data);
315 ioEvent.name = obj.name;
316 ioEvent.payload = obj.payload;
317 }
318 catch (e) {
319 this.log.warn('IoService', 'onMessage parse message fail. save as RAW');
320 }
321 this.mtObserver.next(ioEvent);
322 }
323 socketOnError(event) {
324 this.log.silly('IoService', 'socketOnError(%s)', event);
325 // this._websocket = null
326 }
327 /**
328 * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
329 * code: 1006 CLOSE_ABNORMAL
330 * - Reserved. Used to indicate that a connection was closed abnormally
331 * (that is, with no close frame being sent) when a status code is expected.
332 */
333 socketOnClose(closeEvent) {
334 this.log.verbose('IoService', 'socketOnClose({code:%s, reason:%s, returnValue:%s})', closeEvent.code, closeEvent.reason, closeEvent.returnValue);
335 this.socketUpdateState();
336 /**
337 * reconnect inside onClose
338 */
339 if (this.autoReconnect) {
340 this.state.on('pending');
341 setTimeout(() => __awaiter(this, void 0, void 0, function* () {
342 try {
343 yield this.connectRxSocket();
344 this.state.on(true);
345 }
346 catch (e) {
347 this.log.warn('IoService', 'socketOnClose() autoReconnect() exception: %s', e);
348 this.state.off(true);
349 }
350 }), 1000);
351 }
352 else {
353 this.state.off(true);
354 }
355 this._websocket = null;
356 if (!closeEvent.wasClean) {
357 this.log.warn('IoService', 'socketOnClose() event.wasClean FALSE');
358 // TODO emit error
359 }
360 }
361}
362
363class WechatyComponent {
364 constructor(log, ngZone) {
365 this.log = log;
366 this.ngZone = ngZone;
367 this.message = new EventEmitter();
368 this.scan = new EventEmitter();
369 this.login = new EventEmitter();
370 this.logout = new EventEmitter();
371 this.error = new EventEmitter();
372 this.heartbeat = new EventEmitter();
373 this.timerSub = null;
374 this.counter = 0;
375 this.timestamp = new Date();
376 this.log.verbose('WechatyComponent', 'constructor() v%s', VERSION);
377 }
378 get token() { return this._token; }
379 set token(_newToken) {
380 this.log.verbose('WechatyComponent', 'set token(%s)', _newToken);
381 const newToken = (_newToken || '').trim();
382 if (this._token === newToken) {
383 this.log.silly('WechatyComponent', 'set token(%s) not new', newToken);
384 return;
385 }
386 this._token = newToken;
387 if (!this.ioService) {
388 this.log.silly('WechatyComponent', 'set token() skip token init value');
389 this.log.silly('WechatyComponent', 'set token() because ioService will do it inside ngOnInit()');
390 return;
391 }
392 this.log.silly('WechatyComponent', 'set token(%s) reloading ioService now...', newToken);
393 this.ioService.token(this.token);
394 this.ioService.restart(); // async
395 }
396 ngOnInit() {
397 return __awaiter(this, void 0, void 0, function* () {
398 this.log.verbose('WechatyComponent', 'ngOnInit() with token: ' + this.token);
399 this.ioService = new IoService();
400 yield this.ioService.init();
401 this.ioService.event.subscribe(this.onIo.bind(this));
402 this.log.silly('WechatyComponent', 'ngOnInit() ioService.event.subscribe()-ed');
403 /**
404 * @Input(token) might not initialized in constructor()
405 */
406 if (this.token) {
407 this.ioService.token(this.token);
408 yield this.ioService.start();
409 }
410 // this.startTimer()
411 });
412 }
413 ngOnDestroy() {
414 this.log.verbose('WechatyComponent', 'ngOnDestroy()');
415 this.endTimer();
416 if (this.ioService) {
417 this.ioService.stop();
418 // this.ioService = null
419 }
420 }
421 onIo(e) {
422 this.log.silly('WechatyComponent', 'onIo#%d(%s)', this.counter++, e.name);
423 this.timestamp = new Date();
424 switch (e.name) {
425 case 'scan':
426 this.scan.emit(e.payload);
427 break;
428 case 'login':
429 this.login.emit(e.payload);
430 break;
431 case 'logout':
432 this.logout.emit(e.payload);
433 break;
434 case 'message':
435 this.message.emit(e.payload);
436 break;
437 case 'error':
438 this.error.emit(e.payload);
439 break;
440 case 'ding':
441 case 'dong':
442 case 'raw':
443 this.heartbeat.emit(e.name + '[' + e.payload + ']');
444 break;
445 case 'heartbeat':
446 this.heartbeat.emit(e.payload);
447 break;
448 case 'sys':
449 this.log.silly('WechatyComponent', 'onIo(%s): %s', e.name, e.payload);
450 break;
451 default:
452 this.log.warn('WechatyComponent', 'onIo() unknown event name: %s[%s]', e.name, e.payload);
453 break;
454 }
455 }
456 reset(reason) {
457 this.log.verbose('WechatyComponent', 'reset(%s)', reason);
458 const resetEvent = {
459 name: 'reset',
460 payload: reason,
461 };
462 if (!this.ioService) {
463 throw new Error('no ioService');
464 }
465 this.ioService.event.next(resetEvent);
466 }
467 shutdown(reason) {
468 this.log.verbose('WechatyComponent', 'shutdown(%s)', reason);
469 const shutdownEvent = {
470 name: 'shutdown',
471 payload: reason,
472 };
473 if (!this.ioService) {
474 throw new Error('no ioService');
475 }
476 this.ioService.event.next(shutdownEvent);
477 }
478 startSyncMessage() {
479 this.log.verbose('WechatyComponent', 'startSyncMessage()');
480 const botieEvent = {
481 name: 'botie',
482 payload: {
483 args: ['message'],
484 script: 'return this.syncMessage(message)',
485 },
486 };
487 if (!this.ioService) {
488 throw new Error('no ioService');
489 }
490 this.ioService.event.next(botieEvent);
491 }
492 startTimer() {
493 this.log.verbose('WechatyComponent', 'startTimer()');
494 this.ender = new Subject();
495 // https://github.com/angular/protractor/issues/3349#issuecomment-232253059
496 // https://github.com/juliemr/ngconf-2016-zones/blob/master/src/app/main.ts#L38
497 this.ngZone.runOutsideAngular(() => {
498 this.timer = interval(3000).pipe(tap(i => { this.log.verbose('do', ' %d', i); }), takeUntil(this.ender), share());
499 // .publish()
500 });
501 this.timerSub = this.timer.subscribe(t => {
502 this.counter = t;
503 if (!this.ioService) {
504 throw new Error('no ioService');
505 }
506 this.ioService.rpcDing(this.counter);
507 // this.message.emit('#' + this.token + ':' + dong)
508 });
509 }
510 endTimer() {
511 this.log.verbose('WechatyComponent', 'endTimer()');
512 if (this.timerSub) {
513 this.timerSub.unsubscribe();
514 this.timerSub = null;
515 }
516 // this.timer = null
517 if (this.ender) {
518 this.ender.next(null);
519 // this.ender = null
520 }
521 }
522 logoff(reason) {
523 this.log.silly('WechatyComponent', 'logoff(%s)', reason);
524 const quitEvent = {
525 name: 'logout',
526 payload: reason,
527 };
528 this.ioService.event.next(quitEvent);
529 }
530 get readyState() {
531 return this.ioService.readyState;
532 }
533}
534WechatyComponent.decorators = [
535 { type: Component, args: [{
536 // tslint:disable-next-line:component-selector
537 selector: 'wechaty',
538 /**
539 * http://localhost:4200/app.component.html 404 (Not Found)
540 * zone.js:344 Unhandled Promise rejection: Failed to load app.component.html
541 * https://github.com/angular/angular-cli/issues/2592#issuecomment-266635266
542 * https://github.com/angular/angular-cli/issues/2293
543 *
544 * console.log from angular:
545 * If you're using Webpack you should inline the template and the styles,
546 * see https://goo.gl/X2J8zc.
547 */
548 template: '<ng-content></ng-content>'
549 },] }
550];
551WechatyComponent.ctorParameters = () => [
552 { type: Brolog },
553 { type: NgZone }
554];
555WechatyComponent.propDecorators = {
556 message: [{ type: Output }],
557 scan: [{ type: Output }],
558 login: [{ type: Output }],
559 logout: [{ type: Output }],
560 error: [{ type: Output }],
561 heartbeat: [{ type: Output }],
562 token: [{ type: Input }]
563};
564
565class WechatyModule {
566}
567WechatyModule.decorators = [
568 { type: NgModule, args: [{
569 id: 'wechaty',
570 declarations: [
571 WechatyComponent,
572 ],
573 exports: [
574 WechatyComponent,
575 ],
576 },] }
577];
578
579/**
580 * Generated bundle index. Do not edit.
581 */
582
583export { VERSION, WechatyComponent, WechatyModule, WechatyComponent as ɵa };
584//# sourceMappingURL=chatie-angular.js.map