1 | import { __awaiter } from 'tslib';
|
2 | import { EventEmitter, Component, NgZone, Output, Input, NgModule } from '@angular/core';
|
3 | import { BehaviorSubject, Observable, Subject, interval } from 'rxjs';
|
4 | import { filter, share, tap, takeUntil } from 'rxjs/operators';
|
5 | import { Brolog } from 'brolog';
|
6 | import { StateSwitch } from 'state-switch';
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | const VERSION = '0.7.4';
|
12 |
|
13 | var 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 = {}));
|
20 | class IoService {
|
21 | constructor() {
|
22 | this.autoReconnect = true;
|
23 | this.log = Brolog.instance();
|
24 | this.CONNECT_TIMEOUT = 10 * 1000;
|
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 |
|
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 |
|
140 |
|
141 |
|
142 |
|
143 |
|
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 |
|
152 | this.moObserver = {
|
153 | next: this.socketSend.bind(this),
|
154 | error: this.socketClose.bind(this),
|
155 | complete: this.socketClose.bind(this),
|
156 | };
|
157 |
|
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 |
|
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 |
|
171 | if (this._websocket) {
|
172 | throw new Error('already has a websocket');
|
173 | }
|
174 |
|
175 |
|
176 |
|
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);
|
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 |
|
201 | this._websocket.onmessage = this.socketOnMessage.bind(this);
|
202 |
|
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 |
|
216 |
|
217 |
|
218 | stateOnOpen() {
|
219 | this.log.verbose('IoService', 'stateOnOpen()');
|
220 | this.socketSendBuffer();
|
221 | this.rpcUpdate('from stateOnOpen()');
|
222 | }
|
223 | |
224 |
|
225 |
|
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 |
|
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 |
|
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 |
|
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 |
|
305 |
|
306 |
|
307 | socketOnMessage(message) {
|
308 | this.log.verbose('IoService', 'onMessage({data: %s})', message.data);
|
309 | const data = message.data;
|
310 | const ioEvent = {
|
311 | name: 'raw',
|
312 | payload: data,
|
313 | };
|
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 |
|
327 | }
|
328 | |
329 |
|
330 |
|
331 |
|
332 |
|
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 |
|
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 |
|
360 | }
|
361 | }
|
362 | }
|
363 |
|
364 | class 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();
|
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 |
|
406 |
|
407 | if (this.token) {
|
408 | this.ioService.token(this.token);
|
409 | yield this.ioService.start();
|
410 | }
|
411 |
|
412 | });
|
413 | }
|
414 | ngOnDestroy() {
|
415 | this.log.verbose('WechatyComponent', 'ngOnDestroy()');
|
416 | this.endTimer();
|
417 | if (this.ioService) {
|
418 | this.ioService.stop();
|
419 |
|
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 |
|
497 |
|
498 | this.ngZone.runOutsideAngular(() => {
|
499 | this.timer = interval(3000).pipe(tap(i => { this.log.verbose('do', ' %d', i); }), takeUntil(this.ender), share());
|
500 |
|
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 |
|
509 | });
|
510 | }
|
511 | endTimer() {
|
512 | this.log.verbose('WechatyComponent', 'endTimer()');
|
513 | if (this.timerSub) {
|
514 | this.timerSub.unsubscribe();
|
515 | this.timerSub = null;
|
516 | }
|
517 |
|
518 | if (this.ender) {
|
519 | this.ender.next(null);
|
520 |
|
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 | }
|
535 | WechatyComponent.decorators = [
|
536 | { type: Component, args: [{
|
537 |
|
538 | selector: 'wechaty',
|
539 | |
540 |
|
541 |
|
542 |
|
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 | template: '<ng-content></ng-content>'
|
550 | },] }
|
551 | ];
|
552 | WechatyComponent.ctorParameters = () => [
|
553 | { type: Brolog },
|
554 | { type: NgZone }
|
555 | ];
|
556 | WechatyComponent.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 |
|
566 | class WechatyModule {
|
567 | }
|
568 | WechatyModule.decorators = [
|
569 | { type: NgModule, args: [{
|
570 | id: 'wechaty',
|
571 | declarations: [
|
572 | WechatyComponent,
|
573 | ],
|
574 | exports: [
|
575 | WechatyComponent,
|
576 | ],
|
577 | },] }
|
578 | ];
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 | export { VERSION, WechatyComponent, WechatyModule, WechatyComponent as ɵa };
|
585 |
|