2021-01-05 09:30:18 +03:00
import { URL _WEBSOCKET } from './constants.js' ;
2020-08-24 05:06:58 +03:00
/ * *
* These are the types of messages that we can handle with the websocket .
* Mostly used by ` websocket.js ` but if other components need to handle
* different types then it can import this file .
* /
export const SOCKET _MESSAGE _TYPES = {
CHAT : 'CHAT' ,
PING : 'PING' ,
NAME _CHANGE : 'NAME_CHANGE' ,
2020-10-17 03:36:11 +03:00
PONG : 'PONG' ,
2021-02-19 10:05:52 +03:00
SYSTEM : 'SYSTEM' ,
USER _JOINED : 'USER_JOINED' ,
2021-07-20 05:22:29 +03:00
CHAT _ACTION : 'CHAT_ACTION' ,
CONNECTED _USER _INFO : 'CONNECTED_USER_INFO' ,
ERROR _USER _DISABLED : 'ERROR_USER_DISABLED' ,
ERROR _NEEDS _REGISTRATION : 'ERROR_NEEDS_REGISTRATION' ,
ERROR _MAX _CONNECTIONS _EXCEEDED : 'ERROR_MAX_CONNECTIONS_EXCEEDED' ,
2020-08-24 05:06:58 +03:00
} ;
2020-08-06 20:55:33 +03:00
2020-08-13 19:28:47 +03:00
export const CALLBACKS = {
2020-08-06 20:55:33 +03:00
RAW _WEBSOCKET _MESSAGE _RECEIVED : 'rawWebsocketMessageReceived' ,
WEBSOCKET _CONNECTED : 'websocketConnected' ,
2021-07-20 05:23:06 +03:00
} ;
2020-08-06 20:55:33 +03:00
2020-08-24 05:06:58 +03:00
const TIMER _WEBSOCKET _RECONNECT = 5000 ; // ms
2020-08-06 20:55:33 +03:00
2020-08-24 05:06:58 +03:00
export default class Websocket {
2021-07-20 05:22:29 +03:00
constructor ( accessToken ) {
2020-08-06 20:55:33 +03:00
this . websocket = null ;
this . websocketReconnectTimer = null ;
2021-07-20 05:22:29 +03:00
this . accessToken = accessToken ;
2020-08-06 20:55:33 +03:00
this . websocketConnectedListeners = [ ] ;
this . websocketDisconnectListeners = [ ] ;
this . rawMessageListeners = [ ] ;
this . send = this . send . bind ( this ) ;
2020-09-07 00:28:21 +03:00
this . createAndConnect = this . createAndConnect . bind ( this ) ;
this . scheduleReconnect = this . scheduleReconnect . bind ( this ) ;
2021-07-20 05:22:29 +03:00
this . shutdown = this . shutdown . bind ( this ) ;
2020-08-06 20:55:33 +03:00
2021-07-20 05:22:29 +03:00
this . isShutdown = false ;
2021-02-19 10:05:52 +03:00
2020-09-07 00:28:21 +03:00
this . createAndConnect ( ) ;
}
createAndConnect ( ) {
2021-07-20 05:22:29 +03:00
const url = new URL ( URL _WEBSOCKET ) ;
url . searchParams . append ( 'accessToken' , this . accessToken ) ;
2021-07-20 05:23:06 +03:00
2021-07-20 05:22:29 +03:00
const ws = new WebSocket ( url . toString ( ) ) ;
2020-08-06 20:55:33 +03:00
ws . onopen = this . onOpen . bind ( this ) ;
ws . onclose = this . onClose . bind ( this ) ;
ws . onerror = this . onError . bind ( this ) ;
ws . onmessage = this . onMessage . bind ( this ) ;
this . websocket = ws ;
}
// Other components should register for websocket callbacks.
addListener ( type , callback ) {
if ( type == CALLBACKS . WEBSOCKET _CONNECTED ) {
this . websocketConnectedListeners . push ( callback ) ;
} else if ( type == CALLBACKS . WEBSOCKET _DISCONNECTED ) {
this . websocketDisconnectListeners . push ( callback ) ;
} else if ( type == CALLBACKS . RAW _WEBSOCKET _MESSAGE _RECEIVED ) {
this . rawMessageListeners . push ( callback ) ;
}
}
// Interface with other components
// Outbound: Other components can pass an object to `send`.
send ( message ) {
// Sanity check that what we're sending is a valid type.
if ( ! message . type || ! SOCKET _MESSAGE _TYPES [ message . type ] ) {
2020-09-07 00:28:21 +03:00
console . warn (
` Outbound message: Unknown socket message type: " ${ message . type } " sent. `
) ;
2020-08-06 20:55:33 +03:00
}
2020-08-13 19:28:47 +03:00
2020-08-06 20:55:33 +03:00
const messageJSON = JSON . stringify ( message ) ;
this . websocket . send ( messageJSON ) ;
}
2021-07-20 05:22:29 +03:00
shutdown ( ) {
this . isShutdown = true ;
this . websocket . close ( ) ;
}
2020-08-06 20:55:33 +03:00
// Private methods
// Fire the callbacks of the listeners.
notifyWebsocketConnectedListeners ( message ) {
this . websocketConnectedListeners . forEach ( function ( callback ) {
callback ( message ) ;
} ) ;
}
notifyWebsocketDisconnectedListeners ( message ) {
this . websocketDisconnectListeners . forEach ( function ( callback ) {
callback ( message ) ;
} ) ;
}
notifyRawMessageListeners ( message ) {
this . rawMessageListeners . forEach ( function ( callback ) {
callback ( message ) ;
} ) ;
}
// Internal websocket callbacks
onOpen ( e ) {
if ( this . websocketReconnectTimer ) {
clearTimeout ( this . websocketReconnectTimer ) ;
}
this . notifyWebsocketConnectedListeners ( ) ;
}
onClose ( e ) {
// connection closed, discard old websocket and create a new one in 5s
this . websocket = null ;
this . notifyWebsocketDisconnectedListeners ( ) ;
this . handleNetworkingError ( 'Websocket closed.' ) ;
2021-07-20 05:22:29 +03:00
if ( ! this . isShutdown ) {
this . scheduleReconnect ( ) ;
}
2020-08-06 20:55:33 +03:00
}
// On ws error just close the socket and let it re-connect again for now.
onError ( e ) {
this . handleNetworkingError ( ` Socket error: ${ JSON . parse ( e ) } ` ) ;
this . websocket . close ( ) ;
2021-07-20 05:22:29 +03:00
if ( ! this . isShutdown ) {
this . scheduleReconnect ( ) ;
}
2020-09-07 00:28:21 +03:00
}
scheduleReconnect ( ) {
this . websocketReconnectTimer = setTimeout (
this . createAndConnect ,
TIMER _WEBSOCKET _RECONNECT
) ;
2020-08-06 20:55:33 +03:00
}
/ *
onMessage is fired when an inbound object comes across the websocket .
If the message is of type ` PING ` we send a ` PONG ` back and do not
pass it along to listeners .
* /
onMessage ( e ) {
2021-07-27 05:23:15 +03:00
// Optimization where multiple events can be sent within a
// single websocket message. So split them if needed.
var messages = e . data . split ( '\n' ) ;
for ( var i = 0 ; i < messages . length ; i ++ ) {
try {
var model = JSON . parse ( e . data ) ;
} catch ( e ) {
// console.log(e, e.data);
return ;
}
if ( ! model . type ) {
console . error ( 'No type provided' , model ) ;
return ;
}
// Send PONGs
if ( model . type === SOCKET _MESSAGE _TYPES . PING ) {
this . sendPong ( ) ;
return ;
}
// Notify any of the listeners via the raw socket message callback.
this . notifyRawMessageListeners ( model ) ;
2020-08-06 20:55:33 +03:00
}
}
// Reply to a PING as a keep alive.
sendPong ( ) {
const pong = { type : SOCKET _MESSAGE _TYPES . PONG } ;
this . send ( pong ) ;
}
handleNetworkingError ( error ) {
2021-07-20 05:23:06 +03:00
console . error (
2021-07-28 23:00:20 +03:00
` Chat has been disconnected and is likely not working for you. It's possible you were removed from chat. If this is a server configuration issue, visit troubleshooting steps to resolve. https://owncast.online/docs/troubleshooting/#chat-is-disabled: ${ error } `
2021-07-20 05:23:06 +03:00
) ;
2020-08-24 05:06:58 +03:00
}
2020-08-06 20:55:33 +03:00
}