import { tsInterface } from '../interface/entry' import { tsTime } from '../time/time' import { tsString } from '../utils/string' import { tsEvent } from '../event/event' export enum WSState { CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3 } export enum WSEventType { Connect_Error = 'Connect_Error', Connect_Open = 'Connect_Open', Connect_Close = 'Connect_Close' } export enum WSCloseCode { CloseCode_Reconnect = 4998, CloseCode_Shutdown = 4999 } export type WSOptions = { connCtor?: (url: string) => tsInterface.IWebSocket pingHandler: (conn: WSConn) => void onMessageHandler: (conn: WSConn, data: any) => void binaryType: string pingInterval: number reconnectLimit: number } export class WSConn extends tsEvent.EventEntry { private url: string = '' private closeCode: number = 0 private conn: tsInterface.IWebSocket = null private options: WSOptions = null private pingTimer: tsTime.Timer = null private lastSendTime: number = 0 private reconnectTimer: tsTime.Timer = null private onOpenCount: number = 0 constructor( id: number, name: string, options: WSOptions = { connCtor: null, pingHandler: null, onMessageHandler: null, binaryType: 'arraybuffer', pingInterval: 10, reconnectLimit: 0 } ) { super(id, name) this.entryName = 'WSConn' this.options = options this.debugf('constructor options:{0}', options) } private handlePing(): void { if (tsTime.unixSec() - this.lastSendTime < this.options.pingInterval) { return } if (this.options.pingHandler) { this.options.pingHandler(this) } } stopReconnect(): void { if (!this.reconnectTimer) { return } this.reconnectTimer.stop() this.reconnectTimer = null } private checkReconnect(): void { if ((this.closeCode > 0 && this.closeCode != WSCloseCode.CloseCode_Reconnect) || this.options.reconnectLimit == 0 || this.reconnectTimer) { this.errorf( 'checkReconnect closeCode:{0} isClosed:{1} isCloseing:{2} isConnected:{3} isConnecting:{4}', this.closeCode, this.isClosed(), this.isCloseing(), this.isConnected(), this.isConnecting() ) return } this.reconnectTimer = tsTime.addTimer( this.fullName + '_reconnect', 1, (timer: tsTime.Timer, caller: WSConn): void => { if (!caller.isClosed()) { caller.stopReconnect() } else { caller.connect('', true) } }, this, -1, true ) } private handleCloseAndError(result: any, conn: tsInterface.IWebSocket): void { if (!this.conn || conn != this.conn) { this.error('onError err:conn is other') return } this.setConn(null) if (this.closeCode != WSCloseCode.CloseCode_Reconnect) { this.dispatchEvent(WSEventType.Connect_Close, result) } this.checkReconnect() } private onError(result: any, conn: tsInterface.IWebSocket) { this.errorf('onError url:{0}', this.url) this.handleCloseAndError(result, conn) } private onClose(result: any, conn: tsInterface.IWebSocket) { this.debug('onClose res:', result) this.handleCloseAndError(result, conn) } private onOpen(result: any) { this.onOpenCount++ this.debug('onOpen count:', this.onOpenCount) if (this.options.pingInterval > 0 && !this.pingTimer) { this.pingTimer = tsTime.addTimer( this.fullName + '_ping', this.options.pingInterval, (timer: tsTime.Timer, caller: WSConn): void => { caller.handlePing() }, this, -1, true ) } this.lastSendTime = tsTime.unixSec() this.dispatchEvent(WSEventType.Connect_Open, result) this.stopReconnect() } private onMessage(result: any) { this.trace('recv res:', result) if (this.options.onMessageHandler) { this.options.onMessageHandler(this, result.data) } } private setConn(conn: tsInterface.IWebSocket): void { this.conn = conn if (conn) { let self = this conn.onerror = (ev: any) => { self.onError(ev, conn) } conn.onopen = (ev: any) => { self.onOpen(ev) } conn.onclose = (ev: any) => { self.onClose(ev, conn) } conn.onmessage = (ev: any) => { self.onMessage(ev) } } else { if (this.pingTimer) { this.pingTimer.stop() this.pingTimer = null } } } connect(url: string, isReconnect: boolean = false): void { this.debugf('connect isReconnect:{0}', isReconnect) if (tsString.isNullOrEmpty(url)) { url = this.url } this.closeCode = 0 if (this.conn) { this.error('connect err:conn is busy') return } if (!isReconnect && this.reconnectTimer) { this.error('connect err:conn in reconnecting') return } if (isReconnect && this.onOpenCount > 0) { url += '&reconnect=true' } this.debugf('connect {0}', url) let conn: tsInterface.IWebSocket = null if (!this.options.connCtor) { conn = new WebSocket(url) conn.binaryType = this.options.binaryType } else { conn = this.options.connCtor(url) } this.setConn(conn) } reconnect(url: string, tag?: string): void { this.onOpenCount = 0 this.url = url this.debugf('reconnect url:{0} tag:{1}', url, tag) if (this.isConnecting()) { this.errorf('reconnect err:conn in connecting tag:{0}', tag) this.close(WSCloseCode.CloseCode_Reconnect, '客户端重连关闭') this.handleCloseAndError({}, this.conn) return } if (this.isCloseing()) { this.errorf('reconnect err:conn in closeing tag:{0}', tag) this.close(WSCloseCode.CloseCode_Reconnect, '客户端重连关闭') this.handleCloseAndError({}, this.conn) return } if (this.conn) { this.close(WSCloseCode.CloseCode_Reconnect, '客户端重连关闭') } else { this.connect(url) } } isConnecting(): boolean { if (this.conn && this.conn.readyState == WSState.CONNECTING) { return true } return false } isConnected(): boolean { if (this.conn && this.conn.readyState == WSState.OPEN) { return true } return false } isCloseing(): boolean { if (this.conn && this.conn.readyState == WSState.CLOSING) { return true } return false } isClosed(): boolean { if (!this.conn || this.conn.readyState == WSState.CLOSED) { return true } return false } close(code: number = WSCloseCode.CloseCode_Shutdown, reason: string = '客户端主动关闭'): boolean { if (!this.isConnected()) { return false } this.debugf('close code:{0} reason:{1}', code, reason) this.closeCode = code this.conn.close(code, reason) return true } send(data: any): void { if (!this.isConnected()) { this.errorf( 'send err:conn not connected closeCode:{0} isClosed:{1} isCloseing:{2} isConnected:{3} isConnecting:{4}', this.closeCode, this.isClosed(), this.isCloseing(), this.isConnected(), this.isConnecting() ) return } this.trace('send data:{0}', data) this.lastSendTime = tsTime.unixSec() this.conn.send(data) } }