Mercurial > hg > dml-open-vis
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); |