// 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) { // 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); const loggedInResp = await context.jamClient.OnLoggedIn(msg); // ACTS AS CONTINUATION if (loggedInResp && loggedInResp['CustomUrl']) { handleLoggedInResponse(loggedInResp['CustomUrl']); } } catch (e) { console.log("fallback to old callback", e); const loggedInResp = await context.jamClient.OnLoggedIn( payload.user_id, payload.token, payload.username ); // ACTS AS CONTINUATION if (loggedInResp && loggedInResp['CustomUrl']) { handleLoggedInResponse(loggedInResp['CustomUrl']); } } $.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) { context.JK.ClientUpdateInstance.runCheck( payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size ); } }, 0); } function handleLoggedInResponse(customUrl) { console.log("handleLoggedInResponse", customUrl); setTimeout(function () { if (customUrl) { window.location.href = customUrl; } }, 1000); } 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 $( '' + secs + " " + (secs == 1 ? ' second.s' : "seconds.") + "" ); } 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, product: "JamClientModern", //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);