2017-11-13 01:23:39 -05:00
'use strict' ;
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
// ============================================================================
// Socket Client
// This connects to the FrankerFaceZ socket servers for PubSub and RPC.
// ============================================================================
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
import Module from 'utilities/module' ;
import { DEBUG , WS _CLUSTERS } from 'utilities/constants' ;
2020-07-14 21:24:07 -04:00
import { on } from 'utilities/dom' ;
2015-11-01 17:28:19 -05:00
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
export const State = {
DISCONNECTED : 0 ,
CONNECTING : 1 ,
CONNECTED : 2
}
2015-01-20 01:53:18 -05:00
2020-01-11 17:13:56 -05:00
const ANONYMOUS _ID = '683b45e4-f853-4c45-bf96-7d799cc93e34' ;
2017-11-13 01:23:39 -05:00
export default class SocketClient extends Module {
constructor ( ... args ) {
super ( ... args ) ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
this . inject ( 'settings' ) ;
2021-02-22 20:11:35 -05:00
this . inject ( 'experiments' ) ;
2015-01-20 01:53:18 -05:00
2020-02-10 19:43:35 -05:00
this . settings . addUI ( 'socket.info' , {
path : 'Debugging > Socket >> Info @{"sort": -1000}' ,
force _seen : true ,
no _filter : true ,
component : 'socket-info' ,
getSocket : ( ) => this ,
} ) ;
2023-09-06 16:10:47 -04:00
this . settings . add ( 'auth.mode' , {
default : 'chat' ,
ui : {
path : 'Data Management > Authentication >> General' ,
title : 'Authentication Provider' ,
description : 'Which method should the FrankerFaceZ client use to authenticate against the FFZ servers when necessary?' ,
component : 'setting-select-box' ,
force _seen : true ,
data : [
{
value : 'chat' ,
title : 'Twitch Chat'
} ,
{
value : false ,
title : 'Disabled (No Authentication)'
}
]
} ,
changed : ( ) => this . _cached _token = null
} ) ;
2018-05-31 18:34:15 -04:00
this . settings . add ( 'socket.use-cluster' , {
2017-11-13 01:23:39 -05:00
default : 'Production' ,
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
ui : {
path : 'Debugging @{"expanded": false, "sort": 9999} > Socket >> General' ,
title : 'Server Cluster' ,
2018-05-31 18:34:15 -04:00
description : 'Which server cluster to connect to. Do not change this unless you are actually doing development work on the socket server backend. Doing so will break all features relying on the socket server, including emote information lookups, link tooltips, and live data updates.' ,
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
component : 'setting-select-box' ,
2015-08-02 02:41:03 -04:00
2017-11-13 01:23:39 -05:00
data : [ {
value : null ,
title : 'Disabled'
} ] . concat ( Object . keys ( WS _CLUSTERS ) . map ( x => ( {
value : x ,
title : x
} ) ) )
}
} ) ;
2015-08-02 02:41:03 -04:00
2017-11-13 01:23:39 -05:00
this . _want _connected = false ;
2015-11-01 17:28:19 -05:00
2018-03-14 13:58:04 -04:00
this . _topics = new Map ;
2017-11-13 01:23:39 -05:00
this . _pending = [ ] ;
this . _awaiting = new Map ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . _socket = null ;
this . _state = 0 ;
this . _last _id = 1 ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . _delay = 0 ;
this . _last _ping = null ;
this . _time _drift = 0 ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . _host _idx = - 1 ;
this . _host _pool = - 1 ;
2015-11-01 17:28:19 -05:00
2018-05-31 18:34:15 -04:00
this . settings . on ( ':changed:socket.use-cluster' , ( ) => {
2017-11-13 01:23:39 -05:00
this . _host = null ;
2020-01-11 17:13:56 -05:00
if ( this . disconnected )
2017-11-13 01:23:39 -05:00
this . connect ( ) ;
else
this . reconnect ( ) ;
} ) ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . on ( ':command:reconnect' , this . reconnect , this ) ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . on ( ':command:do_authorize' , challenge => {
2018-04-08 19:44:44 +02:00
// this.log.warn('Unimplemented: do_authorize', challenge);
// We don't have our own IRC connection yet, so the site's chat has to do.
const _chat = this . resolve ( 'site.chat' ) ;
2018-08-28 19:13:26 -04:00
const chat = _chat && _chat . ChatService . first ;
const con = chat . client && chat . client . connection ;
2018-04-08 19:44:44 +02:00
2018-08-28 19:13:26 -04:00
if ( con && con . send )
con . send ( ` PRIVMSG #frankerfacezauthorizer :AUTH ${ challenge } ` ) ;
2017-11-13 01:23:39 -05:00
} ) ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . enable ( ) ;
2015-11-01 17:28:19 -05:00
}
2016-05-22 14:08:11 -04:00
2021-02-22 20:11:35 -05:00
onEnable ( ) {
// For now, stop connecting to the sockets for people using the
// API links experiment.
if ( this . experiments . getAssignment ( 'api_links' ) )
return ;
this . connect ( ) ;
}
2017-11-13 01:23:39 -05:00
onDisable ( ) { this . disconnect ( ) }
2016-05-22 14:08:11 -04:00
2017-11-13 01:23:39 -05:00
// ========================================================================
// Properties
// ========================================================================
2016-05-22 14:08:11 -04:00
2017-11-13 01:23:39 -05:00
get connected ( ) {
return this . _state === State . CONNECTED ;
}
2016-05-22 14:08:11 -04:00
2017-11-13 01:23:39 -05:00
get connecting ( ) {
return this . _state === State . CONNECTING ;
}
2016-05-22 14:08:11 -04:00
2017-11-13 01:23:39 -05:00
get disconnected ( ) {
return this . _state === State . DISCONNECTED ;
}
2016-05-22 14:08:11 -04:00
2015-10-24 22:44:00 -04:00
2019-10-11 17:41:07 -04:00
// ========================================================================
// FFZ API Helpers
// ========================================================================
getAPIToken ( ) {
2023-09-06 16:10:47 -04:00
const mode = this . settings . get ( 'auth.mode' ) ;
if ( mode === 'chat' )
return this . _getAPIToken _Chat ( ) ;
return Promise . reject ( new Error ( 'The user has disabled authentication.' ) ) ;
}
_getAPIToken _Chat ( ) {
2019-10-11 17:41:07 -04:00
if ( this . _cached _token ) {
if ( this . _cached _token . expires > ( Date . now ( ) + 15000 ) )
return Promise . resolve ( this . _cached _token ) ;
}
if ( this . _token _waiters )
return new Promise ( ( s , f ) => this . _token _waiters . push ( [ s , f ] ) ) ;
this . _token _waiters = [ ] ;
return new Promise ( ( s , f ) => {
this . _token _waiters . push ( [ s , f ] ) ;
2020-07-14 21:24:07 -04:00
let done = false , timer = null ;
const fail = err => {
if ( done )
return ;
clearTimeout ( timer ) ;
done = true ;
this . log . error ( 'Unable to get API token.' , err ) ;
const waiters = this . _token _waiters ;
this . _token _waiters = null ;
for ( const pair of waiters )
pair [ 1 ] ( err ) ;
}
const user = this . resolve ( 'site' ) ? . getUser ? . ( ) ;
if ( ! user || ! user . id )
return fail ( new Error ( 'Unable to get current user or not logged in.' ) ) ;
2023-03-06 17:08:47 -05:00
const es = new EventSource ( ` https://api.frankerfacez.com/auth/ext_verify/ ${ user . id } ` ) ;
2020-07-14 21:24:07 -04:00
on ( es , 'challenge' , event => {
const conn = this . resolve ( 'site.chat' ) ? . ChatService ? . first ? . client ? . connection ;
if ( conn && conn . send )
conn . send ( ` PRIVMSG #frankerfacezauthorizer :AUTH ${ event . data } ` ) ;
} ) ;
on ( es , 'token' , event => {
if ( done )
return ;
clearTimeout ( timer ) ;
let token = null ;
try {
token = JSON . parse ( event . data ) ;
} catch ( err ) {
fail ( err ) ;
return ;
}
if ( ! token || ! token . token ) {
fail ( new Error ( 'Received empty token from server.' ) ) ;
return ;
}
token . expires = ( new Date ( token . expires ) ) . getTime ( ) ;
this . _cached _token = token ;
done = true ;
const waiters = this . _token _waiters ;
this . _token _waiters = null ;
for ( const pair of waiters )
pair [ 0 ] ( token ) ;
} ) ;
on ( es , 'error' , err => {
fail ( err ) ;
} ) ;
on ( es , 'close' , ( ) => {
es . close ( ) ;
if ( ! done )
fail ( new Error ( 'Connection closed unexpectedly.' ) ) ;
} ) ;
timer = setTimeout ( ( ) => {
fail ( new Error ( 'timeout' ) ) ;
} , 5000 ) ;
/ * t h i s . c a l l ( ' g e t _ a p i _ t o k e n ' ) . t h e n ( t o k e n = > {
2019-10-11 17:41:07 -04:00
token . expires = ( new Date ( token . expires ) ) . getTime ( ) ;
this . _cached _token = token ;
const waiters = this . _token _waiters ;
this . _token _waiters = null ;
for ( const pair of waiters )
pair [ 0 ] ( token ) ;
} ) . catch ( err => {
this . log . error ( 'Unable to get API token.' , err ) ;
const waiters = this . _token _waiters ;
this . _token _waiters = null ;
for ( const pair of waiters )
pair [ 1 ] ( err ) ;
2020-07-14 21:24:07 -04:00
} ) ; * /
2019-10-11 17:41:07 -04:00
} ) ;
}
async getBareAPIToken ( ) {
return ( await this . getAPIToken ( ) ) ? . token ;
}
2017-11-13 01:23:39 -05:00
// ========================================================================
// Connection Logic
// ========================================================================
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
selectHost ( ) {
2018-05-31 18:34:15 -04:00
const cluster _id = this . settings . get ( 'socket.use-cluster' ) ,
2018-04-02 03:30:22 -04:00
cluster = WS _CLUSTERS [ cluster _id ] ,
l = cluster && cluster . length ;
2015-11-01 17:28:19 -05:00
2018-04-02 03:30:22 -04:00
if ( ! l )
2017-11-13 01:23:39 -05:00
return null ;
2015-11-01 17:28:19 -05:00
2018-04-02 03:30:22 -04:00
let total = 0 , i = l ;
2017-11-13 01:23:39 -05:00
while ( i -- > 0 )
total += cluster [ i ] [ 1 ] ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
let val = Math . random ( ) * total ;
for ( let i = 0 ; i < l ; i ++ ) {
val -= cluster [ i ] [ 1 ] ;
if ( val <= 0 )
return cluster [ i ] [ 0 ] ;
}
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
return cluster [ l - 1 ] [ 0 ] ;
}
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
_reconnect ( ) {
if ( ! this . _reconnect _timer ) {
if ( this . _delay < 60000 )
2019-05-07 15:04:12 -04:00
this . _delay += ( Math . floor ( Math . random ( ) * 15 ) + 5 ) * 1000 ;
2017-11-13 01:23:39 -05:00
else
this . _delay = ( Math . floor ( Math . random ( ) * 60 ) + 30 ) * 1000 ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
this . _reconnect _timer = setTimeout ( ( ) => {
this . connect ( ) ;
} , this . _delay ) ;
}
2015-01-27 17:05:51 -05:00
}
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
reconnect ( ) {
this . disconnect ( ) ;
this . _reconnect ( ) ;
}
2015-01-20 01:53:18 -05:00
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
connect ( ) {
this . _want _connected = true ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
if ( this . _reconnect _timer ) {
clearTimeout ( this . _reconnect _timer ) ;
this . _reconnect _timer = null ;
2015-05-17 19:02:57 -04:00
}
2017-11-13 01:23:39 -05:00
if ( ! this . disconnected )
return ;
const host = this . _host = this . _host || this . selectHost ( ) ;
if ( ! host )
return ;
2015-06-10 18:46:04 -04:00
2017-11-13 01:23:39 -05:00
this . _state = State . CONNECTING ;
this . _last _id = 1 ;
2015-06-10 18:46:04 -04:00
2017-11-13 01:23:39 -05:00
this . _delay = 0 ;
this . _last _ping = null ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
this . log . info ( ` Using Server: ${ host } ` ) ;
2015-07-04 17:06:36 -04:00
2017-11-13 01:23:39 -05:00
let ws ;
2015-07-04 17:06:36 -04:00
2017-11-13 01:23:39 -05:00
try {
ws = this . _socket = new WebSocket ( host ) ;
} catch ( err ) {
this . _state = State . DISCONNECTED ;
this . _reconnect ( ) ;
this . log . error ( 'Unable to create WebSocket.' , err ) ;
return ;
2015-07-04 17:06:36 -04:00
}
2017-11-13 01:23:39 -05:00
ws . onopen = ( ) => {
2018-04-28 17:56:03 -04:00
if ( this . _socket !== ws ) {
this . log . warn ( 'A socket connected that is not our primary socket.' ) ;
return ws . close ( ) ;
}
2017-11-13 01:23:39 -05:00
this . _state = State . CONNECTED ;
this . _sent _user = false ;
this . log . info ( 'Connected.' ) ;
// Initial HELLO. Here we get a Client-ID and initial server timestamp.
// This is handled entirely on the socket server and so should be
// fast enough to use as a ping.
this . _ping _time = performance . now ( ) ;
this . _send (
'hello' ,
2020-01-11 17:13:56 -05:00
[ ` ffz_ ${ window . FrankerFaceZ . version _info } ` , ANONYMOUS _ID ] ,
2017-11-13 01:23:39 -05:00
( success , data ) => {
if ( ! success )
return this . log . warn ( 'Error Saying Hello' , data ) ;
this . _on _pong ( false , success , data [ 1 ] ) ;
2020-01-11 17:13:56 -05:00
/ * i f ( d a t a [ 0 ] = = = A N O N Y M O U S _ I D )
this . log . info ( 'Client ID: <Anonymous>' ) ;
else {
this . settings . provider . set ( 'client-id' , data [ 0 ] ) ;
this . log . info ( 'Client ID:' , data [ 0 ] ) ;
} * /
2017-11-13 01:23:39 -05:00
} ) ;
// Grab the current user from the site.
const site = this . resolve ( 'site' ) ,
send _user = ( ) => {
if ( this . _sent _user || ! this . connected )
return ;
const user = site . getUser ( ) ;
if ( user && user . login ) {
this . _sent _user = true ;
this . _send ( 'setuser' , user . login ) ;
} else if ( ! site . enabled )
this . once ( 'site:enabled' , send _user , this ) ;
}
send _user ( ) ;
// Subscribe to Topics
2018-03-14 17:32:19 -04:00
for ( const topic of this . _topics . keys ( ) )
2017-11-13 01:23:39 -05:00
this . _send ( 'sub' , topic ) ;
2015-01-20 01:53:18 -05:00
2015-10-26 12:13:28 -07:00
2017-11-13 01:23:39 -05:00
// Send pending commands.
for ( const [ command , args , callback ] of this . _pending )
this . _send ( command , args , callback ) ;
this . _pending = [ ] ;
// We're ready.
this . _send ( 'ready' , this . _offline _time || 0 ) ;
this . _offline _time = null ;
this . emit ( ':connected' ) ;
2015-10-26 12:13:28 -07:00
}
2017-11-13 01:23:39 -05:00
ws . onerror = ( ) => {
2018-04-28 17:56:03 -04:00
if ( ws !== this . _socket )
return ;
2017-11-13 01:23:39 -05:00
if ( ! this . _offline _time )
this . _offline _time = Date . now ( ) ;
2015-10-26 12:13:28 -07:00
}
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
ws . onclose = event => {
2018-04-28 17:56:03 -04:00
if ( ws !== this . _socket )
return ;
2017-11-13 01:23:39 -05:00
const old _state = this . _state ;
this . log . info ( ` Disconnected. ( ${ event . code } : ${ event . reason } ) ` ) ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
this . _state = State . DISCONNECTED ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
for ( const [ cmd _id , callback ] of this . _awaiting ) {
const err = new Error ( 'disconnected' ) ;
try {
if ( typeof callback === 'function' )
callback ( false , err ) ;
else
callback [ 1 ] ( err ) ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
} catch ( error ) {
this . log . warn ( ` Callback Error # ${ cmd _id } ` , error ) ;
}
2015-02-08 02:01:09 -05:00
}
2017-11-13 01:23:39 -05:00
this . _awaiting . clear ( ) ;
if ( ! this . _want _connected )
return ;
if ( ! this . _offline _time )
this . _offline _time = Date . now ( ) ;
// Reset the host if we didn't manage to connect.
if ( old _state !== State . CONNECTED )
this . _host = null ;
this . _reconnect ( ) ;
this . emit ( ':closed' , event . code , event . reason ) ;
2015-02-08 02:01:09 -05:00
}
2015-03-15 21:24:22 -04:00
2015-08-02 02:41:03 -04:00
2017-11-13 01:23:39 -05:00
ws . onmessage = event => {
2018-04-28 17:56:03 -04:00
if ( ws !== this . _socket )
return ;
2017-11-13 01:23:39 -05:00
// Format:
// -1 <cmd_name>[ <json_data>]
// <reply-id> <ok/err>[ <json_data>]
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
const raw = event . data ,
idx = raw . indexOf ( ' ' ) ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
if ( idx === - 1 )
return this . log . warn ( 'Malformed message from server.' , event . data ) ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
const reply = parseInt ( raw . slice ( 0 , idx ) , 10 ) ,
ix2 = raw . indexOf ( ' ' , idx + 1 ) ,
2015-10-24 21:33:31 -07:00
2017-11-13 01:23:39 -05:00
cmd = raw . slice ( idx + 1 , ix2 === - 1 ? raw . length : ix2 ) ,
data = ix2 === - 1 ? undefined : JSON . parse ( raw . slice ( ix2 + 1 ) ) ;
2015-02-26 00:42:11 -05:00
2017-11-13 01:23:39 -05:00
if ( reply === - 1 ) {
this . log . debug ( ` Received Command: ${ cmd } ` , data ) ;
this . emit ( ` :command: ${ cmd } ` , data ) ;
2015-02-26 00:42:11 -05:00
2017-02-14 00:07:55 -05:00
} else {
2017-11-13 01:23:39 -05:00
const success = cmd === 'ok' ,
callback = this . _awaiting . get ( reply ) ;
if ( callback ) {
this . _awaiting . delete ( reply ) ;
if ( typeof callback === 'function' )
callback ( success , data ) ;
else
callback [ success ? 0 : 1 ] ( data ) ;
} else if ( ! success || DEBUG )
this . log . info ( ` Received Reply # ${ reply } : ` , success ? 'OK' : 'Error' , data ) ;
2015-01-20 01:53:18 -05:00
}
}
}
2017-11-13 01:23:39 -05:00
disconnect ( ) {
this . _want _connected = false ;
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
if ( this . _reconnect _timer ) {
clearTimeout ( this . _reconnect _timer ) ;
this . _reconnect _timer = null ;
}
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
if ( this . disconnected )
return ;
try {
this . _socket . close ( ) ;
} catch ( err ) { /* if this caused an exception, we don't care -- it's still closed */ }
2015-01-20 01:53:18 -05:00
2017-11-13 01:23:39 -05:00
this . _socket = null ;
this . _state = State . DISCONNECTED ;
2020-02-10 19:43:35 -05:00
this . emit ( ':disconnected' ) ;
2015-07-13 21:52:44 -04:00
}
2015-02-24 00:33:29 -05:00
2017-11-13 01:23:39 -05:00
// ========================================================================
// Latency
// ========================================================================
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
_on _pong ( skip _log , success , data ) {
const now = performance . now ( ) ;
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
if ( ! success ) {
this . _ping _time = null ;
if ( ! skip _log )
this . log . warn ( 'Error Pinging Server' , data ) ;
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
} else if ( this . _ping _time ) {
const d _now = Date . now ( ) ,
rtt = now - this . _ping _time ,
ping = this . _last _ping = rtt / 2 ;
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
this . _ping _time = null ;
const drift = this . _time _drift = d _now - ( data + ping ) ;
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
if ( ! skip _log ) {
this . log . info ( 'Server Time:' , new Date ( data ) . toISOString ( ) ) ;
this . log . info ( ' Local Time:' , new Date ( d _now ) . toISOString ( ) ) ;
this . log . info ( ` Est. Ping: ${ ping . toFixed ( 5 ) } ms ` ) ;
this . log . info ( ` Time Offset: ${ drift / 1000 } ` ) ;
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
if ( Math . abs ( drift ) > 300000 )
this . log . warn ( 'Local time differs from server time by more than 5 minutes.' ) ;
}
}
2020-02-10 19:43:35 -05:00
this . emit ( ':pong' ) ;
2017-11-13 01:23:39 -05:00
}
2015-11-19 02:45:56 -05:00
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
ping ( skip _log ) {
if ( this . _ping _time || ! this . connected )
return ;
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
this . _ping _time = performance . now ( ) ;
this . _send ( 'ping' , undefined , ( s , d ) => this . _on _pong ( skip _log , s , d ) ) ;
}
2015-11-19 02:45:56 -05:00
2017-11-13 01:23:39 -05:00
// ========================================================================
// Communication
// ========================================================================
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
_send ( command , args , callback ) {
if ( ! this . connected )
return this . log . warn ( ` Tried sending command " ${ command } " while disconnected. ` ) ;
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
const cmd _id = this . _last _id ++ ;
if ( callback )
this . _awaiting . set ( cmd _id , callback ) ;
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
this . _socket . send ( ` ${ cmd _id } ${ command } ${ args !== undefined ? ` ${ JSON . stringify ( args ) } ` : '' } ` ) ;
}
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
send ( command , ... args ) {
if ( args . length === 1 )
args = args [ 0 ] ;
2020-02-10 19:43:35 -05:00
else if ( ! args . length )
args = undefined ;
2017-11-13 01:23:39 -05:00
if ( ! this . connected )
this . _pending . push ( [ command , args ] ) ;
else
this . _send ( command , args ) ;
2015-11-19 02:45:56 -05:00
}
2015-06-05 03:59:28 -04:00
2017-11-13 01:23:39 -05:00
call ( command , ... args ) {
if ( args . length === 1 )
args = args [ 0 ] ;
2020-02-10 19:43:35 -05:00
else if ( ! args . length )
args = undefined ;
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
return new Promise ( ( resolve , reject ) => {
if ( ! this . connected )
this . _pending . push ( [ command , args , [ resolve , reject ] ] ) ;
else
this . _send ( command , args , [ resolve , reject ] ) ;
} ) ;
}
2015-11-01 17:28:19 -05:00
2017-11-13 01:23:39 -05:00
// ========================================================================
// Topics
// ========================================================================
2018-03-14 13:58:04 -04:00
subscribe ( referrer , ... topics ) {
2017-11-13 01:23:39 -05:00
const t = this . _topics ;
2020-02-10 19:43:35 -05:00
let changed = false ;
2017-11-13 01:23:39 -05:00
for ( const topic of topics ) {
2018-03-14 13:58:04 -04:00
if ( ! t . has ( topic ) ) {
if ( this . connected )
this . _send ( 'sub' , topic ) ;
t . set ( topic , new Set ) ;
2020-02-10 19:43:35 -05:00
changed = true ;
2018-03-14 13:58:04 -04:00
}
2017-11-13 01:23:39 -05:00
2018-03-14 13:58:04 -04:00
const tp = t . get ( topic ) ;
tp . add ( referrer ) ;
2015-11-01 17:28:19 -05:00
}
2020-02-10 19:43:35 -05:00
if ( changed )
this . emit ( ':sub-change' ) ;
2015-11-01 17:28:19 -05:00
}
2015-06-05 03:59:28 -04:00
2018-03-14 13:58:04 -04:00
unsubscribe ( referrer , ... topics ) {
2017-11-13 01:23:39 -05:00
const t = this . _topics ;
2020-02-10 19:43:35 -05:00
let changed = false ;
2017-11-13 01:23:39 -05:00
for ( const topic of topics ) {
2018-03-14 13:58:04 -04:00
if ( ! t . has ( topic ) )
continue ;
2015-02-24 00:33:29 -05:00
2018-03-14 13:58:04 -04:00
const tp = t . get ( topic ) ;
tp . delete ( referrer ) ;
if ( ! tp . size ) {
2020-02-10 19:43:35 -05:00
changed = true ;
2018-03-14 13:58:04 -04:00
t . delete ( topic ) ;
if ( this . connected )
this . _send ( 'unsub' , topic ) ;
}
2017-11-13 01:23:39 -05:00
}
2020-02-10 19:43:35 -05:00
if ( changed )
this . emit ( ':sub-change' ) ;
2017-11-13 01:23:39 -05:00
}
2015-02-24 00:33:29 -05:00
2016-04-11 18:57:25 -04:00
2017-11-13 01:23:39 -05:00
get topics ( ) {
2018-03-14 13:58:04 -04:00
return Array . from ( this . _topics . keys ( ) ) ;
2015-02-24 00:33:29 -05:00
}
2017-11-13 01:23:39 -05:00
}
2023-11-13 20:47:45 -05:00
SocketClient . State = State ;