UNPKG

@chatie/angular

Version:

Wechaty Component NgModule

586 lines (579 loc) 21.4 kB
import { __awaiter } from 'tslib'; import { EventEmitter, Component, NgZone, Output, Input, NgModule } from '@angular/core'; import { BehaviorSubject, Observable, Subject, interval } from 'rxjs'; import { filter, share, tap, takeUntil } from 'rxjs/operators'; import { Brolog } from 'brolog'; import { StateSwitch } from 'state-switch'; /** * This file was auto generated from scripts/generate-version.sh */ const VERSION = '0.8.3'; var ReadyState; (function (ReadyState) { ReadyState[ReadyState["CLOSED"] = WebSocket.CLOSED] = "CLOSED"; ReadyState[ReadyState["CLOSING"] = WebSocket.CLOSING] = "CLOSING"; ReadyState[ReadyState["CONNECTING"] = WebSocket.CONNECTING] = "CONNECTING"; ReadyState[ReadyState["OPEN"] = WebSocket.OPEN] = "OPEN"; })(ReadyState || (ReadyState = {})); class IoService { constructor() { this.autoReconnect = true; this.log = Brolog.instance(); this.CONNECT_TIMEOUT = 10 * 1000; // 10 seconds this.ENDPOINT = 'wss://api.chatie.io/v0/websocket/token/'; this.PROTOCOL = 'web|0.0.1'; this.sendBuffer = []; this.log.verbose('IoService', 'constructor()'); } get readyState() { return this._readyState.asObservable(); } init() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'init()'); if (this.state) { throw new Error('re-init'); } this.snapshot = { readyState: ReadyState.CLOSED, event: null, }; this._readyState = new BehaviorSubject(ReadyState.CLOSED); this.state = new StateSwitch('IoService', this.log); this.state.setLog(this.log); try { yield this.initStateDealer(); yield this.initRxSocket(); } catch (e) { this.log.silly('IoService', 'init() exception: %s', e.message); throw e; } this.readyState.subscribe(s => { this.log.silly('IoService', 'init() readyState.subscribe(%s)', ReadyState[s]); this.snapshot.readyState = s; }); // IMPORTANT: subscribe to event and make it HOT! this.event.subscribe(s => { this.log.silly('IoService', 'init() event.subscribe({name:%s})', s.name); this.snapshot.event = s; }); return; }); } token(newToken) { this.log.silly('IoService', 'token(%s)', newToken); if (newToken) { this._token = newToken; return; } return this._token; } start() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'start() with token:%s', this._token); if (!this._token) { throw new Error('start() without token'); } if (this.state.on()) { throw new Error('state is already ON'); } if (this.state.pending()) { throw new Error('state is pending'); } this.state.on('pending'); this.autoReconnect = true; try { yield this.connectRxSocket(); this.state.on(true); } catch (e) { this.log.warn('IoService', 'start() failed:%s', e.message); this.state.off(true); } }); } stop() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'stop()'); if (this.state.off()) { this.log.warn('IoService', 'stop() state is already off'); if (this.state.pending()) { throw new Error('state pending() is true'); } return; } this.state.off('pending'); this.autoReconnect = false; if (!this._websocket) { throw new Error('no websocket'); } yield this.socketClose(1000, 'IoService.stop()'); this.state.off(true); return; }); } restart() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'restart()'); try { yield this.stop(); yield this.start(); } catch (e) { this.log.error('IoService', 'restart() error:%s', e.message); throw e; } return; }); } initStateDealer() { this.log.verbose('IoService', 'initStateDealer()'); const isReadyStateOpen = (s) => s === ReadyState.OPEN; this.readyState.pipe(filter(isReadyStateOpen)) .subscribe(open => this.stateOnOpen()); } /** * Creates a subject from the specified observer and observable. * - https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md * Create an Rx.Subject using Subject.create that allows onNext without subscription * A socket implementation (example, don't use) * - http://stackoverflow.com/a/34862286/1123955 */ initRxSocket() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'initRxSocket()'); if (this.event) { throw new Error('re-init is not permitted'); } // 1. Mobile Originated. moObserver.next() means mobile is sending this.moObserver = { next: this.socketSend.bind(this), error: this.socketClose.bind(this), complete: this.socketClose.bind(this), }; // 2. Mobile Terminated. mtObserver.next() means mobile is receiving const observable = new Observable((observer) => { this.log.verbose('IoService', 'initRxSocket() Observable.create()'); this.mtObserver = observer; return this.socketClose.bind(this); }); // 3. Subject for MO & MT Observers this.event = Subject.create(this.moObserver, observable.pipe(share())); }); } connectRxSocket() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'connectRxSocket()'); // FIXME: check & close the old one if (this._websocket) { throw new Error('already has a websocket'); } // if (this.state.target() !== 'open' // || this.state.current() !== 'open' // || this.state.stable() if (this.state.off()) { throw new Error('switch state is off'); } else if (!this.state.pending()) { throw new Error('switch state is already ON'); } this._websocket = new WebSocket(this.endPoint(), this.PROTOCOL); this.socketUpdateState(); const onOpenPromise = new Promise((resolve, reject) => { this.log.verbose('IoService', 'connectRxSocket() Promise()'); const id = setTimeout(() => { this._websocket = null; const e = new Error('rxSocket connect timeout after ' + Math.round(this.CONNECT_TIMEOUT / 1000)); reject(e); }, this.CONNECT_TIMEOUT); // timeout for connect websocket this._websocket.onopen = (e) => { this.log.verbose('IoService', 'connectRxSocket() Promise() WebSocket.onOpen() resolve()'); this.socketUpdateState(); clearTimeout(id); resolve(); }; }); // Handle the payload this._websocket.onmessage = this.socketOnMessage.bind(this); // Deal the event this._websocket.onerror = this.socketOnError.bind(this); this._websocket.onclose = this.socketOnClose.bind(this); return onOpenPromise; }); } endPoint() { const url = this.ENDPOINT + this._token; this.log.verbose('IoService', 'endPoint() => %s', url); return url; } /****************************************************************** * * State Event Listeners * */ stateOnOpen() { this.log.verbose('IoService', 'stateOnOpen()'); this.socketSendBuffer(); this.rpcUpdate('from stateOnOpen()'); } /****************************************************************** * * Io RPC Methods * */ rpcDing(payload) { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'ding(%s)', payload); const e = { name: 'ding', payload, }; this.event.next(e); // TODO: get the return value }); } rpcUpdate(payload) { return __awaiter(this, void 0, void 0, function* () { this.event.next({ name: 'update', payload, }); }); } /****************************************************************** * * Socket Actions * */ socketClose(code, reason) { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('IoService', 'socketClose()'); if (!this._websocket) { throw new Error('no websocket'); } this._websocket.close(code, reason); this.socketUpdateState(); const future = new Promise(resolve => { this.readyState.pipe(filter(s => s === ReadyState.CLOSED)) .subscribe(resolve); }); yield future; return; }); } socketSend(ioEvent) { this.log.silly('IoService', 'socketSend({name:%s, payload:%s})', ioEvent.name, ioEvent.payload); if (!this._websocket) { this.log.silly('IoService', 'socketSend() no _websocket'); } const strEvt = JSON.stringify(ioEvent); this.sendBuffer.push(strEvt); // XXX can move this to onOpen? this.socketSendBuffer(); } socketSendBuffer() { this.log.silly('IoService', 'socketSendBuffer() length:%s', this.sendBuffer.length); if (!this._websocket) { throw new Error('socketSendBuffer(): no _websocket'); } if (this._websocket.readyState !== WebSocket.OPEN) { this.log.warn('IoService', 'socketSendBuffer() readyState is not OPEN, send job delayed.'); return; } while (this.sendBuffer.length) { const buf = this.sendBuffer.shift(); this.log.silly('IoService', 'socketSendBuffer() sending(%s)', buf); this._websocket.send(buf); } } socketUpdateState() { var _a; this.log.verbose('IoService', 'socketUpdateState() is %s', ReadyState[(_a = this._websocket) === null || _a === void 0 ? void 0 : _a.readyState]); if (!this._websocket) { this.log.error('IoService', 'socketUpdateState() no _websocket'); return; } this._readyState.next(this._websocket.readyState); } /****************************************************************** * * Socket Events Listener * */ socketOnMessage(message) { this.log.verbose('IoService', 'onMessage({data: %s})', message.data); const data = message.data; // WebSocket data const ioEvent = { name: 'raw', payload: data, }; // this is default io event for unknown format message try { const obj = JSON.parse(data); ioEvent.name = obj.name; ioEvent.payload = obj.payload; } catch (e) { this.log.warn('IoService', 'onMessage parse message fail. save as RAW'); } this.mtObserver.next(ioEvent); } socketOnError(event) { this.log.silly('IoService', 'socketOnError(%s)', event); // this._websocket = null } /** * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent * code: 1006 CLOSE_ABNORMAL * - Reserved. Used to indicate that a connection was closed abnormally * (that is, with no close frame being sent) when a status code is expected. */ socketOnClose(closeEvent) { this.log.verbose('IoService', 'socketOnClose({code:%s, reason:%s, returnValue:%s})', closeEvent.code, closeEvent.reason, closeEvent.returnValue); this.socketUpdateState(); /** * reconnect inside onClose */ if (this.autoReconnect) { this.state.on('pending'); setTimeout(() => __awaiter(this, void 0, void 0, function* () { try { yield this.connectRxSocket(); this.state.on(true); } catch (e) { this.log.warn('IoService', 'socketOnClose() autoReconnect() exception: %s', e); this.state.off(true); } }), 1000); } else { this.state.off(true); } this._websocket = null; if (!closeEvent.wasClean) { this.log.warn('IoService', 'socketOnClose() event.wasClean FALSE'); // TODO emit error } } } class WechatyComponent { constructor(log, ngZone) { this.log = log; this.ngZone = ngZone; this.message = new EventEmitter(); this.scan = new EventEmitter(); this.login = new EventEmitter(); this.logout = new EventEmitter(); this.error = new EventEmitter(); this.heartbeat = new EventEmitter(); this.timerSub = null; this.counter = 0; this.timestamp = new Date(); this.log.verbose('WechatyComponent', 'constructor() v%s', VERSION); } get token() { return this._token; } set token(_newToken) { this.log.verbose('WechatyComponent', 'set token(%s)', _newToken); const newToken = (_newToken || '').trim(); if (this._token === newToken) { this.log.silly('WechatyComponent', 'set token(%s) not new', newToken); return; } this._token = newToken; if (!this.ioService) { this.log.silly('WechatyComponent', 'set token() skip token init value'); this.log.silly('WechatyComponent', 'set token() because ioService will do it inside ngOnInit()'); return; } this.log.silly('WechatyComponent', 'set token(%s) reloading ioService now...', newToken); this.ioService.token(this.token); this.ioService.restart(); // async } ngOnInit() { return __awaiter(this, void 0, void 0, function* () { this.log.verbose('WechatyComponent', 'ngOnInit() with token: ' + this.token); this.ioService = new IoService(); yield this.ioService.init(); this.ioService.event.subscribe(this.onIo.bind(this)); this.log.silly('WechatyComponent', 'ngOnInit() ioService.event.subscribe()-ed'); /** * @Input(token) might not initialized in constructor() */ if (this.token) { this.ioService.token(this.token); yield this.ioService.start(); } // this.startTimer() }); } ngOnDestroy() { this.log.verbose('WechatyComponent', 'ngOnDestroy()'); this.endTimer(); if (this.ioService) { this.ioService.stop(); // this.ioService = null } } onIo(e) { this.log.silly('WechatyComponent', 'onIo#%d(%s)', this.counter++, e.name); this.timestamp = new Date(); switch (e.name) { case 'scan': this.scan.emit(e.payload); break; case 'login': this.login.emit(e.payload); break; case 'logout': this.logout.emit(e.payload); break; case 'message': this.message.emit(e.payload); break; case 'error': this.error.emit(e.payload); break; case 'ding': case 'dong': case 'raw': this.heartbeat.emit(e.name + '[' + e.payload + ']'); break; case 'heartbeat': this.heartbeat.emit(e.payload); break; case 'sys': this.log.silly('WechatyComponent', 'onIo(%s): %s', e.name, e.payload); break; default: this.log.warn('WechatyComponent', 'onIo() unknown event name: %s[%s]', e.name, e.payload); break; } } reset(reason) { this.log.verbose('WechatyComponent', 'reset(%s)', reason); const resetEvent = { name: 'reset', payload: reason, }; if (!this.ioService) { throw new Error('no ioService'); } this.ioService.event.next(resetEvent); } shutdown(reason) { this.log.verbose('WechatyComponent', 'shutdown(%s)', reason); const shutdownEvent = { name: 'shutdown', payload: reason, }; if (!this.ioService) { throw new Error('no ioService'); } this.ioService.event.next(shutdownEvent); } startSyncMessage() { this.log.verbose('WechatyComponent', 'startSyncMessage()'); const botieEvent = { name: 'botie', payload: { args: ['message'], source: 'return this.syncMessage(message)', }, }; if (!this.ioService) { throw new Error('no ioService'); } this.ioService.event.next(botieEvent); } startTimer() { this.log.verbose('WechatyComponent', 'startTimer()'); this.ender = new Subject(); // https://github.com/angular/protractor/issues/3349#issuecomment-232253059 // https://github.com/juliemr/ngconf-2016-zones/blob/master/src/app/main.ts#L38 this.ngZone.runOutsideAngular(() => { this.timer = interval(3000).pipe(tap(i => { this.log.verbose('do', ' %d', i); }), takeUntil(this.ender), share()); // .publish() }); this.timerSub = this.timer.subscribe(t => { this.counter = t; if (!this.ioService) { throw new Error('no ioService'); } this.ioService.rpcDing(this.counter); // this.message.emit('#' + this.token + ':' + dong) }); } endTimer() { this.log.verbose('WechatyComponent', 'endTimer()'); if (this.timerSub) { this.timerSub.unsubscribe(); this.timerSub = null; } // this.timer = null if (this.ender) { this.ender.next(null); // this.ender = null } } logoff(reason) { this.log.silly('WechatyComponent', 'logoff(%s)', reason); const quitEvent = { name: 'logout', payload: reason, }; this.ioService.event.next(quitEvent); } get readyState() { return this.ioService.readyState; } } WechatyComponent.decorators = [ { type: Component, args: [{ // tslint:disable-next-line:component-selector selector: 'wechaty', /** * http://localhost:4200/app.component.html 404 (Not Found) * zone.js:344 Unhandled Promise rejection: Failed to load app.component.html * https://github.com/angular/angular-cli/issues/2592#issuecomment-266635266 * https://github.com/angular/angular-cli/issues/2293 * * console.log from angular: * If you're using Webpack you should inline the template and the styles, * see https://goo.gl/X2J8zc. */ template: '<ng-content></ng-content>' },] } ]; WechatyComponent.ctorParameters = () => [ { type: Brolog }, { type: NgZone } ]; WechatyComponent.propDecorators = { message: [{ type: Output }], scan: [{ type: Output }], login: [{ type: Output }], logout: [{ type: Output }], error: [{ type: Output }], heartbeat: [{ type: Output }], token: [{ type: Input }] }; class WechatyModule { } WechatyModule.decorators = [ { type: NgModule, args: [{ id: 'wechaty', declarations: [ WechatyComponent, ], exports: [ WechatyComponent, ], },] } ]; /** * Generated bundle index. Do not edit. */ export { VERSION, WechatyComponent, WechatyModule, WechatyComponent as ɵa }; //# sourceMappingURL=chatie-angular.js.map