import { Backoff } from "./backoff/backoff" ; import { Buffer } from "./buffer/buffer" ; type eventListener < K extends WebsocketEvents > = { readonly listener : ( instance : Websocket , ev : WebsocketEventMap [ K ] ) => any ; readonly options ? : boolean | EventListenerOptions ; } ; export enum WebsocketEvents { open = 'open' , close = 'close' , error = 'error' , message = 'message' , retry = 'retry' } interface WebsocketEventMap { close : CloseEvent ; error : Event ; message : MessageEvent ; open : Event ; retry : CustomEvent < RetryEventDetails > ; } export interface RetryEventDetails { readonly retries : number ; readonly backoff : number ; } type WebsocketEventListeners = { open : eventListener < WebsocketEvents . open > [ ] ; close : eventListener < WebsocketEvents . close > [ ] ; error : eventListener < WebsocketEvents . error > [ ] ; message : eventListener < WebsocketEvents . message > [ ] ; retry : eventListener < WebsocketEvents . retry > [ ] ; } ; type WebsocketBuffer = Buffer < string | ArrayBufferLike | Blob | ArrayBufferView > ; export class Websocket { private readonly url : string ; private readonly protocols ? : string | string [ ] ; private readonly buffer ? : WebsocketBuffer ; private readonly backoff ? : Backoff ; private readonly eventListeners : WebsocketEventListeners = { open : [ ] , close : [ ] , error : [ ] , message : [ ] , retry : [ ] } ; private closedByUser : boolean = false ; private websocket ? : WebSocket ; private retries : number = 0 ; constructor ( url , protocols ? , buffer ? , backoff ? ) { this . url = url ; this . protocols = protocols ; this . buffer = buffer ; this . backoff = backoff ; this . tryConnect ( ) ; } get underlyingWebsocket ( ) : WebSocket | undefined { return this . websocket ; } public send ( data ) { if ( this . closedByUser ) return ; if ( this . websocket === undefined || this . websocket . readyState !== this . websocket . OPEN ) this . buffer ?. write ( [ data ] ) ; else this . websocket . send ( data ) ; } public close ( code ? , reason ? ) { this . closedByUser = true ; this . websocket ?. close ( code , reason ) ; } public addEventListener < K extends WebsocketEvents > ( type , listener , options ? ) { const eventListener = { listener , options } as eventListener < K > ; const eventListeners = this . eventListeners [ type ] as eventListener < K > [ ] ; eventListeners . push ( eventListener ) ; } public removeEventListener < K extends WebsocketEvents > ( type , listener , options ? ) { ( this . eventListeners [ type ] as eventListener < K > [ ] ) = ( this . eventListeners [ type ] as eventListener < K > [ ] ) . filter ( l => { return l . listener !== listener && ( l . options === undefined || l . options !== options ) ; } ) ; } private dispatchEvent < K extends WebsocketEvents > ( type , ev ) { const listeners = this . eventListeners [ type ] as eventListener < K > [ ] ; const onceListeners = [ ] as eventListener < K > [ ] ; listeners . forEach ( l => { l . listener ( this , ev ) ; if ( l . options !== undefined && ( l . options as AddEventListenerOptions ) . once ) onceListeners . push ( l ) ; } ) ; onceListeners . forEach ( l => this . removeEventListener ( type , l . listener , l . options ) ) ; } private tryConnect ( ) { if ( this . websocket !== undefined ) { this . websocket . removeEventListener ( WebsocketEvents . open , this . handleOpenEvent ) ; this . websocket . removeEventListener ( WebsocketEvents . close , this . handleCloseEvent ) ; this . websocket . removeEventListener ( WebsocketEvents . error , this . handleErrorEvent ) ; this . websocket . removeEventListener ( WebsocketEvents . message , this . handleMessageEvent ) ; this . websocket . close ( ) ; } this . websocket = new WebSocket ( this . url , this . protocols ) ; this . websocket . addEventListener ( WebsocketEvents . open , this . handleOpenEvent ) ; this . websocket . addEventListener ( WebsocketEvents . close , this . handleCloseEvent ) ; this . websocket . addEventListener ( WebsocketEvents . error , this . handleErrorEvent ) ; this . websocket . addEventListener ( WebsocketEvents . message , this . handleMessageEvent ) ; } private handleOpenEvent = ( ev ) => this . handleEvent ( WebsocketEvents . open , ev ) ; private handleCloseEvent = ( ev ) => this . handleEvent ( WebsocketEvents . close , ev ) ; private handleErrorEvent = ( ev ) => this . handleEvent ( WebsocketEvents . error , ev ) ; private handleMessageEvent = ( ev ) => this . handleEvent ( WebsocketEvents . message , ev ) ; private handleEvent < K extends WebsocketEvents > ( type , ev ) { switch ( type ) { case WebsocketEvents . close : if ( ! this . closedByUser ) this . reconnect ( ) ; break ; case WebsocketEvents . open : this . retries = 0 ; this . backoff ?. reset ( ) ; this . buffer ?. forEach ( this . send . bind ( this ) ) ; this . buffer ?. clear ( ) ; break ; } this . dispatchEvent < K > ( type , ev ) ; } private reconnect ( ) { if ( this . backoff === undefined ) return ; const backoff = this . backoff . next ( ) ; setTimeout ( ( ) => { this . dispatchEvent ( WebsocketEvents . retry , new CustomEvent < RetryEventDetails > ( WebsocketEvents . retry , { detail : { retries : ++ this . retries , backoff : backoff } } ) ) ; this . tryConnect ( ) ; } , backoff ) ; } }