mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.0.0 Beta 1
This commit is contained in:
parent
c2688646af
commit
262757a20d
187 changed files with 22878 additions and 38882 deletions
856
src/socket.js
856
src/socket.js
|
@ -1,485 +1,461 @@
|
|||
var FFZ = window.FrankerFaceZ,
|
||||
constants = require('./constants'),
|
||||
utils = require('./utils'),
|
||||
'use strict';
|
||||
|
||||
pick_server = function(pool) {
|
||||
var total = 0, i = pool.length, val;
|
||||
while(i--)
|
||||
total += pool[i][1];
|
||||
// ============================================================================
|
||||
// Socket Client
|
||||
// This connects to the FrankerFaceZ socket servers for PubSub and RPC.
|
||||
// ============================================================================
|
||||
|
||||
val = Math.random() * total;
|
||||
for(i = 0; i < pool.length; i++) {
|
||||
val -= pool[i][1];
|
||||
import Module from 'utilities/module';
|
||||
import {DEBUG, WS_CLUSTERS} from 'utilities/constants';
|
||||
|
||||
|
||||
export const State = {
|
||||
DISCONNECTED: 0,
|
||||
CONNECTING: 1,
|
||||
CONNECTED: 2
|
||||
}
|
||||
|
||||
|
||||
export default class SocketClient extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.add('socket.cluster', {
|
||||
default: 'Production',
|
||||
|
||||
ui: {
|
||||
path: 'Debugging @{"expanded": false, "sort": 9999} > Socket >> General',
|
||||
title: 'Server Cluster',
|
||||
|
||||
component: 'setting-select-box',
|
||||
|
||||
data: [{
|
||||
value: null,
|
||||
title: 'Disabled'
|
||||
}].concat(Object.keys(WS_CLUSTERS).map(x => ({
|
||||
value: x,
|
||||
title: x
|
||||
})))
|
||||
}
|
||||
});
|
||||
|
||||
this._want_connected = false;
|
||||
|
||||
this._topics = new Set;
|
||||
this._pending = [];
|
||||
this._awaiting = new Map;
|
||||
|
||||
this._socket = null;
|
||||
this._state = 0;
|
||||
this._last_id = 1;
|
||||
|
||||
this._delay = 0;
|
||||
this._last_ping = null;
|
||||
this._time_drift = 0;
|
||||
|
||||
this._host_idx = -1;
|
||||
this._host_pool = -1;
|
||||
|
||||
|
||||
this.settings.on(':changed:socket.cluster', () => {
|
||||
this._host = null;
|
||||
if ( this.disconnected)
|
||||
this.connect();
|
||||
else
|
||||
this.reconnect();
|
||||
});
|
||||
|
||||
this.on(':command:reconnect', this.reconnect, this);
|
||||
|
||||
this.on(':command:do_authorize', challenge => {
|
||||
this.log.warn('Unimplemented: do_authorize', challenge);
|
||||
});
|
||||
|
||||
|
||||
this.enable();
|
||||
}
|
||||
|
||||
|
||||
onEnable() { this.connect() }
|
||||
onDisable() { this.disconnect() }
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Properties
|
||||
// ========================================================================
|
||||
|
||||
get connected() {
|
||||
return this._state === State.CONNECTED;
|
||||
}
|
||||
|
||||
|
||||
get connecting() {
|
||||
return this._state === State.CONNECTING;
|
||||
}
|
||||
|
||||
|
||||
get disconnected() {
|
||||
return this._state === State.DISCONNECTED;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Connection Logic
|
||||
// ========================================================================
|
||||
|
||||
selectHost() {
|
||||
const cluster_id = this.settings.get('socket.cluster'),
|
||||
cluster = WS_CLUSTERS[cluster_id];
|
||||
|
||||
if ( ! cluster || ! cluster.length )
|
||||
return null;
|
||||
|
||||
let total = 0, i = cluster.length, l = i;
|
||||
while(i-- > 0)
|
||||
total += cluster[i][1];
|
||||
|
||||
let val = Math.random() * total;
|
||||
for(let i=0; i < l; i++) {
|
||||
val -= cluster[i][1];
|
||||
if ( val <= 0 )
|
||||
return i;
|
||||
return cluster[i][0];
|
||||
}
|
||||
|
||||
return pool.length - 1;
|
||||
};
|
||||
return cluster[l-1][0];
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype._ws_open = false;
|
||||
FFZ.prototype._ws_delay = 0;
|
||||
FFZ.prototype._ws_host_idx = -1;
|
||||
FFZ.prototype._ws_current_pool = -1;
|
||||
FFZ.prototype._ws_last_ping = null;
|
||||
_reconnect() {
|
||||
if ( ! this._reconnect_timer ) {
|
||||
if ( this._delay < 60000 )
|
||||
this._delay += (Math.floor(Math.random() * 10) + 5) * 1000;
|
||||
else
|
||||
this._delay = (Math.floor(Math.random() * 60) + 30) * 1000;
|
||||
|
||||
FFZ.prototype._ws_server_offset = null;
|
||||
this._reconnect_timer = setTimeout(() => {
|
||||
this.connect();
|
||||
}, this._delay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.ws_commands = {};
|
||||
FFZ.ws_on_close = [];
|
||||
reconnect() {
|
||||
this.disconnect();
|
||||
this._reconnect();
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// Settings
|
||||
// ----------------
|
||||
connect() {
|
||||
this._want_connected = true;
|
||||
|
||||
/*var ffz_socket_seed;
|
||||
if ( this._reconnect_timer ) {
|
||||
clearTimeout(this._reconnect_timer);
|
||||
this._reconnect_timer = null;
|
||||
}
|
||||
|
||||
try {
|
||||
ffz_socket_seed = JSON.parse(localStorage.ffz_socket_seed);
|
||||
} catch(err) { }
|
||||
|
||||
if ( ! ffz_socket_seed ) {
|
||||
ffz_socket_seed = Math.random();
|
||||
localStorage.ffz_socket_seed = JSON.stringify(ffz_socket_seed);
|
||||
}*/
|
||||
|
||||
|
||||
FFZ.settings_info.socket_server_pool = {
|
||||
type: "select",
|
||||
options: {
|
||||
0: "Disabled",
|
||||
1: "Production",
|
||||
2: "Development"
|
||||
},
|
||||
|
||||
value: 1,
|
||||
|
||||
process_value: function(val) {
|
||||
if ( typeof val === "string" )
|
||||
return parseInt(val) || 0;
|
||||
return val;
|
||||
},
|
||||
|
||||
visible: function() { return (localStorage.getItem('ffz_socket_server_pool') !== null && this.settings.socket_server_pool !== 1) || this.settings.developer_mode || (Date.now() - parseInt(localStorage.ffzLastDevMode || "0")) < 604800000; },
|
||||
|
||||
category: "Debugging",
|
||||
name: "Socket Server Cluster",
|
||||
help: "Select which cluster of socket servers to connect to.",
|
||||
|
||||
on_update: function(val) {
|
||||
if ( val === this._ws_current_pool )
|
||||
if ( ! this.disconnected )
|
||||
return;
|
||||
|
||||
const host = this._host = this._host || this.selectHost();
|
||||
if ( ! host )
|
||||
return;
|
||||
|
||||
this._state = State.CONNECTING;
|
||||
this._last_id = 1;
|
||||
|
||||
this._delay = 0;
|
||||
this._last_ping = null;
|
||||
|
||||
this.log.info(`Using Server: ${host}`);
|
||||
|
||||
let ws;
|
||||
|
||||
try {
|
||||
this._ws_sock.close();
|
||||
} catch(err) { }
|
||||
|
||||
this._ws_open = false;
|
||||
this._ws_delay = 0;
|
||||
this._ws_host_idx = -1;
|
||||
|
||||
if ( this._ws_recreate_timer ) {
|
||||
clearTimeout(this._ws_recreate_timer);
|
||||
this._ws_recreate_timer = null;
|
||||
}
|
||||
|
||||
if ( val === 0 )
|
||||
ws = this._socket = new WebSocket(host);
|
||||
} catch(err) {
|
||||
this._state = State.DISCONNECTED;
|
||||
this._reconnect();
|
||||
this.log.error('Unable to create WebSocket.', err);
|
||||
return;
|
||||
|
||||
this.ws_create();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ----------------
|
||||
// Subscription
|
||||
// ----------------
|
||||
|
||||
FFZ.prototype.ws_sub = function(topic) {
|
||||
this._ws_topics = this._ws_topics || {};
|
||||
if ( this._ws_topics[topic] )
|
||||
return true;
|
||||
|
||||
if ( ! this._ws_open )
|
||||
return false;
|
||||
|
||||
this.ws_send("sub", topic);
|
||||
this._ws_topics[topic] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.ws_unsub = function(topic) {
|
||||
this._ws_topics = this._ws_topics || {};
|
||||
if ( ! this._ws_topics[topic] )
|
||||
return true;
|
||||
|
||||
if ( ! this._ws_open )
|
||||
return true;
|
||||
|
||||
this.ws_send("unsub", topic);
|
||||
this._ws_topics[topic] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// Socket Creation
|
||||
// ----------------
|
||||
|
||||
FFZ.prototype.ws_create = function() {
|
||||
var f = this, ws;
|
||||
|
||||
this._ws_last_req = 1;
|
||||
this._ws_callbacks = {1: f._ws_on_hello.bind(f)};
|
||||
this._ws_pending = this._ws_pending || [];
|
||||
this._ws_topics = {};
|
||||
this._ws_recreate_timer = null;
|
||||
|
||||
var pool_id = this.settings.socket_server_pool,
|
||||
pool = constants.WS_SERVER_POOLS[pool_id];
|
||||
|
||||
this._ws_current_pool = pool_id;
|
||||
|
||||
if ( ! pool )
|
||||
return;
|
||||
|
||||
if ( this._ws_host_idx < 0 )
|
||||
this._ws_host_idx = pick_server(pool);
|
||||
|
||||
var server = pool[this._ws_host_idx][0];
|
||||
|
||||
this.log("Using Socket Server: " + server + " [" + pool_id + ":" + this._ws_host_idx + "]");
|
||||
|
||||
try {
|
||||
ws = this._ws_sock = new WebSocket(server);
|
||||
} catch(err) {
|
||||
this._ws_exists = false;
|
||||
return this.log("Error Creating WebSocket: " + err);
|
||||
}
|
||||
|
||||
this._ws_exists = true;
|
||||
|
||||
ws.onopen = function(e) {
|
||||
f._ws_open = true;
|
||||
f._ws_authenticated = false;
|
||||
f._ws_delay = 0;
|
||||
f.log("Socket Connected.");
|
||||
|
||||
// Hard-code the first command.
|
||||
f._ws_ping_time = window.performance ? performance.now() : Date.now();
|
||||
ws.send("1 hello " + JSON.stringify(["ffz_" + FFZ.version_info, localStorage.ffzClientId]));
|
||||
|
||||
var user = f.get_user();
|
||||
if ( user )
|
||||
f.ws_send("setuser", user.login);
|
||||
|
||||
// Join the right channel if we're in the dashboard.
|
||||
if ( f.is_dashboard ) {
|
||||
var match = location.pathname.match(/\/([^\/]+)/);
|
||||
if ( match )
|
||||
f.ws_sub("channel." + match[1]);
|
||||
}
|
||||
|
||||
// Send the current rooms.
|
||||
for(var room_id in f.rooms) {
|
||||
var room = f.rooms[room_id];
|
||||
if ( ! f.rooms.hasOwnProperty(room_id) || ! room )
|
||||
continue;
|
||||
ws.onopen = () => {
|
||||
this._state = State.CONNECTED;
|
||||
this._sent_user = false;
|
||||
|
||||
if ( room.important ) {
|
||||
f.ws_sub("room." + room_id);
|
||||
this.log.info('Connected.');
|
||||
|
||||
/*if ( room.needs_history ) {
|
||||
room.needs_history = false;
|
||||
if ( ! f.has_bttv && f.settings.chat_history )
|
||||
f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id));
|
||||
}*/
|
||||
}
|
||||
}
|
||||
// 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',
|
||||
[`ffz_${window.FrankerFaceZ.version_info}`, this.settings.provider.get('client-id')],
|
||||
(success, data) => {
|
||||
if ( ! success )
|
||||
return this.log.warn('Error Saying Hello', data);
|
||||
|
||||
// Send the channel(s).
|
||||
if ( f._cindex ) {
|
||||
var channel_id = f._cindex.get('channel.id'),
|
||||
hosted_id = f._cindex.get('channel.hostModeTarget.id');
|
||||
this._on_pong(false, success, data[1]);
|
||||
this.settings.provider.set('client-id', data[0]);
|
||||
this.log.info('Client ID:', data[0]);
|
||||
});
|
||||
|
||||
if ( channel_id )
|
||||
f.ws_sub("channel." + channel_id);
|
||||
|
||||
if ( hosted_id )
|
||||
f.ws_sub("channel." + hosted_id);
|
||||
}
|
||||
// Grab the current user from the site.
|
||||
const site = this.resolve('site'),
|
||||
send_user = () => {
|
||||
if ( this._sent_user || ! this.connected )
|
||||
return;
|
||||
|
||||
// Send any pending commands.
|
||||
var pending = f._ws_pending;
|
||||
f._ws_pending = [];
|
||||
const user = site.getUser();
|
||||
if ( user && user.login ) {
|
||||
this._sent_user = true;
|
||||
this._send('setuser', user.login);
|
||||
|
||||
for(var i=0; i < pending.length; i++) {
|
||||
var d = pending[i];
|
||||
f.ws_send(d[0], d[1], d[2]);
|
||||
}
|
||||
|
||||
// If reconnecting, get the backlog that we missed.
|
||||
if ( f._ws_offline_time ) {
|
||||
var timestamp = f._ws_offline_time;
|
||||
delete f._ws_offline_time;
|
||||
f.ws_send("ready", timestamp);
|
||||
} else {
|
||||
f.ws_send("ready", 0);
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = function() {
|
||||
if ( ! f._ws_offline_time ) {
|
||||
f._ws_offline_time = new Date().getTime();
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = function(e) {
|
||||
var was_open = f._ws_open;
|
||||
f.log("Socket closed. (Code: " + e.code + ", Reason: " + e.reason + ")");
|
||||
|
||||
// If a recreate is already scheduled, this is expected.
|
||||
if ( f._ws_recreate_timer )
|
||||
return;
|
||||
|
||||
f._ws_open = false;
|
||||
if ( ! f._ws_offline_time ) {
|
||||
f._ws_offline_time = new Date().getTime();
|
||||
}
|
||||
|
||||
// When the connection closes, run our callbacks.
|
||||
for (var i=0; i < FFZ.ws_on_close.length; i++) {
|
||||
try {
|
||||
FFZ.ws_on_close[i].bind(f)();
|
||||
} catch(err) {
|
||||
f.log("Error on Socket Close Callback: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle selected server if our last attempt to connect didn't
|
||||
// actually connect.
|
||||
if ( ! was_open )
|
||||
// Actually, let's get a random new server instead.
|
||||
f._ws_host_idx = -1; //(f._ws_host_idx + 1) % pool.length;
|
||||
|
||||
// We never ever want to not have a socket.
|
||||
if ( f._ws_delay < 60000 )
|
||||
f._ws_delay += (Math.floor(Math.random()*10) + 5) * 1000;
|
||||
else
|
||||
// Randomize delay.
|
||||
f._ws_delay = (Math.floor(Math.random()*60)+30)*1000;
|
||||
|
||||
f._ws_recreate_timer = setTimeout(f.ws_create.bind(f), f._ws_delay);
|
||||
}
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
// Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA]
|
||||
var cmd, data, ind = e.data.indexOf(" "),
|
||||
msg = e.data.substr(ind + 1),
|
||||
request = parseInt(e.data.slice(0, ind));
|
||||
|
||||
ind = msg.indexOf(" ");
|
||||
if ( ind === -1 )
|
||||
ind = msg.length;
|
||||
|
||||
cmd = msg.slice(0, ind);
|
||||
msg = msg.substr(ind + 1);
|
||||
if ( msg )
|
||||
data = JSON.parse(msg);
|
||||
|
||||
if ( request === -1 ) {
|
||||
// It's a command from the server.
|
||||
var command = FFZ.ws_commands[cmd];
|
||||
if ( command )
|
||||
command.bind(f)(data);
|
||||
else
|
||||
f.log("Invalid command: " + cmd, data, false, true);
|
||||
|
||||
} /*else if ( cmd === "error" ) {
|
||||
f.log("Socket server reported error: " + data);
|
||||
if (f._ws_callbacks[request] )
|
||||
delete f._ws_callbacks[request];
|
||||
|
||||
}*/ else {
|
||||
var success = cmd === 'ok',
|
||||
has_callback = typeof f._ws_callbacks[request] === "function";
|
||||
|
||||
if ( ! has_callback ) {
|
||||
if ( ! success || constants.DEBUG)
|
||||
f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data, false, true);
|
||||
|
||||
} else {
|
||||
try {
|
||||
f._ws_callbacks[request](success, data);
|
||||
} catch(err) {
|
||||
f.error("Callback for " + request + ": " + err);
|
||||
} else if ( ! site.enabled )
|
||||
this.once('site:enabled', send_user, this);
|
||||
}
|
||||
|
||||
delete f._ws_callbacks[request];
|
||||
send_user();
|
||||
|
||||
|
||||
// Subscribe to Topics
|
||||
for(const topic of this._topics)
|
||||
this._send('sub', topic);
|
||||
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
if ( ! this._offline_time )
|
||||
this._offline_time = Date.now();
|
||||
}
|
||||
|
||||
ws.onclose = event => {
|
||||
const old_state = this._state;
|
||||
this.log.info(`Disconnected. (${event.code}:${event.reason})`);
|
||||
|
||||
this._state = State.DISCONNECTED;
|
||||
|
||||
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);
|
||||
|
||||
} catch(error) {
|
||||
this.log.warn(`Callback Error #${cmd_id}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
ws.onmessage = event => {
|
||||
// Format:
|
||||
// -1 <cmd_name>[ <json_data>]
|
||||
// <reply-id> <ok/err>[ <json_data>]
|
||||
|
||||
const raw = event.data,
|
||||
idx = raw.indexOf(' ');
|
||||
|
||||
if ( idx === -1 )
|
||||
return this.log.warn('Malformed message from server.', event.data);
|
||||
|
||||
const reply = parseInt(raw.slice(0, idx), 10),
|
||||
ix2 = raw.indexOf(' ', idx + 1),
|
||||
|
||||
cmd = raw.slice(idx+1, ix2 === -1 ? raw.length : ix2),
|
||||
data = ix2 === -1 ? undefined : JSON.parse(raw.slice(ix2+1));
|
||||
|
||||
if ( reply === -1 ) {
|
||||
this.log.debug(`Received Command: ${cmd}`, data);
|
||||
this.emit(`:command:${cmd}`, data);
|
||||
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FFZ.prototype.ws_send = function(func, data, callback, can_wait) {
|
||||
if ( ! this._ws_open ) {
|
||||
if ( can_wait ) {
|
||||
var pending = this._ws_pending = this._ws_pending || [];
|
||||
pending.push([func, data, callback]);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
disconnect() {
|
||||
this._want_connected = false;
|
||||
|
||||
var request = ++this._ws_last_req;
|
||||
data = data !== undefined ? " " + JSON.stringify(data) : "";
|
||||
|
||||
if ( callback )
|
||||
this._ws_callbacks[request] = callback;
|
||||
|
||||
try {
|
||||
this._ws_sock.send(request + " " + func + data);
|
||||
} catch(err) {
|
||||
this.log("Socket Send Error: " + err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// HELLO Response
|
||||
// ----------------
|
||||
|
||||
FFZ.prototype._ws_on_hello = function(success, data) {
|
||||
if ( ! success )
|
||||
return this.log("Error Saying Hello: " + data);
|
||||
|
||||
this._ws_on_pong(false, success, data[1]);
|
||||
|
||||
localStorage.ffzClientId = data[0];
|
||||
this.log("Client ID: " + localStorage.ffzClientId);
|
||||
}
|
||||
|
||||
|
||||
// -----------------
|
||||
// Time Calculation
|
||||
// -----------------
|
||||
|
||||
FFZ.prototype.setup_time = function() {
|
||||
var last_time = Date.now(),
|
||||
f = this;
|
||||
|
||||
setInterval(function() {
|
||||
var new_time = Date.now(),
|
||||
difference = (new_time - last_time) - 5000;
|
||||
|
||||
last_time = new_time;
|
||||
if ( Math.abs(difference) > 1000 ) {
|
||||
f.log("WARNING! Time drift of " + difference + "ms across 5 seconds. Did the local time change?");
|
||||
f._ws_server_offset = null;
|
||||
f.ws_ping();
|
||||
if ( this._reconnect_timer ) {
|
||||
clearTimeout(this._reconnect_timer);
|
||||
this._reconnect_timer = null;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
FFZ.prototype.ws_ping = function(skip_log) {
|
||||
// Only 1 ping at a time.
|
||||
if ( this._ws_ping_time )
|
||||
return;
|
||||
if ( this.disconnected )
|
||||
return;
|
||||
|
||||
this._ws_ping_time = window.performance ? performance.now() : Date.now();
|
||||
if ( ! this.ws_send("ping", undefined, this._ws_on_pong.bind(this, skip_log)) )
|
||||
this._ws_ping_time = null;
|
||||
}
|
||||
|
||||
FFZ.prototype._ws_on_pong = function(skip_log, success, server_time) {
|
||||
var d_now = Date.now(),
|
||||
now = window.performance ? performance.now() : d_now;
|
||||
|
||||
if ( ! success ) {
|
||||
this._ws_ping_time = null;
|
||||
if ( ! skip_log )
|
||||
this.log("Error Pinging Server: " + server_time);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this._ws_ping_time ) {
|
||||
var rtt = now - this._ws_ping_time,
|
||||
ping = this._ws_last_ping = rtt / 2;
|
||||
|
||||
this._ws_ping_time = null;
|
||||
this._ws_server_offset = (d_now - (server_time + ping));
|
||||
|
||||
if ( ! skip_log ) {
|
||||
this.log("Server Time: " + new Date(server_time).toISOString());
|
||||
this.log("Local Time: " + new Date(d_now).toISOString());
|
||||
this.log("Estimated Ping: " + ping + "ms");
|
||||
this.log("Time Offset: " + (this._ws_server_offset < 0 ? "-" : "") + utils.time_to_string(Math.abs(this._ws_server_offset) / 1000));
|
||||
|
||||
if ( Math.abs(this._ws_server_offset) > 300000 ) {
|
||||
this.log("WARNING! The time offset with the server is greater than 5 minutes.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// Reconnect Logic
|
||||
// ----------------
|
||||
|
||||
FFZ.ws_commands.reconnect = function() {
|
||||
this.log("Socket Reconnect Command Received");
|
||||
|
||||
// Set the socket as closed and close it.
|
||||
this._ws_open = false;
|
||||
this._ws_sock.close();
|
||||
|
||||
// Socket Close Callbacks
|
||||
for(var i=0; i < FFZ.ws_on_close.length; i++) {
|
||||
try {
|
||||
FFZ.ws_on_close[i].call(this);
|
||||
} catch(err) {
|
||||
this.log("Error on Socket Close Callback: " + err);
|
||||
this._socket.close();
|
||||
} catch(err) { /* if this caused an exception, we don't care -- it's still closed */ }
|
||||
|
||||
this._socket = null;
|
||||
this._state = State.DISCONNECTED;
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Latency
|
||||
// ========================================================================
|
||||
|
||||
_on_pong(skip_log, success, data) {
|
||||
const now = performance.now();
|
||||
|
||||
if ( ! success ) {
|
||||
this._ping_time = null;
|
||||
if ( ! skip_log )
|
||||
this.log.warn('Error Pinging Server', data);
|
||||
|
||||
} else if ( this._ping_time ) {
|
||||
const d_now = Date.now(),
|
||||
rtt = now - this._ping_time,
|
||||
ping = this._last_ping = rtt / 2;
|
||||
|
||||
this._ping_time = null;
|
||||
const drift = this._time_drift = d_now - (data + ping);
|
||||
|
||||
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}`);
|
||||
|
||||
if ( Math.abs(drift) > 300000 )
|
||||
this.log.warn('Local time differs from server time by more than 5 minutes.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Randomize the reconnect delay to avoid a complete hammering.
|
||||
this._ws_delay = Math.floor(Math.random() * 5) * 1000;
|
||||
this._ws_recreate_timer = setTimeout(this.ws_create.bind(this), this._ws_delay);
|
||||
|
||||
ping(skip_log) {
|
||||
if ( this._ping_time || ! this.connected )
|
||||
return;
|
||||
|
||||
this._ping_time = performance.now();
|
||||
this._send('ping', undefined, (s,d) => this._on_pong(skip_log, s, d));
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Communication
|
||||
// ========================================================================
|
||||
|
||||
_send(command, args, callback) {
|
||||
if ( args.length === 1 )
|
||||
args = args[0];
|
||||
|
||||
if ( ! this.connected )
|
||||
return this.log.warn(`Tried sending command "${command}" while disconnected.`);
|
||||
|
||||
const cmd_id = this._last_id++;
|
||||
if ( callback )
|
||||
this._awaiting.set(cmd_id, callback);
|
||||
|
||||
this._socket.send(`${cmd_id} ${command}${args !== undefined ? ` ${JSON.stringify(args)}` : ''}`);
|
||||
}
|
||||
|
||||
|
||||
send(command, ...args) {
|
||||
if ( args.length === 1 )
|
||||
args = args[0];
|
||||
|
||||
if ( ! this.connected )
|
||||
this._pending.push([command, args]);
|
||||
else
|
||||
this._send(command, args);
|
||||
}
|
||||
|
||||
|
||||
call(command, ...args) {
|
||||
if ( args.length === 1 )
|
||||
args = args[0];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if ( ! this.connected )
|
||||
this._pending.push([command, args, [resolve, reject]]);
|
||||
else
|
||||
this._send(command, args, [resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Topics
|
||||
// ========================================================================
|
||||
|
||||
subscribe(...topics) {
|
||||
const t = this._topics;
|
||||
for(const topic of topics) {
|
||||
if ( this.connected && ! t.has(topic) )
|
||||
this._send('sub', topic);
|
||||
|
||||
t.add(topic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
unsubscribe(...topics) {
|
||||
const t = this._topics;
|
||||
for(const topic of topics) {
|
||||
if ( this.connected && t.has(topic) )
|
||||
this._send('unsub', topic);
|
||||
|
||||
t.delete(topic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get topics() {
|
||||
return Array.from(this._topics);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ----------------
|
||||
// Authorization
|
||||
// ----------------
|
||||
|
||||
FFZ.ws_commands.do_authorize = function(data) {
|
||||
// Try finding a channel we can send on.
|
||||
var conn;
|
||||
for(var room_id in this.rooms) {
|
||||
if ( ! this.rooms.hasOwnProperty(room_id) )
|
||||
continue;
|
||||
|
||||
var r = this.rooms[room_id],
|
||||
c = r && r.room && r.room.tmiRoom && r.room.tmiRoom._getConnection();
|
||||
|
||||
if ( c.isConnected ) {
|
||||
conn = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( conn )
|
||||
conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data);
|
||||
else
|
||||
// Try again shortly.
|
||||
setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 1000);
|
||||
}
|
||||
SocketClient.State = State;
|
Loading…
Add table
Add a link
Reference in a new issue