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