Mercurial > hg > dml-open-vis
view src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js @ 1:f38015048f48 tip
Added GPL
author | Daniel Wolff |
---|---|
date | Sat, 13 Feb 2016 20:43:38 +0100 |
parents | 493bcb69166c |
children |
line wrap: on
line source
"use strict"; /** * The module wraps all API calls to the data server and abstracts JSONP calls. * * API spec: https://code.soundsoftware.ac.uk/projects/dml/wiki/Vis_API_-_front_end_~_data_server * * * Module options: * * apiRootPaths * A pool of URLs from where the data are obtained. Multiple entry points (domains) can be used. * The choice of a particular root path is "random", i.e. this is a "hash" function of a query contents: * "doThis?x=1&y=1" -> path 1 of 3 * "doThat?x=1&y=1" -> path 3 * "doThat?x=1&y=123" -> path 1 * "doThat?x=1&y=2" -> path 2 * ... * "doThis?x=1&y=1" -> path 1 * * The same query is always requested from the same instance (domain) to facilitate HTTP caching. * // instead of http:// and https:// can be used for protocol auto-selection. * Trailing slash is compulsory. * * Examples: * ["//127.0.0.1/"], * ["//127.0.0.1/my-path/to/dml-api/"], * ["//a.example.com/dml-api/", "//b.example.com/dml-api/", "//c.example.com/dml-api/"], * * apiVersion * A natural number representing the version of the data API (i.e. the available features of the interface) * * Examples: * 1 * 42 * * dataVersion * A natural number or a string representing the version of the selected music library. * As API calls are supplemented with dv=[dataVersion], a change in the value of dataVersion drops the HTTP cache. * dataVersion is delivered to the front end with the contents of the web page. * * Examples: * 42 * "2015-01-01" * */ App.module("DataModule.CliopatriaAPI", function(CliopatriaAPI, App, Backbone, Marionette, $, _, Logger) { // Prevent auto start CliopatriaAPI.startWithParent = false; // Define module options var defaultModuleOptions = { // if a request is not returned in this number of ms, handle it as failed callbackTimeout: 30000, // expired requests are being checked this often (ms) callbackTimeoutCheckingInterval: 1000, // callback of JSONP responses callbackFunctionName: "jsonp_cb", // optional data version suffix that is appended to all API requests, e.g. "123" → &dv=123 dataVersion: null, apiRootPaths: [], apiVersion: 1, dataCaching: true }; // ========================================================================= // private variables var logger = null; /** * derived from CliopatriaAPI.options.dataVersion */ var dataVersionSuffix = null; /** * Real API root paths include API version (e.g. http://example.com/path/to/api/v42/) */ var realAPIRootPaths = null; /** * The number of the real API paths */ var realAPIRootPathsCount = null; /** * query pool: an object where keys correspond to sent queries (methodName?param1=1&...&dv=42) and values are: * [timeSent, methodName, parameters, callbacks, scriptElement, query] */ var queryPool = null; /** * Generates query from API method name and parameters (+ appends data version) * Parameters are sorted alphabetically, except dv, which is always the last */ var generateQuery = null; /** * a function that is called every callbackTimeoutCheckingInterval ms and checks for expired query pool entries */ var callbackTimeoutExpiryChecker = null; /** * timeout (interval) handler for callbackTimeoutExpiryChecker */ var callbackTimeoutExpiryCheckerId = null; /** * Initialization checker */ var assertModuleIsInitialized = function() { if (!logger) { throw "DataModule.CliopatriaAPI has not been initialized"; } }; /** * Module initializer * */ CliopatriaAPI.addInitializer(function(options) { CliopatriaAPI.options = _.extend(defaultModuleOptions, options); logger = Logger.get("DataModule.CliopatriaAPI"); //logger.setLevel(Logger.DEBUG); //logger.setLevel(Logger.INFO); // Set data version suffix (common for all requests) if (CliopatriaAPI.options.dataVersion) { //dataVersionSuffix = _.str.sprintf("&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion)); dataVersionSuffix = _.str.sprintf("&format=jsonp&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion)); } else { //dataVersionSuffix = ""; dataVersionSuffix = "&format=jsonp"; } realAPIRootPaths = []; for (var i = 0; i < CliopatriaAPI.options.apiRootPaths.length; ++i) { var apiRootPath = CliopatriaAPI.options.apiRootPaths[i]; realAPIRootPaths.push(_.str.sprintf("%sv%s/", apiRootPath, CliopatriaAPI.options.apiVersion)); } realAPIRootPathsCount = realAPIRootPaths.length; if (!realAPIRootPathsCount) { logger.error("DataModule.CliopatriaAPI has no registered API paths"); } queryPool = {}; // A function that generates a query by the method name and the parameters generateQuery = function(methodName, parameters) { // Sort parameters var sortedParameterPairs = []; for (var parameterName in parameters) { sortedParameterPairs.push([parameterName, parameters[parameterName]]); } sortedParameterPairs.sort(function(a, b) {return _.str.naturalCmp(a[0], b[0]);}); // encode them var encodedParametersAsArrayOfStrings = []; for (var parameterPairIndex in sortedParameterPairs) { encodedParametersAsArrayOfStrings.push(encodeURIComponent(sortedParameterPairs[parameterPairIndex][0]) + "=" + encodeURIComponent(sortedParameterPairs[parameterPairIndex][1])); }; if (!CliopatriaAPI.options.dataCaching) { encodedParametersAsArrayOfStrings.push("random="+Math.round(Math.random() * 100000)); } var encodedParameters = encodedParametersAsArrayOfStrings.join("&"); // append data version (dv) if (methodName == "getCollectionId") { if (encodedParameters.length) { encodedParameters += dataVersionSuffix; } else { encodedParameters += dataVersionSuffix.slice(1); } } return methodName + "?" + encodedParameters; }; // A function that handles API responses (successful JSONPs) window[CliopatriaAPI.options.callbackFunctionName] = function(data) { logger.debug("Callback function called with data:", data); // Look for an entry in the queue pool //// If the API server says that the query had %aa instead of expected %AA, //// fix the quoted query in the response to match a query that waits in the query pool var queryInData = data.query.replace(/%[a-z0-9]{2}/g, function(v) {return v.toUpperCase();}); // Delete all stuff that prepends the actual API query (path including the version) queryInData = queryInData.replace(/^.*\/v[\d]\//, ""); var queryPoolEntry = queryPool[queryInData]; if (!queryPoolEntry) { logger.warn(_.str.sprintf("No data API pool entry found for query %s. Response was wasted.", queryInData), data); return; } // Remove the entry from the pool delete queryPool[queryInData]; // Remove the correspondent JSONP DOM node in order to be able to make the same request again $(queryPoolEntry[4]).remove(); // Execute all attached callbacks executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, data.result); }; // Executes all callbacks on request success or error (timeout) var executeAllCallbacksOfTheQueryPoolEntry = function(queryPoolEntry, result) { logger.info(_.str.sprintf("Executing %d callback%s for request %s", queryPoolEntry[3].length, queryPoolEntry[3].length != 1 ? "s" : "", queryPoolEntry[5]), result); for (var i = 0; i < queryPoolEntry[3].length; i++) { var callback = queryPoolEntry[3][i]; if (_.isFunction(callback)) { try { callback.call(null, result, queryPoolEntry[5], queryPoolEntry[1], queryPoolEntry[2]); } catch (e) { logger.error(_.str.sprintf("An error occured when executing callback %s of %s for data API query %s", i + 1, queryPoolEntry[3].length, queryPoolEntry[5]), e); } } } }; // A function that checks expired query pool entries callbackTimeoutExpiryChecker = function() { var currentTimestamp = new Date().getTime(); for(var query in queryPool) { var queryPoolEntry = queryPool[query]; logger.debug(_.str.sprintf("Checking callback timeouts: timestamp diff = %d ms for query %s", currentTimestamp - queryPoolEntry[0], query)); if (currentTimestamp - queryPoolEntry[0] > CliopatriaAPI.options.callbackTimeout) { delete queryPool[query]; $(queryPoolEntry[4]).remove(); executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, { errors: [ { "code": 100, "message": "API request failed" } ] }); } } }; callbackTimeoutExpiryCheckerId = setInterval(callbackTimeoutExpiryChecker, CliopatriaAPI.options.callbackTimeoutCheckingInterval); }); /** * Sends a request to the data API * * @param {string} methodName * API method name (function) * * @param {object} parameters * unsorted list of request parameters ({string} key => {string|number} value}) * * @param {function} callback * a function to be executed on success or failure * * callback function arguments: * {string} result * {string} query (HTTP GET request) * {string} requestedMethodName * {object} requestedParameters * * //@return {void} * // nothing, but this may be replaced with a promise in future * * @return {string} * a uri that has been called */ CliopatriaAPI.request = function(methodName, parameters, callback) { assertModuleIsInitialized(); var query = generateQuery(methodName, parameters); // Choose which of the alternative API paths to use var realAPIRootPath; if (realAPIRootPathsCount == 1) { realAPIRootPath = realAPIRootPaths[0]; } else { var sumOfCharCodes = 0; for (var i = 0; i < query.length; i++) { sumOfCharCodes += query.charCodeAt(i); } realAPIRootPath = realAPIRootPaths[sumOfCharCodes % realAPIRootPathsCount]; } if (!realAPIRootPath) { throw _.str.sprintf("API requsest to Cliopatria cannot be made – api root path is undefined"); } var requestURI = realAPIRootPath + query; var queryPoolEntry = queryPool[query]; if (!queryPoolEntry) { // If a request with the same parameters is not in the current queue, send it // Prepare a real jsonp request logger.info("Sending request", query); var scriptElement = document.createElement('script'); scriptElement.type = 'text/javascript'; scriptElement.async = true; scriptElement.src = requestURI; // Create an entry in the query pool queryPoolEntry = [new Date().getTime(), methodName, parameters, [callback], scriptElement, query]; queryPool[query] = queryPoolEntry; logger.debug("New entry in queryPool: ", query/*, queryPoolEntry*/); // Start the request document.getElementsByTagName('body')[0].appendChild(scriptElement); } else { // If an identical request has been called previously, don't initiate a new query, // simply add a callback to the list of callbacks logger.info("request (imaginary)", query); //queryPoolEntry[0] = new Date().getTime(); queryPoolEntry[3].push(callback); } return requestURI; }; /** * Module finalizer */ CliopatriaAPI.on("stop", function(){ clearInterval(callbackTimeoutExpiryCheckerId); }); }, Logger);