jam-cloud/web/app/assets/javascripts/jamkazam.js

419 lines
14 KiB
JavaScript

(function (context, $) {
"use strict";
// Change underscore's default templating characters as they
// conflict withe the ERB rendering. Templates will use:
// {{ interpolate }}
// {% evaluate %}
// {{- escape }}
//
context._.templateSettings = {
evaluate: /\{%([\s\S]+?)%\}/g,
interpolate: /\{\{([\s\S]+?)\}\}/g,
escape: /\{\{-([\s\S]+?)\}\}/g
};
context.JK = context.JK || {};
var JamKazam = context.JK.JamKazam = function () {
var app;
var logger = context.JK.logger;
var heartbeatInterval = null;
var heartbeatMS = null;
var heartbeatMissedMS = 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset
var inBadState = false;
var lastHeartbeatAckTime = null;
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features
layoutOpts: {
layoutFooter: true // specify false if you want footer to be left alone
}
};
/**
* Dynamically build routes from markup. Any layout="screen" will get a route corresponding to
* his layout-id attribute. If a layout-arg attribute is present, that will be named as a data
* section of the route.
*/
function routing() {
var routes = context.RouteMap,
rules = {},
rule,
routingContext = {};
$('div[layout="screen"]').each(function () {
var target = $(this).attr('layout-id'),
targetUrl = target,
targetArg = $(this).attr('layout-arg'),
fn = function (data) {
app.layout.changeToScreen(target, data);
};
if (targetArg) {
targetUrl += "/:" + targetArg;
}
rules[target] = {route: '/' + targetUrl + '/:d?', method: target};
routingContext[target] = fn;
});
routes.context(routingContext);
for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]);
$(context).bind('hashchange', routes.handler);
$(routes.handler);
}
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() > heartbeatMissedMS) {
logger.error("no heartbeat ack received from server after ", heartbeatMissedMS, " seconds . giving up on socket connection");
context.JK.JamServer.close(true);
}
else {
lastHeartbeatFound = true;
}
}
function _heartbeat() {
if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat();
context.JK.JamServer.send(message);
lastHeartbeatFound = false;
}
}
function loggedIn(header, payload) {
app.clientId = payload.client_id;
// tell the backend that we have logged in
context.jamClient.OnLoggedIn(payload.user_id, payload.token);
$.cookie('client_id', payload.client_id);
heartbeatMS = payload.heartbeat_interval * 1000;
logger.debug("jamkazam.js.loggedIn(): clientId now " + app.clientId + "; Setting up heartbeat every " + heartbeatMS + " MS");
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
}
function heartbeatAck(header, payload) {
lastHeartbeatAckTime = new Date();
}
/**
* This occurs when the websocket gateway loses a connection to the backend messaging system,
* resulting in severe loss of functionality
*/
function serverBadStateError() {
if (!inBadState) {
inBadState = true;
app.notify({title: "Server Unstable", text: "The server is currently unstable, resulting in feature loss. If you are experiencing any problems, please try to use JamKazam later."})
}
}
/**
* This occurs when the websocket gateway loses a connection to the backend messaging system,
* resulting in severe loss of functionality
*/
function serverBadStateRecovered() {
if (inBadState) {
inBadState = false;
app.notify({title: "Server Recovered", text: "The server is now stable again. If you are still experiencing problems, either create a new music session or restart the client altogether."})
}
}
/**
* This occurs when a new download from a recording has become available
*/
function downloadAvailable() {
context.jamClient.OnDownloadAvailable();
}
/**
* 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?
*/
function socketClosed(in_error) {
// tell the backend that we have logged out
context.jamClient.OnLoggedOut();
// stop future heartbeats
if (heartbeatInterval != null) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
// stop checking for heartbeat acks
if (heartbeatAckCheckInterval != null) {
clearTimeout(heartbeatAckCheckInterval);
heartbeatAckCheckInterval = null;
}
}
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 registerBadStateError() {
logger.debug("register for server_bad_state_error");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError);
}
function registerBadStateRecovered() {
logger.debug("register for server_bad_state_recovered");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered);
}
function registerDownloadAvailable() {
logger.debug("register for download_available");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable);
}
function registerSocketClosed() {
logger.debug("register for socket closed");
context.JK.JamServer.registerOnSocketClosed(socketClosed);
}
/**
* Generic error handler for Ajax calls.
*/
function ajaxError(jqXHR, textStatus, errorMessage) {
logger.error("Unexpected ajax error: " + textStatus);
if (jqXHR.status == 404) {
app.notify({title: "Oops!", text: "What you were looking for is gone now."});
}
else if (jqXHR.status = 422) {
// present a nicer message
try {
var text = "<ul>";
var errorResponse = JSON.parse(jqXHR.responseText)["errors"];
for (var key in errorResponse) {
var errorsForKey = errorResponse[key];
console.log("key: " + key);
var prettyKey = context.JK.entityToPrintable[key];
if (!prettyKey) {
prettyKey = key;
}
for (var i = 0; i < errorsForKey.length; i++) {
text += "<li>" + prettyKey + " " + errorsForKey[i] + "</li>";
}
}
text += "<ul>";
app.notify({title: "Oops!", text: text, "icon_url": "/assets/content/icon_alert_big.png"});
}
catch (e) {
// give up; not formatted correctly
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
}
}
else {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
}
}
/**
* Expose ajaxError.
*/
this.ajaxError = ajaxError;
/**
* Provide a handler object for events related to a particular screen
* being shown or hidden.
* @screen is a string corresponding to the screen's layout-id attribute
* @handler is an object with up to four optional keys:
* beforeHide, afterHide, beforeShow, afterShow, which should all have
* functions as values. If there is data provided by the screen's route
* it will be provided to these functions.
*/
this.bindScreen = function (screen, handler) {
this.layout.bindScreen(screen, handler);
};
this.bindDialog = function (dialog, handler) {
this.layout.bindDialog(dialog, handler);
};
/**
* Allow individual wizard steps to register a function to be invokes
* when they are shown.
*/
this.registerWizardStepFunction = function (stepId, showFunction) {
this.layout.registerWizardStepFunction(stepId, showFunction);
};
/**
* Switch to the wizard step with the provided id.
*/
this.setWizardStep = function (targetStepId) {
this.layout.setWizardStep(targetStepId);
};
/**
* Show a notification. Expects an object with a
* title property and a text property.
*/
this.notify = function (message, descriptor) {
this.layout.notify(message, descriptor);
};
/** Shows an alert notification. Expects text, title */
this.notifyAlert = function (title, text) {
this.notify({title: title, text: text, icon_url: "/assets/content/icon_alert_big.png"});
}
/** Using the standard rails style error object, shows an alert with all seen errors */
this.notifyServerError = function (jqXHR, title) {
if (!title) {
title = "Server Error";
}
if (jqXHR.status == 422) {
var errors = JSON.parse(jqXHR.responseText);
var $errors = context.JK.format_all_errors(errors);
this.notify({title: title, text: $errors, icon_url: "/assets/content/icon_alert_big.png"})
}
else {
if (jqXHR.responseText.indexOf('<!DOCTYPE html>') == 0 || jqXHR.responseText.indexOf('<html')) {
// we need to check more status codes and make tailored messages at this point
var showMore = $('<a href="#">Show Error Detail</a>');
showMore.data('responseText', jqXHR.responseText);
showMore.click(function () {
var self = $(this);
var text = self.data('responseText');
var bodyIndex = text.indexOf('<body');
if(bodyIndex > -1) {
text = text.substr(bodyIndex);
}
console.log("html", text);
$('#server-error-dialog .error-contents').html(text);
app.layout.showDialog('server-error-dialog')
return false;
});
this.notify({title: title, text: showMore, icon_url: "/assets/content/icon_alert_big.png"})
}
else {
this.notify({title: title, text: "status=" + jqXHR.status + ", message=" + jqXHR.responseText, icon_url: "/assets/content/icon_alert_big.png"})
}
}
}
/**
* Initialize any common events.
*/
function events() {
// Hook up the screen navigation controls.
$(".content-nav .arrow-left").click(function (evt) {
evt.preventDefault();
context.history.back();
return false;
});
$(".content-nav .arrow-right").click(function (evt) {
evt.preventDefault();
context.history.forward();
return false;
});
context.JK.popExternalLinks();
}
// Due to timing of initialization, this must be called externally
// after all screens have been given a chance to initialize.
// It is called from index.html.erb after connecting, and initialization
// of other screens.
function initialRouting() {
routing();
var hash = context.location.hash;
try {
context.RouteMap.parse(hash);
}
catch (e) {
console.log("ignoring bogus screen name: %o", hash)
hash = null;
}
var url = '/client#/home';
if (hash) {
url = hash;
}
logger.debug("Changing screen to " + url);
context.location = url;
}
this.unloadFunction = function () {
logger.debug("window.unload function called.");
context.JK.JamServer.close(false);
if (context.jamClient) {
// Unregister for callbacks.
context.jamClient.RegisterRecordingCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
context.jamClient.FTUERegisterVUCallbacks("", "", "");
context.jamClient.FTUERegisterLatencyCallback("");
context.jamClient.RegisterVolChangeCallBack("");
}
};
this.initialize = function (inOpts) {
var url, hash;
app = this;
this.opts = $.extend(opts, inOpts);
this.layout = new context.JK.Layout();
this.layout.initialize(this.opts.layoutOpts);
events();
this.layout.handleDialogState();
if (opts.inClient) {
registerLoginAck();
registerHeartbeatAck();
registerBadStateRecovered();
registerBadStateError();
registerSocketClosed();
registerDownloadAvailable();
context.JK.FaderHelpers.initialize();
context.window.onunload = this.unloadFunction;
}
};
// Holder for a function to invoke upon successfully completing the FTUE.
// See createSession.submitForm as an example.
this.afterFtue = null;
// enable temporary suspension of heartbeat for fine-grained control
this.heartbeatActive = true;
/**
* Expose clientId as a public variable.
* Will be set upon LOGIN_ACK
*/
this.clientId = null;
this.initialRouting = initialRouting;
return this;
};
})(window, jQuery);