2022-05-12 09:31:31 +03:00
import { MessageType , SocketEvent } from '../interfaces/socket-events' ;
2022-05-03 08:13:36 +03:00
2022-05-12 09:31:31 +03:00
export interface SocketMessage {
2022-05-04 00:17:05 +03:00
type : MessageType ;
2022-05-03 03:45:22 +03:00
data : any ;
}
export default class WebsocketService {
websocket : WebSocket ;
accessToken : string ;
2023-03-14 01:23:14 +03:00
host : string ;
2022-05-03 03:45:22 +03:00
path : string ;
websocketReconnectTimer : ReturnType < typeof setTimeout > ;
2022-10-19 02:39:49 +03:00
isShutdown = false ;
2023-05-20 04:27:15 +03:00
backOff = 0 ;
2022-10-19 02:39:49 +03:00
2022-05-12 09:31:31 +03:00
handleMessage ? : ( message : SocketEvent ) = > void ;
2022-05-03 08:13:36 +03:00
2023-03-14 01:23:14 +03:00
socketConnected : ( ) = > void ;
socketDisconnected : ( ) = > void ;
2022-10-19 06:40:57 +03:00
constructor ( accessToken , path , host ) {
2022-05-03 03:45:22 +03:00
this . accessToken = accessToken ;
2022-05-03 08:13:36 +03:00
this . path = path ;
2022-10-19 02:39:49 +03:00
this . websocketReconnectTimer = null ;
2022-10-19 06:40:57 +03:00
this . isShutdown = false ;
2023-03-14 01:23:14 +03:00
this . host = host ;
2022-05-03 03:45:22 +03:00
2022-10-19 02:39:49 +03:00
this . createAndConnect = this . createAndConnect . bind ( this ) ;
this . shutdown = this . shutdown . bind ( this ) ;
2022-05-03 03:45:22 +03:00
2023-03-14 01:23:14 +03:00
this . createAndConnect ( ) ;
2022-05-03 03:45:22 +03:00
}
2023-03-14 01:23:14 +03:00
createAndConnect() {
if ( ! this . host ) {
return ;
}
2023-04-01 07:00:56 +03:00
if ( this . isShutdown ) {
return ;
}
2023-03-14 01:23:14 +03:00
const url = new URL ( this . host ) ;
2022-10-10 07:16:46 +03:00
url . protocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
url . pathname = '/ws' ;
url . port = window . location . port === '3000' ? '8080' : window . location . port ;
2022-05-03 03:45:22 +03:00
url . searchParams . append ( 'accessToken' , this . accessToken ) ;
const ws = new WebSocket ( url . toString ( ) ) ;
ws . onopen = this . onOpen . bind ( this ) ;
2024-10-23 02:29:46 +03:00
ws . onclose = this . onClose . bind ( this ) ;
2022-05-03 03:45:22 +03:00
ws . onmessage = this . onMessage . bind ( this ) ;
this . websocket = ws ;
}
onOpen() {
if ( this . websocketReconnectTimer ) {
clearTimeout ( this . websocketReconnectTimer ) ;
}
2023-03-14 01:23:14 +03:00
this . socketConnected ( ) ;
2023-05-20 04:27:15 +03:00
this . backOff = 0 ;
2022-05-03 03:45:22 +03:00
}
2024-10-23 02:29:46 +03:00
onClose() {
2022-10-19 02:39:49 +03:00
if ( ! this . isShutdown ) {
2024-10-23 02:29:46 +03:00
handleNetworkingError ( ) ;
this . socketDisconnected ( ) ;
2022-10-19 02:39:49 +03:00
this . scheduleReconnect ( ) ;
}
}
scheduleReconnect() {
2023-04-01 07:00:56 +03:00
if ( this . isShutdown ) {
return ;
}
2022-10-19 02:39:49 +03:00
if ( this . websocketReconnectTimer ) {
clearTimeout ( this . websocketReconnectTimer ) ;
}
this . websocketReconnectTimer = setTimeout (
this . createAndConnect ,
2023-05-20 04:27:15 +03:00
Math . min ( this . backOff , 10 _000 ) ,
2022-10-19 02:39:49 +03:00
) ;
2023-05-20 04:27:15 +03:00
this . backOff += 1000 ;
2022-10-19 02:39:49 +03:00
}
shutdown() {
this . isShutdown = true ;
this . websocket . close ( ) ;
2022-05-03 03:45:22 +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 : SocketMessage ) {
// Optimization where multiple events can be sent within a
// single websocket message. So split them if needed.
const messages = e . data . split ( '\n' ) ;
2022-05-26 23:52:04 +03:00
let socketEvent : SocketEvent ;
2022-05-03 03:45:22 +03:00
// eslint-disable-next-line no-plusplus
for ( let i = 0 ; i < messages . length ; i ++ ) {
try {
2022-05-26 23:52:04 +03:00
socketEvent = JSON . parse ( messages [ i ] ) ;
2022-05-03 08:13:36 +03:00
if ( this . handleMessage ) {
2022-05-26 23:52:04 +03:00
this . handleMessage ( socketEvent ) ;
2022-05-03 08:13:36 +03:00
}
2022-10-10 07:16:46 +03:00
} catch ( err ) {
console . error ( err , err . data ) ;
2022-05-03 03:45:22 +03:00
return ;
}
2022-05-26 23:52:04 +03:00
if ( ! socketEvent . type ) {
console . error ( 'No type provided' , socketEvent ) ;
2022-05-03 03:45:22 +03:00
return ;
}
// Send PONGs
2022-05-26 23:52:04 +03:00
if ( socketEvent . type === MessageType . PING ) {
2022-05-03 03:45:22 +03:00
this . sendPong ( ) ;
return ;
}
}
}
2022-05-26 23:52:04 +03:00
isConnected ( ) : boolean {
return this . websocket ? . readyState === this . websocket ? . OPEN ;
}
2022-05-03 03:45:22 +03:00
// Outbound: Other components can pass an object to `send`.
2022-05-26 23:52:04 +03:00
send ( socketEvent : any ) {
2022-05-03 03:45:22 +03:00
// Sanity check that what we're sending is a valid type.
2022-05-26 23:52:04 +03:00
if ( ! socketEvent . type || ! MessageType [ socketEvent . type ] ) {
console . warn ( ` Outbound message: Unknown socket message type: " ${ socketEvent . type } " sent. ` ) ;
2022-05-03 03:45:22 +03:00
}
2022-05-26 23:52:04 +03:00
const messageJSON = JSON . stringify ( socketEvent ) ;
2022-05-03 03:45:22 +03:00
this . websocket . send ( messageJSON ) ;
}
// Reply to a PING as a keep alive.
sendPong() {
2022-05-04 00:17:05 +03:00
const pong = { type : MessageType . PONG } ;
2022-05-03 03:45:22 +03:00
this . send ( pong ) ;
}
}
2023-03-14 01:23:14 +03:00
function handleNetworkingError() {
2022-05-03 03:45:22 +03:00
console . error (
2023-03-14 01:23:14 +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 ` ,
2022-05-03 03:45:22 +03:00
) ;
2022-05-03 08:13:36 +03:00
}