comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:493bcb69166c
1 "use strict";
2 /**
3 * The module wraps all API calls to the data server and abstracts JSONP calls.
4 *
5 * API spec: https://code.soundsoftware.ac.uk/projects/dml/wiki/Vis_API_-_front_end_~_data_server
6 *
7 *
8 * Module options:
9 *
10 * apiRootPaths
11 * A pool of URLs from where the data are obtained. Multiple entry points (domains) can be used.
12 * The choice of a particular root path is "random", i.e. this is a "hash" function of a query contents:
13 * "doThis?x=1&y=1" -> path 1 of 3
14 * "doThat?x=1&y=1" -> path 3
15 * "doThat?x=1&y=123" -> path 1
16 * "doThat?x=1&y=2" -> path 2
17 * ...
18 * "doThis?x=1&y=1" -> path 1
19 *
20 * The same query is always requested from the same instance (domain) to facilitate HTTP caching.
21 * // instead of http:// and https:// can be used for protocol auto-selection.
22 * Trailing slash is compulsory.
23 *
24 * Examples:
25 * ["//127.0.0.1/"],
26 * ["//127.0.0.1/my-path/to/dml-api/"],
27 * ["//a.example.com/dml-api/", "//b.example.com/dml-api/", "//c.example.com/dml-api/"],
28 *
29 * apiVersion
30 * A natural number representing the version of the data API (i.e. the available features of the interface)
31 *
32 * Examples:
33 * 1
34 * 42
35 *
36 * dataVersion
37 * A natural number or a string representing the version of the selected music library.
38 * As API calls are supplemented with dv=[dataVersion], a change in the value of dataVersion drops the HTTP cache.
39 * dataVersion is delivered to the front end with the contents of the web page.
40 *
41 * Examples:
42 * 42
43 * "2015-01-01"
44 *
45 */
46 App.module("DataModule.CliopatriaAPI", function(CliopatriaAPI, App, Backbone, Marionette, $, _, Logger) {
47 // Prevent auto start
48 CliopatriaAPI.startWithParent = false;
49
50 // Define module options
51 var defaultModuleOptions = {
52 // if a request is not returned in this number of ms, handle it as failed
53 callbackTimeout: 30000,
54
55 // expired requests are being checked this often (ms)
56 callbackTimeoutCheckingInterval: 1000,
57
58 // callback of JSONP responses
59 callbackFunctionName: "jsonp_cb",
60
61 // optional data version suffix that is appended to all API requests, e.g. "123" → &dv=123
62 dataVersion: null,
63
64 apiRootPaths: [],
65
66 apiVersion: 1,
67
68 dataCaching: true
69 };
70
71 // =========================================================================
72 // private variables
73 var logger = null;
74
75 /**
76 * derived from CliopatriaAPI.options.dataVersion
77 */
78 var dataVersionSuffix = null;
79
80 /**
81 * Real API root paths include API version (e.g. http://example.com/path/to/api/v42/)
82 */
83 var realAPIRootPaths = null;
84
85 /**
86 * The number of the real API paths
87 */
88 var realAPIRootPathsCount = null;
89
90
91 /**
92 * query pool: an object where keys correspond to sent queries (methodName?param1=1&...&dv=42) and values are:
93 * [timeSent, methodName, parameters, callbacks, scriptElement, query]
94 */
95 var queryPool = null;
96
97 /**
98 * Generates query from API method name and parameters (+ appends data version)
99 * Parameters are sorted alphabetically, except dv, which is always the last
100 */
101 var generateQuery = null;
102
103 /**
104 * a function that is called every callbackTimeoutCheckingInterval ms and checks for expired query pool entries
105 */
106 var callbackTimeoutExpiryChecker = null;
107
108 /**
109 * timeout (interval) handler for callbackTimeoutExpiryChecker
110 */
111 var callbackTimeoutExpiryCheckerId = null;
112
113 /**
114 * Initialization checker
115 */
116 var assertModuleIsInitialized = function() {
117 if (!logger) {
118 throw "DataModule.CliopatriaAPI has not been initialized";
119 }
120 };
121
122 /**
123 * Module initializer
124 *
125 */
126 CliopatriaAPI.addInitializer(function(options) {
127
128 CliopatriaAPI.options = _.extend(defaultModuleOptions, options);
129
130 logger = Logger.get("DataModule.CliopatriaAPI");
131 //logger.setLevel(Logger.DEBUG);
132 //logger.setLevel(Logger.INFO);
133
134 // Set data version suffix (common for all requests)
135 if (CliopatriaAPI.options.dataVersion) {
136 //dataVersionSuffix = _.str.sprintf("&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
137 dataVersionSuffix = _.str.sprintf("&format=jsonp&dv=%s", encodeURIComponent(CliopatriaAPI.options.dataVersion));
138 } else {
139 //dataVersionSuffix = "";
140 dataVersionSuffix = "&format=jsonp";
141 }
142
143 realAPIRootPaths = [];
144 for (var i = 0; i < CliopatriaAPI.options.apiRootPaths.length; ++i) {
145 var apiRootPath = CliopatriaAPI.options.apiRootPaths[i];
146
147 realAPIRootPaths.push(_.str.sprintf("%sv%s/", apiRootPath, CliopatriaAPI.options.apiVersion));
148 }
149 realAPIRootPathsCount = realAPIRootPaths.length;
150
151 if (!realAPIRootPathsCount) {
152 logger.error("DataModule.CliopatriaAPI has no registered API paths");
153 }
154
155 queryPool = {};
156
157 // A function that generates a query by the method name and the parameters
158 generateQuery = function(methodName, parameters) {
159 // Sort parameters
160 var sortedParameterPairs = [];
161 for (var parameterName in parameters) {
162 sortedParameterPairs.push([parameterName, parameters[parameterName]]);
163 }
164 sortedParameterPairs.sort(function(a, b) {return _.str.naturalCmp(a[0], b[0]);});
165
166 // encode them
167 var encodedParametersAsArrayOfStrings = [];
168 for (var parameterPairIndex in sortedParameterPairs) {
169 encodedParametersAsArrayOfStrings.push(encodeURIComponent(sortedParameterPairs[parameterPairIndex][0]) + "=" + encodeURIComponent(sortedParameterPairs[parameterPairIndex][1]));
170 };
171 if (!CliopatriaAPI.options.dataCaching) {
172 encodedParametersAsArrayOfStrings.push("random="+Math.round(Math.random() * 100000));
173 }
174 var encodedParameters = encodedParametersAsArrayOfStrings.join("&");
175
176 // append data version (dv)
177 if (methodName == "getCollectionId") {
178 if (encodedParameters.length) {
179 encodedParameters += dataVersionSuffix;
180 } else {
181 encodedParameters += dataVersionSuffix.slice(1);
182 }
183 }
184
185 return methodName + "?" + encodedParameters;
186 };
187
188 // A function that handles API responses (successful JSONPs)
189 window[CliopatriaAPI.options.callbackFunctionName] = function(data) {
190
191 logger.debug("Callback function called with data:", data);
192
193 // Look for an entry in the queue pool
194 //// If the API server says that the query had %aa instead of expected %AA,
195 //// fix the quoted query in the response to match a query that waits in the query pool
196 var queryInData = data.query.replace(/%[a-z0-9]{2}/g, function(v) {return v.toUpperCase();});
197 // Delete all stuff that prepends the actual API query (path including the version)
198 queryInData = queryInData.replace(/^.*\/v[\d]\//, "");
199 var queryPoolEntry = queryPool[queryInData];
200
201 if (!queryPoolEntry) {
202 logger.warn(_.str.sprintf("No data API pool entry found for query %s. Response was wasted.", queryInData), data);
203 return;
204 }
205
206 // Remove the entry from the pool
207 delete queryPool[queryInData];
208
209 // Remove the correspondent JSONP DOM node in order to be able to make the same request again
210 $(queryPoolEntry[4]).remove();
211 // Execute all attached callbacks
212 executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, data.result);
213 };
214
215 // Executes all callbacks on request success or error (timeout)
216 var executeAllCallbacksOfTheQueryPoolEntry = function(queryPoolEntry, result) {
217 logger.info(_.str.sprintf("Executing %d callback%s for request %s", queryPoolEntry[3].length, queryPoolEntry[3].length != 1 ? "s" : "", queryPoolEntry[5]), result);
218 for (var i = 0; i < queryPoolEntry[3].length; i++) {
219 var callback = queryPoolEntry[3][i];
220 if (_.isFunction(callback)) {
221 try {
222 callback.call(null, result, queryPoolEntry[5], queryPoolEntry[1], queryPoolEntry[2]);
223 } catch (e) {
224 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);
225 }
226 }
227 }
228 };
229
230 // A function that checks expired query pool entries
231 callbackTimeoutExpiryChecker = function() {
232
233 var currentTimestamp = new Date().getTime();
234
235 for(var query in queryPool) {
236 var queryPoolEntry = queryPool[query];
237 logger.debug(_.str.sprintf("Checking callback timeouts: timestamp diff = %d ms for query %s", currentTimestamp - queryPoolEntry[0], query));
238 if (currentTimestamp - queryPoolEntry[0] > CliopatriaAPI.options.callbackTimeout) {
239 delete queryPool[query];
240 $(queryPoolEntry[4]).remove();
241 executeAllCallbacksOfTheQueryPoolEntry(queryPoolEntry, {
242 errors: [
243 {
244 "code": 100,
245 "message": "API request failed"
246 }
247 ]
248 });
249 }
250 }
251 };
252
253 callbackTimeoutExpiryCheckerId = setInterval(callbackTimeoutExpiryChecker, CliopatriaAPI.options.callbackTimeoutCheckingInterval);
254 });
255
256
257 /**
258 * Sends a request to the data API
259 *
260 * @param {string} methodName
261 * API method name (function)
262 *
263 * @param {object} parameters
264 * unsorted list of request parameters ({string} key => {string|number} value})
265 *
266 * @param {function} callback
267 * a function to be executed on success or failure
268 *
269 * callback function arguments:
270 * {string} result
271 * {string} query (HTTP GET request)
272 * {string} requestedMethodName
273 * {object} requestedParameters
274 *
275 * //@return {void}
276 * // nothing, but this may be replaced with a promise in future
277 *
278 * @return {string}
279 * a uri that has been called
280 */
281 CliopatriaAPI.request = function(methodName, parameters, callback) {
282 assertModuleIsInitialized();
283
284 var query = generateQuery(methodName, parameters);
285
286 // Choose which of the alternative API paths to use
287 var realAPIRootPath;
288 if (realAPIRootPathsCount == 1) {
289 realAPIRootPath = realAPIRootPaths[0];
290 } else {
291 var sumOfCharCodes = 0;
292 for (var i = 0; i < query.length; i++) {
293 sumOfCharCodes += query.charCodeAt(i);
294 }
295 realAPIRootPath = realAPIRootPaths[sumOfCharCodes % realAPIRootPathsCount];
296 }
297 if (!realAPIRootPath) {
298 throw _.str.sprintf("API requsest to Cliopatria cannot be made – api root path is undefined");
299 }
300
301 var requestURI = realAPIRootPath + query;
302
303 var queryPoolEntry = queryPool[query];
304
305 if (!queryPoolEntry) {
306 // If a request with the same parameters is not in the current queue, send it
307
308 // Prepare a real jsonp request
309 logger.info("Sending request", query);
310 var scriptElement = document.createElement('script');
311 scriptElement.type = 'text/javascript';
312 scriptElement.async = true;
313 scriptElement.src = requestURI;
314
315 // Create an entry in the query pool
316 queryPoolEntry = [new Date().getTime(), methodName, parameters, [callback], scriptElement, query];
317 queryPool[query] = queryPoolEntry;
318 logger.debug("New entry in queryPool: ", query/*, queryPoolEntry*/);
319
320 // Start the request
321 document.getElementsByTagName('body')[0].appendChild(scriptElement);
322
323 } else {
324 // If an identical request has been called previously, don't initiate a new query,
325 // simply add a callback to the list of callbacks
326 logger.info("request (imaginary)", query);
327
328
329 //queryPoolEntry[0] = new Date().getTime();
330 queryPoolEntry[3].push(callback);
331 }
332 return requestURI;
333 };
334
335 /**
336 * Module finalizer
337 */
338 CliopatriaAPI.on("stop", function(){
339 clearInterval(callbackTimeoutExpiryCheckerId);
340 });
341
342 }, Logger);