1268 lines
40 KiB
JavaScript
1268 lines
40 KiB
JavaScript
// The wrapper around the web-socket connection to the server
|
|
// manages the connection, heartbeats, and reconnect logic.
|
|
// presents itself as a dialog, or in-situ banner (_jamServer.html.haml)
|
|
(function (context, $) {
|
|
"use strict";
|
|
|
|
context.JK = context.JK || {};
|
|
|
|
var logger = context.JK.logger;
|
|
var msg_factory = context.JK.MessageFactory;
|
|
var rest = context.JK.Rest();
|
|
var EVENTS = context.JK.EVENTS;
|
|
|
|
// Let socket.io know where WebSocketMain.swf is
|
|
context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";
|
|
|
|
context.JK.JamServer = function (app, activeElementEvent) {
|
|
// uniquely identify the websocket connection
|
|
var channelId = null;
|
|
var clientType = null;
|
|
var mode = null;
|
|
var os = null;
|
|
var isLatencyTesterMode = false;
|
|
|
|
// heartbeat
|
|
var startHeartbeatTimeout = null;
|
|
var heartbeatInterval = null;
|
|
var heartbeatMS = null;
|
|
var connection_expire_time = null;
|
|
var lastHeartbeatSentTime = null;
|
|
var lastHeartbeatAckTime = null;
|
|
var lastHeartbeatFound = false;
|
|
var lastDisconnectedReason = null;
|
|
var heartbeatAckCheckInterval = null;
|
|
var notificationLastSeenAt = undefined;
|
|
var notificationLastSeen = undefined;
|
|
var clientClosedConnection = false;
|
|
var initialConnectAttempt = true;
|
|
var active = true;
|
|
|
|
// reconnection logic
|
|
var connectDeferred = null;
|
|
var freezeInteraction = false;
|
|
var countdownInterval = null;
|
|
var reconnectAttemptLookup = [2, 2, 2, 4, 8, 15, 30];
|
|
var reconnectAttempt = 0;
|
|
var reconnectingWaitPeriodStart = null;
|
|
var reconnectDueTime = null;
|
|
var connectTimeout = null;
|
|
var activityTimeout;
|
|
|
|
|
|
|
|
// elements
|
|
var $inSituBanner = null;
|
|
var $inSituBannerHolder = null;
|
|
var $messageContents = null;
|
|
var $dialog = null;
|
|
var $templateServerConnection = null;
|
|
var $templateNoLogin = null;
|
|
var $templateDisconnected = null;
|
|
var $currentDisplay = null;
|
|
|
|
var $self = $(this);
|
|
|
|
var server = {};
|
|
server.socket = {};
|
|
server.signedIn = false;
|
|
server.clientID = "";
|
|
server.publicIP = "";
|
|
server.dispatchTable = {};
|
|
server.socketClosedListeners = [];
|
|
server.connecting = false; // is the websocket connection being opened?
|
|
server.connected = false; // is the websocket connection opened AND logged in?
|
|
server.reconnecting = false; // are we beginning the reconnect sequence (which includes an internet health check)
|
|
|
|
function heartbeatStateReset() {
|
|
lastHeartbeatSentTime = null;
|
|
lastHeartbeatAckTime = null;
|
|
lastHeartbeatFound = false;
|
|
}
|
|
|
|
// if activeElementVotes is null, then we are assuming this is the initial connect sequence
|
|
function initiateReconnect(activeElementVotes, in_error) {
|
|
var initialConnect = !!activeElementVotes;
|
|
|
|
freezeInteraction =
|
|
activeElementVotes &&
|
|
((activeElementVotes.dialog &&
|
|
activeElementVotes.dialog.freezeInteraction === true) ||
|
|
(activeElementVotes.screen &&
|
|
activeElementVotes.screen.freezeInteraction === true));
|
|
|
|
if (in_error) {
|
|
reconnectAttempt = 0;
|
|
$currentDisplay = renderDisconnected();
|
|
beginReconnectPeriod();
|
|
}
|
|
}
|
|
|
|
// handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect
|
|
function closedCleanup(in_error) {
|
|
if (isLatencyTester()) {
|
|
logger.info("latency-tester: websocket connection lost");
|
|
}
|
|
|
|
if (server.connected) {
|
|
$self.triggerHandler(EVENTS.CONNECTION_DOWN);
|
|
}
|
|
|
|
server.connected = false;
|
|
server.connecting = false;
|
|
|
|
// stop future heartbeats
|
|
if (heartbeatInterval != null) {
|
|
clearInterval(heartbeatInterval);
|
|
heartbeatInterval = null;
|
|
}
|
|
|
|
// stop the heartbeat start delay from happening
|
|
if (startHeartbeatTimeout != null) {
|
|
clearTimeout(startHeartbeatTimeout);
|
|
startHeartbeatTimeout = null;
|
|
}
|
|
|
|
// stop checking for heartbeat acks
|
|
if (heartbeatAckCheckInterval != null) {
|
|
clearTimeout(heartbeatAckCheckInterval);
|
|
heartbeatAckCheckInterval = null;
|
|
}
|
|
|
|
clearConnectTimeout();
|
|
|
|
// noReconnect is a global to suppress reconnect behavior, so check it first
|
|
|
|
// we don't show any reconnect dialog on the initial connect; so we have this one-time flag
|
|
// to cause reconnects in the case that the websocket is down on the initially connect
|
|
if (server.noReconnect) {
|
|
renderLoginRequired();
|
|
} else if (initialConnectAttempt || !server.reconnecting) {
|
|
server.reconnecting = true;
|
|
initialConnectAttempt = false;
|
|
|
|
var result = activeElementEvent("beforeDisconnect");
|
|
|
|
initiateReconnect(result, in_error);
|
|
|
|
activeElementEvent("afterDisconnect");
|
|
|
|
// notify anyone listening that the socket closed
|
|
var len = server.socketClosedListeners.length;
|
|
for (var i = 0; i < len; i++) {
|
|
try {
|
|
server.socketClosedListeners[i](in_error);
|
|
} catch (ex) {
|
|
logger.warn(
|
|
"exception in callback for websocket closed event:" + ex
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////
|
|
//// HEARTBEAT /////
|
|
////////////////////
|
|
function _heartbeatAckCheck() {
|
|
// if we've seen an ack to the latest heartbeat, don't bother with checking again
|
|
// this makes us resilient to front-end hangs
|
|
if (lastHeartbeatFound) {
|
|
return;
|
|
}
|
|
|
|
// check if the server is still sending heartbeat acks back down
|
|
// this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset
|
|
if (
|
|
new Date().getTime() - lastHeartbeatAckTime.getTime() >
|
|
connection_expire_time
|
|
) {
|
|
logger.error(
|
|
"no heartbeat ack received from server after ",
|
|
connection_expire_time,
|
|
" seconds . giving up on socket connection"
|
|
);
|
|
lastDisconnectedReason = "NO_HEARTBEAT_ACK";
|
|
context.JK.JamServer.close(true);
|
|
} else {
|
|
lastHeartbeatFound = true;
|
|
}
|
|
}
|
|
|
|
function _heartbeat() {
|
|
if (isLatencyTester()) {
|
|
logger.info("latency-tester debug: heartbeat" + app.heartbeatActive);
|
|
}
|
|
if (app.heartbeatActive) {
|
|
//console.log("heartbeat active?: " + active)
|
|
var message = context.JK.MessageFactory.heartbeat(
|
|
notificationLastSeen,
|
|
notificationLastSeenAt,
|
|
active
|
|
);
|
|
notificationLastSeenAt = undefined;
|
|
notificationLastSeen = undefined;
|
|
// for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval
|
|
var now = new Date();
|
|
|
|
if (lastHeartbeatSentTime) {
|
|
var drift =
|
|
new Date().getTime() -
|
|
lastHeartbeatSentTime.getTime() -
|
|
heartbeatMS;
|
|
if (drift > 500) {
|
|
logger.warn(
|
|
"significant drift between heartbeats: " +
|
|
drift +
|
|
"ms beyond target interval"
|
|
);
|
|
}
|
|
}
|
|
lastHeartbeatSentTime = now;
|
|
context.JK.JamServer.send(message);
|
|
lastHeartbeatFound = false;
|
|
}
|
|
}
|
|
|
|
function isClientMode() {
|
|
return mode == "client";
|
|
}
|
|
|
|
function isLatencyTester() {
|
|
return isLatencyTesterMode;
|
|
}
|
|
|
|
function clearConnectTimeout() {
|
|
if (connectTimeout) {
|
|
clearTimeout(connectTimeout);
|
|
connectTimeout = null;
|
|
}
|
|
}
|
|
|
|
function loggedIn(header, payload) {
|
|
console.log('_DEBUG_ loggedIn', payload)
|
|
// reason for setTimeout:
|
|
// loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen
|
|
// but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace...
|
|
|
|
setTimeout(async function () {
|
|
server.signedIn = true;
|
|
server.clientID = payload.client_id;
|
|
server.publicIP = payload.public_ip;
|
|
|
|
if (context.jamClient !== undefined) {
|
|
context.jamClient.connected = true;
|
|
context.jamClient.clientID = server.clientID;
|
|
}
|
|
|
|
clearConnectTimeout();
|
|
|
|
heartbeatStateReset();
|
|
|
|
app.clientId = payload.client_id;
|
|
|
|
if (isClientMode() && context.jamClient) {
|
|
// tell the backend that we have logged in
|
|
try {
|
|
var msg = {
|
|
user_id: payload.user_id,
|
|
token: payload.token,
|
|
username: payload.username,
|
|
arses: payload.arses,
|
|
client_id_int: payload.client_id_int,
|
|
client_id: payload.client_id,
|
|
subscription: payload.subscription,
|
|
};
|
|
if (payload.connection_policy) {
|
|
try {
|
|
msg.policy = JSON.parse(payload.connection_policy);
|
|
} catch (e) {
|
|
msg.policy = null;
|
|
console.log("unable to parse connection policy", e);
|
|
}
|
|
}
|
|
console.log("logged with new msg", msg);
|
|
await context.jamClient.OnLoggedIn(msg); // ACTS AS CONTINUATION
|
|
} catch (e) {
|
|
console.log("fallback to old callback", e);
|
|
await context.jamClient.OnLoggedIn(
|
|
payload.user_id,
|
|
payload.token,
|
|
payload.username
|
|
); // ACTS AS CONTINUATION
|
|
}
|
|
|
|
$.cookie("client_id", payload.client_id);
|
|
}
|
|
|
|
// this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios
|
|
// where there is no device on startup for the current profile.
|
|
// So, in that case, it's possible that a reconnect loop will attempt, but we *do not want*
|
|
// it to go through unless we've passed through .OnLoggedIn
|
|
server.connected = true;
|
|
server.reconnecting = false;
|
|
server.connecting = false;
|
|
initialConnectAttempt = false;
|
|
|
|
heartbeatMS = payload.heartbeat_interval * 1000;
|
|
connection_expire_time = payload.connection_expire_time * 1000;
|
|
logger.info(
|
|
"loggedIn(): clientId=" +
|
|
app.clientId +
|
|
" heartbeat=" +
|
|
payload.heartbeat_interval +
|
|
"s expire_time=" +
|
|
payload.connection_expire_time +
|
|
"s"
|
|
);
|
|
|
|
// add some randomness to help move heartbeats apart from each other
|
|
|
|
// send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time
|
|
var randomStartTime = connection_expire_time * (Math.random() / 2);
|
|
|
|
if (startHeartbeatTimeout) {
|
|
logger.warn("start heartbeat timeout is active; should be null");
|
|
clearTimeout(startHeartbeatTimeout);
|
|
}
|
|
|
|
if (heartbeatInterval != null) {
|
|
logger.warn("heartbeatInterval is active; should be null");
|
|
clearInterval(heartbeatInterval);
|
|
heartbeatInterval = null;
|
|
}
|
|
|
|
if (heartbeatAckCheckInterval != null) {
|
|
logger.warn("heartbeatAckCheckInterval is active; should be null");
|
|
clearInterval(heartbeatAckCheckInterval);
|
|
heartbeatAckCheckInterval = null;
|
|
}
|
|
|
|
startHeartbeatTimeout = setTimeout(function () {
|
|
if (server.connected) {
|
|
heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
|
|
heartbeatAckCheckInterval = context.setInterval(
|
|
_heartbeatAckCheck,
|
|
1000
|
|
);
|
|
lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
|
|
}
|
|
}, randomStartTime);
|
|
|
|
logger.info(
|
|
"starting heartbeat timer in " + randomStartTime / 1000 + "s"
|
|
);
|
|
|
|
connectDeferred.resolve();
|
|
$self.triggerHandler(EVENTS.CONNECTION_UP);
|
|
|
|
activeElementEvent("afterConnect", payload);
|
|
if (payload.client_update && context.JK.ClientUpdateInstance) {
|
|
console.log('_DEBUG_ JamServer before ClientUpdateInstance.runCheck', payload.client_update)
|
|
context.JK.ClientUpdateInstance.runCheck(
|
|
payload.client_update.product,
|
|
payload.client_update.version,
|
|
payload.client_update.uri,
|
|
payload.client_update.size
|
|
);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
function setActive(active) {
|
|
if (context.UserActivityActions) {
|
|
context.UserActivityActions.setActive(active);
|
|
}
|
|
}
|
|
function markAway() {
|
|
logger.debug("sleep again!");
|
|
active = false;
|
|
setActive(active);
|
|
var userStatus = msg_factory.userStatus(false, null);
|
|
server.send(userStatus);
|
|
}
|
|
|
|
function activityCheck() {
|
|
var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes
|
|
active = true;
|
|
setActive(active);
|
|
activityTimeout = setTimeout(markAway, timeoutTime);
|
|
$(document).ready(function () {
|
|
$("body").bind("mousedown keydown touchstart focus", function (event) {
|
|
if (activityTimeout) {
|
|
clearTimeout(activityTimeout);
|
|
activityTimeout = null;
|
|
}
|
|
|
|
if (!active) {
|
|
if (server && server.connected) {
|
|
logger.debug("awake again!");
|
|
var userStatus = msg_factory.userStatus(true, null);
|
|
server.send(userStatus);
|
|
}
|
|
}
|
|
active = true;
|
|
setActive(active);
|
|
activityTimeout = setTimeout(markAway, timeoutTime);
|
|
});
|
|
});
|
|
}
|
|
|
|
function heartbeatAck(header, payload) {
|
|
lastHeartbeatAckTime = new Date();
|
|
}
|
|
|
|
function registerLoginAck() {
|
|
logger.debug("register for loggedIn to set clientId");
|
|
context.JK.JamServer.registerMessageCallback(
|
|
context.JK.MessageType.LOGIN_ACK,
|
|
loggedIn
|
|
);
|
|
}
|
|
|
|
function registerHeartbeatAck() {
|
|
logger.debug("register for heartbeatAck");
|
|
context.JK.JamServer.registerMessageCallback(
|
|
context.JK.MessageType.HEARTBEAT_ACK,
|
|
heartbeatAck
|
|
);
|
|
}
|
|
|
|
function registerSocketClosed() {
|
|
logger.debug("register for socket closed");
|
|
context.JK.JamServer.registerOnSocketClosed(socketClosed);
|
|
}
|
|
|
|
function registerServerRejection() {
|
|
logger.debug("register for server rejection");
|
|
context.JK.JamServer.registerMessageCallback(
|
|
context.JK.MessageType.SERVER_REJECTION_ERROR,
|
|
serverRejection
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared
|
|
* @param in_error did the socket close abnormally?
|
|
*/
|
|
async function socketClosed(in_error) {
|
|
// tell the backend that we have logged out
|
|
if (context.jamClient) {
|
|
await context.jamClient.OnLoggedOut();
|
|
}
|
|
}
|
|
|
|
function serverRejection(header, payload) {
|
|
logger.warn(
|
|
"server rejected our websocket connection. reason=" + payload.error_msg
|
|
);
|
|
|
|
if (payload.error_code == "max_user_connections") {
|
|
context.JK.Banner.showAlert(
|
|
"Too Many Connections",
|
|
"You have too many connections to the server. If you believe this is in error, please contact support."
|
|
);
|
|
} else if (
|
|
payload.error_code == "invalid_login" ||
|
|
payload.error_code == "empty_login"
|
|
) {
|
|
logger.debug(payload.error_code + ": no longer reconnecting");
|
|
server.noReconnect = true; // stop trying to log in!!
|
|
} else if (payload.error_code == "no_reconnect") {
|
|
logger.debug(payload.error_code + ": no longer reconnecting");
|
|
server.noReconnect = true; // stop trying to log in!!
|
|
context.JK.Banner.showAlert(
|
|
"Misbehaved Client",
|
|
"Please restart your application in order to continue using JamKazam."
|
|
);
|
|
}
|
|
}
|
|
|
|
///////////////////
|
|
/// RECONNECT /////
|
|
///////////////////
|
|
function internetUp() {
|
|
var start = new Date().getTime();
|
|
server
|
|
.connect()
|
|
.done(function () {
|
|
guardAgainstRapidTransition(start, finishReconnect);
|
|
})
|
|
.fail(function () {
|
|
guardAgainstRapidTransition(start, closedOnReconnectAttempt);
|
|
});
|
|
}
|
|
|
|
// websocket couldn't connect. let's try again soon
|
|
function closedOnReconnectAttempt() {
|
|
failedReconnect();
|
|
}
|
|
|
|
function finishReconnect() {
|
|
if (!clientClosedConnection) {
|
|
lastDisconnectedReason = "WEBSOCKET_CLOSED_REMOTELY";
|
|
clientClosedConnection = false;
|
|
} else if (!lastDisconnectedReason) {
|
|
// let's have at least some sort of type, however generci
|
|
lastDisconnectedReason = "WEBSOCKET_CLOSED_LOCALLY";
|
|
}
|
|
|
|
if ($currentDisplay.is(".no-websocket-connection")) {
|
|
// this path is the 'not in session path'; so there is nothing else to do
|
|
$currentDisplay.removeClass("active");
|
|
|
|
// TODO: tell certain elements that we've reconnected
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
}
|
|
|
|
function buildOptions() {
|
|
return {};
|
|
}
|
|
|
|
function renderLoginRequired() {
|
|
var $inSituContent = $(
|
|
context._.template($templateNoLogin.html(), buildOptions(), {
|
|
variable: "data",
|
|
})
|
|
);
|
|
$inSituContent.find("a.disconnected-login").click(function () {
|
|
var redirectPath =
|
|
"?redirect-to=" + encodeURIComponent(context.JK.locationPath());
|
|
if (gon.isNativeClient) {
|
|
window.location.href = "/signin" + redirectPath;
|
|
} else {
|
|
window.location.href = "/" + redirectPath;
|
|
}
|
|
return false;
|
|
});
|
|
$messageContents.empty();
|
|
$messageContents.append($inSituContent);
|
|
$inSituBannerHolder.addClass("active");
|
|
return $inSituBannerHolder;
|
|
}
|
|
function renderDisconnected() {
|
|
var content = null;
|
|
if (freezeInteraction) {
|
|
var template = $templateDisconnected.html();
|
|
var templateHtml = $(context.JK.fillTemplate(template, buildOptions()));
|
|
templateHtml
|
|
.find(".reconnect-countdown")
|
|
.html(formatDelaySecs(reconnectDelaySecs()));
|
|
content = context.JK.Banner.show({
|
|
html: templateHtml,
|
|
type: "reconnect",
|
|
});
|
|
} else {
|
|
var $inSituContent = $(
|
|
context._.template($templateServerConnection.html(), buildOptions(), {
|
|
variable: "data",
|
|
})
|
|
);
|
|
$inSituContent
|
|
.find(".reconnect-countdown")
|
|
.html(formatDelaySecs(reconnectDelaySecs()));
|
|
$messageContents.empty();
|
|
$messageContents.append($inSituContent);
|
|
$inSituBannerHolder.addClass("active");
|
|
content = $inSituBannerHolder;
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
function formatDelaySecs(secs) {
|
|
return $(
|
|
'<span class="countdown-seconds"><span class="countdown">' +
|
|
secs +
|
|
"</span> " +
|
|
(secs == 1
|
|
? ' second.<span style="visibility:hidden">s</span>'
|
|
: "seconds.") +
|
|
"</span>"
|
|
);
|
|
}
|
|
|
|
function setCountdown($parent) {
|
|
$parent
|
|
.find(".reconnect-countdown")
|
|
.html(formatDelaySecs(reconnectDelaySecs()));
|
|
}
|
|
|
|
function renderCouldNotReconnect() {
|
|
return renderDisconnected();
|
|
}
|
|
|
|
function renderReconnecting() {
|
|
$currentDisplay
|
|
.find(".reconnect-progress-msg")
|
|
.text("Attempting to reconnect...");
|
|
|
|
if ($currentDisplay.is(".no-websocket-connection")) {
|
|
$currentDisplay
|
|
.find(".disconnected-reconnect")
|
|
.removeClass("reconnect-enabled")
|
|
.addClass("reconnect-disabled");
|
|
} else {
|
|
$currentDisplay
|
|
.find(".disconnected-reconnect")
|
|
.removeClass("button-orange")
|
|
.addClass("button-grey");
|
|
}
|
|
}
|
|
|
|
function failedReconnect() {
|
|
reconnectAttempt += 1;
|
|
$currentDisplay = renderCouldNotReconnect();
|
|
beginReconnectPeriod();
|
|
}
|
|
|
|
function guardAgainstRapidTransition(start, nextStep) {
|
|
var now = new Date().getTime();
|
|
|
|
if (now - start < 1500) {
|
|
setTimeout(function () {
|
|
nextStep();
|
|
}, 1500 - (now - start));
|
|
} else {
|
|
nextStep();
|
|
}
|
|
}
|
|
|
|
function attemptReconnect() {
|
|
if (server.connecting) {
|
|
logger.warn("attemptReconnect called when already connecting");
|
|
return;
|
|
}
|
|
|
|
if (server.connected) {
|
|
logger.warn("attemptReconnect called when already connected");
|
|
return;
|
|
}
|
|
|
|
var start = new Date().getTime();
|
|
|
|
renderReconnecting();
|
|
|
|
rest
|
|
.serverHealthCheck()
|
|
.done(function () {
|
|
guardAgainstRapidTransition(start, internetUp);
|
|
})
|
|
.fail(function (xhr, textStatus, errorThrown) {
|
|
if (xhr && xhr.status >= 100) {
|
|
// we could connect to the server, and it's alive
|
|
guardAgainstRapidTransition(start, internetUp);
|
|
} else {
|
|
guardAgainstRapidTransition(start, failedReconnect);
|
|
}
|
|
});
|
|
|
|
return false;
|
|
}
|
|
|
|
function clearReconnectTimers() {
|
|
if (countdownInterval) {
|
|
clearInterval(countdownInterval);
|
|
countdownInterval = null;
|
|
}
|
|
}
|
|
|
|
function beginReconnectPeriod() {
|
|
// allow user to force reconnect
|
|
$currentDisplay
|
|
.find("a.disconnected-reconnect")
|
|
.unbind("click")
|
|
.click(function () {
|
|
if (
|
|
$(this).is(".button-orange") ||
|
|
$(this).is(".reconnect-enabled")
|
|
) {
|
|
logger.debug("user initiated reconnect");
|
|
clearReconnectTimers();
|
|
attemptReconnect();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
reconnectingWaitPeriodStart = new Date().getTime();
|
|
reconnectDueTime =
|
|
reconnectingWaitPeriodStart + reconnectDelaySecs() * 1000;
|
|
|
|
// update count down timer periodically
|
|
countdownInterval = setInterval(function () {
|
|
var now = new Date().getTime();
|
|
if (now > reconnectDueTime) {
|
|
clearReconnectTimers();
|
|
attemptReconnect();
|
|
} else {
|
|
var secondsUntilReconnect = Math.ceil(
|
|
(reconnectDueTime - now) / 1000
|
|
);
|
|
$currentDisplay
|
|
.find(".reconnect-countdown")
|
|
.html(formatDelaySecs(secondsUntilReconnect));
|
|
}
|
|
}, 333);
|
|
}
|
|
|
|
function reconnectDelaySecs() {
|
|
if (reconnectAttempt > reconnectAttemptLookup.length - 1) {
|
|
return reconnectAttemptLookup[reconnectAttemptLookup.length - 1];
|
|
} else {
|
|
return reconnectAttemptLookup[reconnectAttempt];
|
|
}
|
|
}
|
|
|
|
server.registerOnSocketClosed = function (callback) {
|
|
server.socketClosedListeners.push(callback);
|
|
};
|
|
|
|
server.registerMessageCallback = function (messageType, callback) {
|
|
if (server.dispatchTable[messageType] === undefined) {
|
|
server.dispatchTable[messageType] = [];
|
|
}
|
|
|
|
server.dispatchTable[messageType].push(callback);
|
|
};
|
|
|
|
server.unregisterMessageCallback = function (messageType, callback) {
|
|
if (server.dispatchTable[messageType] !== undefined) {
|
|
for (var i = server.dispatchTable[messageType].length; i--; ) {
|
|
if (server.dispatchTable[messageType][i] === callback) {
|
|
server.dispatchTable[messageType].splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (server.dispatchTable[messageType].length === 0) {
|
|
delete server.dispatchTable[messageType];
|
|
}
|
|
}
|
|
};
|
|
|
|
Promise.allSettled = Promise.allSettled ||
|
|
((promises) =>
|
|
Promise.all(
|
|
promises.map(p => p
|
|
.then((value) => ({
|
|
status: "fulfilled",
|
|
value,
|
|
}))
|
|
.catch((reason) => ({
|
|
status: "rejected",
|
|
reason,
|
|
}))
|
|
)
|
|
));
|
|
|
|
server.connect = function () {
|
|
if (server.connecting) {
|
|
logger.error(
|
|
"server.connect should never be called if we are already connecting. cancelling."
|
|
);
|
|
// XXX should return connectPromise, but needs to be tested/vetted
|
|
return;
|
|
}
|
|
if (server.connected) {
|
|
logger.error(
|
|
"server.connect should never be called if we are already connected. cancelling."
|
|
);
|
|
// XXX should return connectPromise, but needs to be tested/vetted
|
|
return;
|
|
}
|
|
|
|
//const clientTypePromise = context.JK.clientType();
|
|
const clientType = context.JK.clientType();
|
|
|
|
const operatingModePromise = context.jamClient.getOperatingMode();
|
|
const macHashPromise = context.jamClient.SessionGetMacHash();
|
|
//const osStringPromise = context.JK.GetOSAsString();
|
|
const osStringPromise = context.jamClient.GetDetailedOS();
|
|
|
|
const allPromises = [
|
|
//clientTypePromise,
|
|
operatingModePromise,
|
|
macHashPromise,
|
|
osStringPromise,
|
|
];
|
|
|
|
connectDeferred = new $.Deferred();
|
|
|
|
Promise.all(allPromises).then((values) => {
|
|
//clientType = values[0];
|
|
mode = values[0];
|
|
machine = values[1];
|
|
os = values[2];
|
|
console.log("clientType, mode, machine", clientType, mode, machine, os);
|
|
|
|
isLatencyTesterMode = mode == "server";
|
|
|
|
channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection
|
|
|
|
var rememberToken = $.cookie("remember_token");
|
|
|
|
if (isClientMode() && !rememberToken) {
|
|
server.noReconnect = true;
|
|
logger.debug("no login info; shutting down websocket");
|
|
renderLoginRequired();
|
|
connectDeferred.reject();
|
|
return connectDeferred;
|
|
}
|
|
// we will log in one of 3 ways:
|
|
// browser: use session cookie, and auth with token
|
|
// native: use session cookie, and use the token
|
|
// latency_tester: ask for client ID from backend; no token (trusted)
|
|
|
|
if (isClientMode()) {
|
|
var client_id =
|
|
gon.global.env == "development" ? $.cookie("client_id") : null;
|
|
client_type = clientType;
|
|
if (machine) {
|
|
machine = machine.all;
|
|
}
|
|
} else {
|
|
var client_type = "latency_tester";
|
|
var client_id = context.jamClient.clientID;
|
|
var machine = null;
|
|
}
|
|
|
|
var params = {
|
|
channel_id: channelId,
|
|
token: rememberToken,
|
|
client_type: client_type, // isClientMode() ? context.JK.clientType() : 'latency_tester',
|
|
client_id: client_id,
|
|
machine: machine,
|
|
os: os,
|
|
//jamblaster_serial_no: context.PlatformStore.jamblasterSerialNo(),
|
|
udp_reachable: context.JK.StunInstance
|
|
? !context.JK.StunInstance.sync()
|
|
: null, // latency tester doesn't have the stun class loaded
|
|
};
|
|
var uri = context.gon.websocket_gateway_uri + "?" + $.param(params); // Set in index.html.erb.
|
|
console.log("_DEBUG_ connecting websocket: " + uri);
|
|
|
|
server.connecting = true;
|
|
server.socket = new context.WebSocket(uri);
|
|
server.socket.channelId = channelId;
|
|
server.socket.onopen = server.onOpen;
|
|
server.socket.onmessage = server.onMessage;
|
|
server.socket.onclose = server.onClose;
|
|
|
|
connectTimeout = setTimeout(function () {
|
|
logger.debug("connection timeout fired");
|
|
connectTimeout = null;
|
|
|
|
if (connectDeferred.state() === "pending") {
|
|
server.close(true);
|
|
connectDeferred.reject();
|
|
}
|
|
}, 4000);
|
|
});
|
|
|
|
let allSettled = false;
|
|
Promise.allSettled(allPromises).then((results) =>
|
|
results.forEach((result) => {
|
|
console.log(result.status);
|
|
allSettled = true;
|
|
})
|
|
);
|
|
|
|
const intervalTime = 200;
|
|
const interval = setInterval(() => {
|
|
logger.log("waiting for the promises to be settled");
|
|
if (allSettled) {
|
|
clearInterval(interval);
|
|
}
|
|
}, intervalTime);
|
|
|
|
return connectDeferred;
|
|
};
|
|
|
|
// server.connect = function () {
|
|
|
|
// if(server.connecting) {
|
|
// logger.error("server.connect should never be called if we are already connecting. cancelling.")
|
|
// // XXX should return connectPromise, but needs to be tested/vetted
|
|
// return;
|
|
// }
|
|
// if(server.connected) {
|
|
// logger.error("server.connect should never be called if we are already connected. cancelling.")
|
|
// // XXX should return connectPromise, but needs to be tested/vetted
|
|
// return;
|
|
// }
|
|
|
|
// if(!clientType) {
|
|
// clientType = context.JK.clientType();
|
|
// }
|
|
// if(!mode) {
|
|
// mode = 'client'
|
|
// if (context.jamClient && context.jamClient.getOperatingMode) {
|
|
// mode = context.jamClient.getOperatingMode()
|
|
// }
|
|
// isLatencyTesterMode = mode == 'server';
|
|
// }
|
|
|
|
// connectDeferred = new $.Deferred();
|
|
// channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection
|
|
|
|
// var rememberToken = $.cookie("remember_token");
|
|
|
|
// if(isClientMode() && !rememberToken) {
|
|
// server.noReconnect = true;
|
|
// logger.debug("no login info; shutting down websocket");
|
|
// renderLoginRequired();
|
|
// connectDeferred.reject();
|
|
// return connectDeferred;
|
|
// }
|
|
// // we will log in one of 3 ways:
|
|
// // browser: use session cookie, and auth with token
|
|
// // native: use session cookie, and use the token
|
|
// // latency_tester: ask for client ID from backend; no token (trusted)
|
|
|
|
// if (isClientMode()) {
|
|
// var client_type = context.JK.clientType()
|
|
// var client_id = (gon.global.env == "development" ? $.cookie('client_id') : null)
|
|
// var machine = context.jamClient.SessionGetMacHash()
|
|
// if (machine) {
|
|
// machine = machine.all
|
|
// }
|
|
// }
|
|
// else {
|
|
// var client_type = 'latency_tester'
|
|
// var client_id = context.jamClient.clientID
|
|
// var machine = null
|
|
// }
|
|
|
|
// client_type = 'browser'
|
|
|
|
// var params = {
|
|
// channel_id: channelId,
|
|
// token: rememberToken,
|
|
// client_type: client_type, // isClientMode() ? context.JK.clientType() : 'latency_tester',
|
|
// client_id: client_id,
|
|
// machine: machine,
|
|
// os: context.JK.GetOSAsString(),
|
|
// //jamblaster_serial_no: context.PlatformStore.jamblasterSerialNo(),
|
|
// udp_reachable: context.JK.StunInstance ? !context.JK.StunInstance.sync() : null // latency tester doesn't have the stun class loaded
|
|
// }
|
|
// var uri = context.gon.websocket_gateway_uri + '?' + $.param(params); // Set in index.html.erb.
|
|
|
|
// logger.debug("connecting websocket: " + uri);
|
|
|
|
// server.connecting = true;
|
|
// server.socket = new context.WebSocket(uri);
|
|
// server.socket.channelId = channelId;
|
|
// server.socket.onopen = server.onOpen;
|
|
// server.socket.onmessage = server.onMessage;
|
|
// server.socket.onclose = server.onClose;
|
|
|
|
// connectTimeout = setTimeout(function () {
|
|
// logger.debug("connection timeout fired")
|
|
// connectTimeout = null;
|
|
|
|
// if(connectDeferred.state() === 'pending') {
|
|
// server.close(true);
|
|
// connectDeferred.reject();
|
|
// }
|
|
// }, 4000);
|
|
|
|
// return connectDeferred;
|
|
// };
|
|
|
|
server.close = function (in_error) {
|
|
logger.info("closing websocket");
|
|
|
|
clientClosedConnection = true;
|
|
server.socket.close();
|
|
|
|
closedCleanup(in_error);
|
|
};
|
|
|
|
server.rememberLogin = function () {
|
|
var token, loginMessage;
|
|
token = $.cookie("remember_token");
|
|
|
|
loginMessage = msg_factory.login_with_token(token, null, clientType);
|
|
server.send(loginMessage);
|
|
};
|
|
|
|
server.latencyTesterLogin = function () {
|
|
var loginMessage = msg_factory.login_with_client_id(
|
|
context.jamClient.clientID,
|
|
"latency_tester"
|
|
);
|
|
server.send(loginMessage);
|
|
};
|
|
|
|
server.onOpen = function () {
|
|
logger.debug("server.onOpen");
|
|
|
|
// we should receive LOGIN_ACK very soon. we already set a timer elsewhere to give 4 seconds to receive it
|
|
};
|
|
|
|
server.onMessage = function (e) {
|
|
console.log("__ONMESSAGE__", JSON.parse(e.data))
|
|
var message = JSON.parse(e.data),
|
|
messageType = message.type.toLowerCase(),
|
|
payload = message[messageType],
|
|
callbacks = server.dispatchTable[message.type];
|
|
|
|
if (message.type == context.JK.MessageType.PEER_MESSAGE) {
|
|
//logger.info("server.onMessage:" + messageType);
|
|
} else if (
|
|
message.type != context.JK.MessageType.HEARTBEAT_ACK &&
|
|
message.type != context.JK.MessageType.PEER_MESSAGE
|
|
) {
|
|
logger.info(
|
|
"server.onMessage:" +
|
|
messageType +
|
|
" payload:" +
|
|
JSON.stringify(payload)
|
|
);
|
|
}
|
|
|
|
if (callbacks !== undefined) {
|
|
var len = callbacks.length;
|
|
for (var i = 0; i < len; i++) {
|
|
try {
|
|
callbacks[i](message, payload);
|
|
} catch (ex) {
|
|
logger.warn("exception in callback for websocket message:" + ex);
|
|
throw ex;
|
|
}
|
|
}
|
|
} else {
|
|
logger.info("Unexpected message type %s.", message.type);
|
|
}
|
|
};
|
|
|
|
// onClose is called if either client or server closes connection
|
|
server.onClose = function () {
|
|
logger.info("Socket to server closed.");
|
|
|
|
var disconnectedSocket = this;
|
|
|
|
if (disconnectedSocket.channelId != server.socket.channelId) {
|
|
logger.debug(
|
|
" ignoring disconnect for non-current socket. current=" +
|
|
server.socket.channelId +
|
|
", disc=" +
|
|
disconnectedSocket.channelId
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (connectDeferred.state() === "pending") {
|
|
connectDeferred.reject();
|
|
}
|
|
|
|
closedCleanup(true);
|
|
};
|
|
|
|
server.send = function (message) {
|
|
var jsMessage = JSON.stringify(message);
|
|
|
|
if (
|
|
isLatencyTester() &&
|
|
(message.type == context.JK.MessageType.HEARTBEAT ||
|
|
message.type == context.JK.MessageType.PEER_MESSAGE)
|
|
) {
|
|
logger.info("latency-tester: server.send(" + jsMessage + ")");
|
|
} else if (
|
|
message.type != context.JK.MessageType.HEARTBEAT &&
|
|
message.type != context.JK.MessageType.PEER_MESSAGE
|
|
) {
|
|
logger.info("server.send(" + jsMessage + ")");
|
|
}
|
|
if (
|
|
server !== undefined &&
|
|
server.socket !== undefined &&
|
|
server.socket.send !== undefined
|
|
) {
|
|
try {
|
|
server.socket.send(jsMessage);
|
|
} catch (err) {
|
|
logger.warn("error when sending on websocket: " + err);
|
|
}
|
|
} else {
|
|
logger.warn("Dropped message because server connection is closed.");
|
|
}
|
|
};
|
|
|
|
server.loginSession = function (sessionId) {
|
|
var loginMessage;
|
|
|
|
if (!server.signedIn) {
|
|
logger.warn("Not signed in!");
|
|
// TODO: surface the error
|
|
return;
|
|
}
|
|
|
|
loginMessage = msg_factory.login_jam_session(sessionId);
|
|
server.send(loginMessage);
|
|
};
|
|
|
|
/** with the advent of the reliable UDP channel, this is no longer how messages are sent from client-to-clent
|
|
* however, the mechanism still exists and is useful in test contexts; and maybe in the future
|
|
* @param receiver_id client ID of message to send
|
|
* @param message the actual message
|
|
*/
|
|
server.sendP2PMessage = function (receiver_id, message) {
|
|
//logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message);
|
|
//console.time('sendP2PMessage');
|
|
var outgoing_msg = msg_factory.client_p2p_message(
|
|
server.clientID,
|
|
receiver_id,
|
|
message
|
|
);
|
|
server.send(outgoing_msg);
|
|
//console.timeEnd('sendP2PMessage');
|
|
};
|
|
|
|
server.sendLogin = function (token) {
|
|
logger.debug("sending login message on behalf of client");
|
|
var outgoing_msg = msg_factory.login_with_token(token, null, null);
|
|
server.send(outgoing_msg);
|
|
};
|
|
|
|
server.sendLogout = function () {
|
|
logger.debug("sending logout message on behalf of client");
|
|
var outgoing_msg = msg_factory.logout();
|
|
server.send(outgoing_msg);
|
|
};
|
|
|
|
server.sendChatMessage = function (channel, message) {
|
|
if (server.connected) {
|
|
var chatMsg = msg_factory.chatMessage(channel, message);
|
|
server.send(chatMsg);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
server.updateNotificationSeen = function (
|
|
notificationId,
|
|
notificationCreatedAt
|
|
) {
|
|
var time = new Date(notificationCreatedAt);
|
|
|
|
if (!notificationCreatedAt) {
|
|
throw "invalid value passed to updateNotificationSeen";
|
|
}
|
|
|
|
if (!notificationLastSeenAt) {
|
|
notificationLastSeenAt = notificationCreatedAt;
|
|
notificationLastSeen = notificationId;
|
|
logger.debug(
|
|
"updated notificationLastSeenAt with: " + notificationCreatedAt
|
|
);
|
|
} else if (time.getTime() > new Date(notificationLastSeenAt).getTime()) {
|
|
notificationLastSeenAt = notificationCreatedAt;
|
|
notificationLastSeen = notificationId;
|
|
logger.debug(
|
|
"updated notificationLastSeenAt with: " + notificationCreatedAt
|
|
);
|
|
} else {
|
|
logger.debug(
|
|
"ignored notificationLastSeenAt for: " + notificationCreatedAt
|
|
);
|
|
}
|
|
};
|
|
|
|
server.registerMessageCallback(
|
|
context.JK.MessageType.PEER_MESSAGE,
|
|
async function (header, payload) {
|
|
if (context.jamClient !== undefined) {
|
|
await context.jamClient.P2PMessageReceived(
|
|
header.from,
|
|
payload.message
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
server.get$Server = function () {
|
|
return $self;
|
|
};
|
|
|
|
context.JK.JamServer = server;
|
|
|
|
// Callbacks from jamClient
|
|
// if (context.JK.isQWebEngine) {
|
|
// context.jamClient.SendP2PMessage.connect(server.sendP2PMessage);
|
|
|
|
// if (context.jamClient.SendLogin) {
|
|
// context.jamClient.SendLogin.connect(server.sendLogin);
|
|
// context.jamClient.SendLogin.connect(server.sendLogout);
|
|
// }
|
|
// }
|
|
|
|
//connect();
|
|
|
|
async function connect() {
|
|
if (context.JK.isQWebEngine) {
|
|
//await context.jamClient.SendP2PMessage.connect(server.sendP2PMessage);
|
|
//if (context.jamClient.SendLogin) {
|
|
//await context.jamClient.SendLogin.connect(server.sendLogin);
|
|
//await context.jamClient.SendLogin.connect(server.sendLogout);
|
|
//}
|
|
}
|
|
}
|
|
|
|
function initialize() {
|
|
registerLoginAck();
|
|
registerHeartbeatAck();
|
|
registerServerRejection();
|
|
registerSocketClosed();
|
|
activityCheck();
|
|
|
|
$inSituBanner = $(".server-connection");
|
|
$inSituBannerHolder = $(".no-websocket-connection");
|
|
$messageContents = $inSituBannerHolder.find(".message-contents");
|
|
$dialog = $("#banner");
|
|
$templateServerConnection = $("#template-server-connection");
|
|
$templateNoLogin = $("#template-no-login");
|
|
$templateDisconnected = $("#template-disconnected");
|
|
|
|
if ($inSituBanner.length != 1) {
|
|
throw (
|
|
"found wrong number of .server-connection: " + $inSituBanner.length
|
|
);
|
|
}
|
|
if ($inSituBannerHolder.length != 1) {
|
|
throw (
|
|
"found wrong number of .no-websocket-connection: " +
|
|
$inSituBannerHolder.length
|
|
);
|
|
}
|
|
if ($messageContents.length != 1) {
|
|
throw (
|
|
"found wrong number of .message-contents: " + $messageContents.length
|
|
);
|
|
}
|
|
if ($dialog.length != 1) {
|
|
throw "found wrong number of #banner: " + $dialog.length;
|
|
}
|
|
if ($templateServerConnection.length != 1) {
|
|
throw (
|
|
"found wrong number of #template-server-connection: " +
|
|
$templateServerConnection.length
|
|
);
|
|
}
|
|
if ($templateDisconnected.length != 1) {
|
|
throw (
|
|
"found wrong number of #template-disconnected: " +
|
|
$templateDisconnected.length
|
|
);
|
|
}
|
|
}
|
|
|
|
this.initialize = initialize;
|
|
|
|
return this;
|
|
};
|
|
})(window, jQuery);
|