@dxfeed/dxlink-websocket-client
Version:
dxLink WebSocket Client allows to connect the remote dxLink WebSocket endpoint
3 lines (2 loc) • 14.5 kB
JavaScript
import{DXLinkChannelState,Logger,Scheduler,DXLinkConnectionState,DXLinkAuthState,DXLinkLogLevel}from"@dxfeed/dxlink-core";class DXLinkWebSocketChannel{constructor(id,service,parameters,sendMessage,config){this.id=void 0,this.service=void 0,this.parameters=void 0,this.sendMessage=void 0,this.status=DXLinkChannelState.REQUESTED,this.messageListeners=new Set,this.statusListeners=new Set,this.errorListeners=new Set,this.logger=void 0,this.send=({type:type,...payload})=>{if(this.status!==DXLinkChannelState.OPENED)throw new Error("Channel is not ready");this.sendMessage({type:type,channel:this.id,...payload})},this.addMessageListener=listener=>this.messageListeners.add(listener),this.removeMessageListener=listener=>this.messageListeners.delete(listener),this.getState=()=>this.status,this.addStateChangeListener=listener=>this.statusListeners.add(listener),this.removeStateChangeListener=listener=>this.statusListeners.delete(listener),this.addErrorListener=listener=>this.errorListeners.add(listener),this.removeErrorListener=listener=>this.errorListeners.delete(listener),this.error=({type:type,message:message})=>this.send({type:"ERROR",error:type,message:message}),this.close=()=>{this.status!==DXLinkChannelState.CLOSED&&(this.logger.debug("Closing by user"),this.setStatus(DXLinkChannelState.CLOSED),this.clear(),this.sendMessage({type:"CHANNEL_CANCEL",channel:this.id}))},this.request=()=>{this.logger.debug("Requesting"),this.sendMessage({type:"CHANNEL_REQUEST",channel:this.id,service:this.service,parameters:this.parameters}),this.processStatusRequested()},this.processPayloadMessage=message=>{for(const listener of this.messageListeners)listener(message)},this.processStatusOpened=()=>{this.logger.debug("Opened"),this.setStatus(DXLinkChannelState.OPENED)},this.processStatusRequested=()=>{this.setStatus(DXLinkChannelState.REQUESTED)},this.processStatusClosed=()=>{this.logger.debug("Closed by remote endpoint"),this.setStatus(DXLinkChannelState.CLOSED),this.clear()},this.processError=error=>{if(0!==this.errorListeners.size)for(const listener of this.errorListeners)try{listener(error)}catch(e){this.logger.error(`Error in channel#${this.id} error listener: `,e)}else this.logger.error(`Unhandled error in channel#${this.id}: `,error)},this.setStatus=newStatus=>{if(this.status===newStatus)return;const prev=this.status;this.status=newStatus;for(const listener of this.statusListeners)try{listener(newStatus,prev)}catch(e){this.logger.error(`Error in channel#${this.id} status listener: `,e)}},this.clear=()=>{this.messageListeners.clear(),this.statusListeners.clear()},this.id=id,this.service=service,this.parameters=parameters,this.sendMessage=sendMessage,this.logger=new Logger(`${DXLinkWebSocketChannel.name}#${id} ${service}`,config.logLevel)}}class DefaultDXLinkWebSocketConnector{constructor(url,protocols){this.url=void 0,this.protocols=void 0,this.socket=void 0,this.isAvailable=!1,this.openListener=void 0,this.closeListener=void 0,this.messageListener=void 0,this.sendMessage=message=>{void 0!==this.socket&&this.isAvailable&&this.socket.send(JSON.stringify(message))},this.setOpenListener=listener=>{this.openListener=listener},this.setCloseListener=listener=>{this.closeListener=listener},this.setMessageListener=listener=>{this.messageListener=listener},this.getUrl=()=>this.url,this.handleOpen=()=>{void 0!==this.socket&&(this.isAvailable=!0,this.socket.removeEventListener("open",this.handleOpen),this.socket.addEventListener("message",this.handleMessage),this.socket.addEventListener("close",this.handleClosed),this.openListener?.())},this.handleClosed=ev=>{void 0!==this.socket&&(this.stop(),this.closeListener?.(ev.reason,!1))},this.handleError=_ev=>{void 0!==this.socket&&(this.stop(),this.closeListener?.("Unable to connect",!0))},this.handleMessage=ev=>{try{const message=JSON.parse(ev.data);if("object"!=typeof message)throw new Error("Unexpected message: "+typeof message);if("string"!=typeof message.type)throw new Error("Unexpected message type: "+typeof message.type);if("number"!=typeof message.channel)throw new Error("Unexpected message channel: "+typeof message.channel);if(void 0===this.messageListener)return console.warn("No message listener set");this.messageListener(message)}catch(error){console.error(error instanceof Error?error:new Error("Parsing error:"+String(error)))}},this.url=url,this.protocols=protocols}start(){void 0===this.socket&&(this.socket=new WebSocket(this.url,this.protocols),this.socket.addEventListener("open",this.handleOpen),this.socket.addEventListener("error",this.handleError),this.socket.addEventListener("close",this.handleClosed))}stop(){void 0!==this.socket&&(this.socket.removeEventListener("open",this.handleOpen),this.socket.removeEventListener("error",this.handleError),this.socket.removeEventListener("close",this.handleClosed),this.socket.removeEventListener("message",this.handleMessage),this.socket.close(),this.socket=void 0,this.isAvailable=!1)}}const DXLINK_WS_PROTOCOL_VERSION="0.1",DEFAULT_CONNECTION_DETAILS={protocolVersion:"0.1",clientVersion:"DXF-JS/0.5.0"};class DXLinkWebSocketClient{constructor(config){this.config=void 0,this.logger=void 0,this.scheduler=new Scheduler,this.connector=void 0,this.connectionState=DXLinkConnectionState.NOT_CONNECTED,this.connectionDetails=DEFAULT_CONNECTION_DETAILS,this.authState=DXLinkAuthState.UNAUTHORIZED,this.connectionStateChangeListeners=new Set,this.errorListeners=new Set,this.authStateChangeListeners=new Set,this.isFirstAuthState=!0,this.lastSettedAuthToken=void 0,this.lastReceivedMillis=0,this.lastSentMillis=0,this.reconnectAttempts=0,this.globalChannelId=1,this.channels=new Map,this.connect=url=>{this.connector?.getUrl()!==url&&(this.disconnect(),this.logger.debug("Connecting to",url),this.setConnectionState(DXLinkConnectionState.CONNECTING),this.connector=this.config.connectorFactory(url),this.connector.setOpenListener(this.processTransportOpen),this.connector.setMessageListener(this.processMessage),this.connector.setCloseListener(this.processTransportClose),this.connector.start())},this.reconnect=()=>{if(this.config.maxReconnectAttempts>=0&&this.reconnectAttempts>=this.config.maxReconnectAttempts)return this.logger.warn("Max reconnect attempts reached"),void this.disconnect();if(this.connectionState!==DXLinkConnectionState.NOT_CONNECTED&&void 0!==this.connector){this.logger.debug("Trying to reconnect",this.connector.getUrl()),this.connector.stop(),this.scheduler.clear(),this.connectionDetails=DEFAULT_CONNECTION_DETAILS,this.lastReceivedMillis=0,this.lastSentMillis=0,this.isFirstAuthState=!0,this.reconnectAttempts++,this.setConnectionState(DXLinkConnectionState.CONNECTING);for(const channel of this.channels.values())channel.getState()!==DXLinkChannelState.CLOSED&&channel.processStatusRequested();this.scheduler.schedule(()=>{void 0!==this.connector&&this.connector.start()},1e3*this.reconnectAttempts,"RECONNECT")}},this.disconnect=()=>{this.connectionState!==DXLinkConnectionState.NOT_CONNECTED&&(this.logger.debug("Disconnecting"),this.connector?.stop(),this.connector=void 0,this.scheduler.clear(),this.connectionDetails=DEFAULT_CONNECTION_DETAILS,this.lastReceivedMillis=0,this.lastSentMillis=0,this.isFirstAuthState=!0,this.reconnectAttempts=0,this.setConnectionState(DXLinkConnectionState.NOT_CONNECTED),this.setAuthState(DXLinkAuthState.UNAUTHORIZED))},this.close=()=>{this.disconnect()},this.getConnectionDetails=()=>this.connectionDetails,this.getConnectionState=()=>this.connectionState,this.addConnectionStateChangeListener=listener=>this.connectionStateChangeListeners.add(listener),this.removeConnectionStateChangeListener=listener=>this.connectionStateChangeListeners.delete(listener),this.setAuthToken=token=>{this.lastSettedAuthToken=token,this.connectionState===DXLinkConnectionState.CONNECTED&&this.sendAuthMessage(token)},this.getAuthState=()=>this.authState,this.addAuthStateChangeListener=listener=>this.authStateChangeListeners.add(listener),this.removeAuthStateChangeListener=listener=>this.authStateChangeListeners.delete(listener),this.addErrorListener=listener=>this.errorListeners.add(listener),this.removeErrorListener=listener=>this.errorListeners.delete(listener),this.openChannel=(service,parameters)=>{const channelId=this.globalChannelId;this.globalChannelId+=2;const channel=new DXLinkWebSocketChannel(channelId,service,parameters,this.sendMessage,this.config);return this.channels.set(channelId,channel),this.connectionState===DXLinkConnectionState.CONNECTED&&this.authState===DXLinkAuthState.AUTHORIZED&&channel.request(),channel},this.setConnectionState=newStatus=>{const prev=this.connectionState;if(prev!==newStatus){this.connectionState=newStatus;for(const listener of this.connectionStateChangeListeners)listener(newStatus,prev)}},this.sendMessage=message=>{this.connector?.sendMessage(message),this.scheduleKeepalive(),this.lastSentMillis=Date.now()},this.sendAuthMessage=token=>{this.logger.debug("Sending auth message"),this.setAuthState(DXLinkAuthState.AUTHORIZING),this.sendMessage({type:"AUTH",channel:0,token:token})},this.setAuthState=newState=>{const prev=this.authState;this.authState=newState;for(const listener of this.authStateChangeListeners)try{listener(newState,prev)}catch(e){this.logger.error("Auth state listener error",e)}},this.processMessage=message=>{if(this.lastReceivedMillis=Date.now(),this.lastReceivedMillis-this.lastSentMillis>=1e3*this.config.keepaliveInterval&&this.sendMessage({type:"KEEPALIVE",channel:0}),(message=>0===message.channel&&("SETUP"===message.type||"KEEPALIVE"===message.type||"AUTH"===message.type||"AUTH_STATE"===message.type||"ERROR"===message.type))(message))switch(message.type){case"SETUP":return this.processSetupMessage(message);case"AUTH_STATE":return this.processAuthStateMessage(message);case"ERROR":return this.publishError({type:message.error,message:message.message});case"KEEPALIVE":return}else if((message=>0!==message.channel)(message)){const channel=this.channels.get(message.channel);if(void 0===channel)return void this.logger.warn("Received lifecycle message for unknown channel",message);if((message=>"CHANNEL_OPENED"===message.type||"CHANNEL_CLOSED"===message.type||"ERROR"===message.type||"CHANNEL_REQUEST"===message.type||"CHANNEL_CANCEL"===message.type)(message)){switch(message.type){case"CHANNEL_OPENED":return channel.processStatusOpened();case"CHANNEL_CLOSED":return channel.processStatusClosed();case"ERROR":return channel.processError({type:message.error,message:message.message})}return}return channel.processPayloadMessage(message)}this.logger.warn("Unhandeled message",message.type)},this.processSetupMessage=serverSetup=>{this.scheduler.cancel("SETUP_TIMEOUT"),this.connectionState!==DXLinkConnectionState.CONNECTING&&this.connectionState!==DXLinkConnectionState.CONNECTED||(this.connectionDetails={...this.connectionDetails,serverVersion:serverSetup.version,clientKeepaliveTimeout:this.config.keepaliveTimeout,serverKeepaliveTimeout:serverSetup.keepaliveTimeout},this.reconnectAttempts=0,void 0===this.lastSettedAuthToken&&this.setConnectionState(DXLinkConnectionState.CONNECTED));const timeoutMills=1e3*(serverSetup.keepaliveTimeout??60);this.scheduler.schedule(()=>this.timeoutCheck(timeoutMills),timeoutMills,"TIMEOUT")},this.publishError=error=>{if(this.logger.debug("Publishing error",error),0!==this.errorListeners.size)for(const listener of this.errorListeners)try{listener(error)}catch(e){this.logger.error("Error listener error",e)}else this.logger.error("Unhandled dxLink error",error)},this.processAuthStateMessage=({state:state})=>{this.logger.debug("Received auth state message",state),this.scheduler.cancel("AUTH_STATE_TIMEOUT"),this.isFirstAuthState?this.isFirstAuthState=!1:"UNAUTHORIZED"===state&&(this.lastSettedAuthToken=void 0),"AUTHORIZED"===state&&(this.setConnectionState(DXLinkConnectionState.CONNECTED),this.requestActiveChannels()),this.setAuthState(DXLinkAuthState[state])},this.requestActiveChannels=()=>{for(const channel of this.channels.values())channel.getState()!==DXLinkChannelState.CLOSED?channel.request():this.channels.delete(channel.id)},this.processTransportOpen=()=>{this.logger.debug("Connection opened");const setupMessage={type:"SETUP",channel:0,version:`${this.connectionDetails.protocolVersion}-${this.connectionDetails.clientVersion}`,keepaliveTimeout:this.config.keepaliveTimeout,acceptKeepaliveTimeout:this.config.acceptKeepaliveTimeout};this.scheduler.schedule(()=>{const errorMessage={type:"ERROR",channel:0,error:"TIMEOUT",message:"No setup message received for "+this.config.actionTimeout+"s"};this.sendMessage(errorMessage),this.publishError({type:errorMessage.error,message:`${errorMessage.message} from server`}),this.reconnect()},1e3*this.config.actionTimeout,"SETUP_TIMEOUT"),this.sendMessage(setupMessage),this.scheduler.schedule(()=>{const errorMessage={type:"ERROR",channel:0,error:"TIMEOUT",message:"No auth state message received for "+this.config.actionTimeout+"s"};this.sendMessage(errorMessage),this.publishError({type:errorMessage.error,message:`${errorMessage.message} from server`}),this.reconnect()},1e3*this.config.actionTimeout,"AUTH_STATE_TIMEOUT"),void 0!==this.lastSettedAuthToken&&this.sendAuthMessage(this.lastSettedAuthToken)},this.processTransportClose=(reason,error)=>{if(this.logger.debug("Connection closed",reason),void 0!==error&&this.publishError({type:"UNKNOWN",message:reason}),this.authState===DXLinkAuthState.UNAUTHORIZED)return this.lastSettedAuthToken=void 0,void this.disconnect();this.reconnect()},this.timeoutCheck=timeoutMills=>{const noKeepaliveDuration=Date.now()-this.lastReceivedMillis;if(noKeepaliveDuration>=timeoutMills)return this.sendMessage({type:"ERROR",channel:0,error:"TIMEOUT",message:"No keepalive received for "+noKeepaliveDuration+"ms"}),this.reconnect();const nextTimeout=Math.max(200,timeoutMills-noKeepaliveDuration);this.scheduler.schedule(()=>this.timeoutCheck(timeoutMills),nextTimeout,"TIMEOUT")},this.scheduleKeepalive=()=>{this.scheduler.schedule(()=>{this.sendMessage({type:"KEEPALIVE",channel:0}),this.scheduleKeepalive()},1e3*this.config.keepaliveInterval,"KEEPALIVE")},this.config={keepaliveInterval:30,keepaliveTimeout:60,acceptKeepaliveTimeout:60,actionTimeout:10,logLevel:DXLinkLogLevel.WARN,maxReconnectAttempts:-1,connectorFactory:url=>new DefaultDXLinkWebSocketConnector(url),...config},this.logger=new Logger(this.constructor.name,this.config.logLevel)}}export{DXLINK_WS_PROTOCOL_VERSION,DXLinkWebSocketClient,DefaultDXLinkWebSocketConnector};
//# sourceMappingURL=index.module.js.map