@chatie/angular
Version:
Wechaty Component NgModule
586 lines (579 loc) • 21.4 kB
JavaScript
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