Mercurial > hg > dml-open-vis
diff src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js @ 0:493bcb69166c
added public content
author | Daniel Wolff |
---|---|
date | Tue, 09 Feb 2016 20:54:02 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/DML/MainVisBundle/Resources/assets/marionette/modules/DataModule/DataModule.10-CliopatriaAPI.js Tue Feb 09 20:54:02 2016 +0100 @@ -0,0 +1,342 @@ +"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);