import { w3cwebsocket } from 'websocket';
import { IWebSocketClient } from './types';
import { randomUUID } from 'crypto';
import { EventEmitter } from 'stream';
import { pack } from '@/utils/transfer';
import debug from 'debug';
import * as CryptoJS from 'crypto-js';
import { exit } from 'process';
const dMsg = debug('client');
interface EventData<T = any> {
requestToken?: string;
eventType: string;
result: T;
}
type Fn = (...args: any[]) => any;
export class WebSocketClient extends w3cwebsocket implements IWebSocketClient {
public evCount = new Map<string, number>();
public evPool: EventEmitter;
public algo: 'aes' | 'hmac';
public secret: string = '';
public timeoutMilli: number = 3000;
public hstSize: number = 30;
constructor(url: string, algo: 'aes' | 'hmac') {
super(url);
this.evPool = new EventEmitter();
this.algo = algo;
this.onopen = () => {
dMsg('CLIENT CONNECTED!');
this.send(this.algo);
};
this.onclose = () => {
dMsg('BYE BYE');
exit(0);
};
this.initial();
}
dispatch(eventType: string, payload?: any): void {
this.evPool.emit(eventType, payload);
}
on(eventType: string, listener: Fn): number {
let currentCount = this.evCount.get(eventType) ?? 0;
this.evPool.on(eventType, listener);
this.evCount.set(eventType, ++currentCount);
return currentCount;
}
once(eventType: string, listener: Fn): number {
let currentCount = this.evCount.get(eventType) ?? 0;
this.evPool.once(eventType, (...args: any[]) => {
listener(...args);
const count = this.evCount.get(eventType) ?? 0;
if (count) this.evCount.set(eventType, count - 1);
});
this.evCount.set(eventType, ++currentCount);
return currentCount;
}
off(eventType: string): boolean {
const containEvent = this.evPool.eventNames().includes(eventType);
this.evPool.removeAllListeners(eventType);
return containEvent;
}
async ready() {
return new Promise<string>((resolve) => {
if (this.secret !== '') return resolve(this.secret);
this.onmessage = ({ data }) => resolve(data.toString());
});
}
async initial() {
this.ready().then((secret) => {
this.secret = secret;
this.onmessage = ({ data }) => {
dMsg("Receive Raw'%s'", data);
try {
const { requestToken = '', eventType, result } =
JSON.parse(data.toString()) as EventData;
dMsg('Resolve: %O', {
requestToken,
eventType,
result,
});
if (requestToken !== '') {
this.dispatch(requestToken, result);
} else {
this.dispatch(eventType, result);
}
} catch (e) {
dMsg('ERROR_REASON: ', (e as Error).message);
}
};
});
}
async sendout(payload: any, requestToken: string = '$NONE') {
await this.ready();
const signature =
this.algo === 'hmac'
? CryptoJS.HmacSHA1(requestToken, this.secret).toString()
: CryptoJS.AES.encrypt(requestToken, this.secret).toString();
this.send(pack({ requestToken, signature, payload }));
}
async request<T = any>(eventType: string, params?: any): Promise<T> {
const requestToken = `request::${randomUUID()}`;
this.sendout({ eventType, params }, requestToken);
return new Promise((resolve, failed) => {
const timer = setTimeout(() => {
this.off(requestToken);
resolve('TIMEOUT' as any);
}, this.timeoutMilli);
this.once(requestToken, (data) => {
clearTimeout(timer);
resolve(data);
});
});
}
history() {
return [];
}
}