LP#1789747 SharedWorker sanity checks
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / lovefield.js
1 /**
2  * Core Service - egLovefield
3  *
4  * Lovefield wrapper factory for low level offline stuff
5  *
6  */
7 angular.module('egCoreMod')
8
9 .factory('egLovefield', ['$q','$rootScope','egCore','$timeout', 
10                  function($q , $rootScope , egCore , $timeout) { 
11
12     var service = {
13         autoId: 0, // each request gets a unique id.
14         cannotConnect: false,
15         pendingRequests: [],
16         activeSchemas: ['cache'], // add 'offline' in the offline UI
17         schemasInProgress: {},
18         connectedSchemas: [],
19         // TODO: relative path would be more portable
20         workerUrl: '/js/ui/default/staff/offline-db-worker.js'
21     };
22
23     service.connectToWorker = function() {
24         if (service.worker) return;
25
26         try {
27             // relative path would be better...
28             service.worker = new SharedWorker(service.workerUrl);
29         } catch (E) {
30             console.warn('SharedWorker() not supported', E);
31             service.cannotConnect = true;
32             return;
33         }
34
35         service.worker.onerror = function(err) {
36             // avoid spamming unit test runner on failure to connect.
37             if (!navigator.userAgent.match(/PhantomJS/)) {
38                 console.error('Error loading shared worker', err);
39             }
40             service.cannotConnect = true;
41         }
42
43         // List for responses and resolve the matching pending request.
44         service.worker.port.addEventListener('message', function(evt) {
45             var response = evt.data;
46             var reqId = response.id;
47             var req = service.pendingRequests.filter(
48                 function(r) { return r.id === reqId})[0];
49
50             if (!req) {
51                 console.error('Recieved response for unknown request ' + reqId);
52                 return;
53             }
54
55             if (response.status === 'OK') {
56                 req.deferred.resolve(response.result);
57             } else {
58                 console.error('worker request failed with ' + response.error);
59                 req.deferred.reject(response.error);
60             }
61         });
62
63         service.worker.port.start();
64     }
65
66     service.connectToSchemas = function() {
67
68         service.connectToWorker(); // no-op if already connected
69
70         if (service.cannotConnect) { 
71             // This can happen in certain environments
72             return $q.reject();
73         }
74
75         var promises = [];
76
77         service.activeSchemas.forEach(function(schema) {
78             promises.push(service.connectToSchema(schema));
79         });
80
81         return $q.all(promises).then(
82             function() {},
83             function() {service.cannotConnect = true}
84         );
85     }
86
87     // Connects if necessary to the active schemas then relays the request.
88     service.request = function(args) {
89         // useful, but very chatty, leaving commented out.
90         // console.debug('egLovfield sending request: ', args);
91         return service.connectToSchemas().then(
92             function() {
93                 return service.relayRequest(args);
94             }
95         );
96     }
97
98     // Send a request to the web worker and register the request for
99     // future resolution.
100     // Store the request ID in the request arguments, so it's included
101     // in the response, and in the pendingRequests list for linking.
102     service.relayRequest = function(args) {
103         var deferred = $q.defer();
104         var reqId = service.autoId++;
105         args.id = reqId;
106         service.pendingRequests.push({id : reqId, deferred: deferred});
107         service.worker.port.postMessage(args);
108         return deferred.promise;
109     }
110
111     // Create and connect to the give schema
112     service.connectToSchema = function(schema) {
113
114         if (service.connectedSchemas.indexOf(schema) >= 0) {
115             // already connected
116             return $q.when();
117         }
118
119         if (service.schemasInProgress[schema]) {
120             return service.schemasInProgress[schema];
121         }
122
123         var deferred = $q.defer();
124
125         service.relayRequest(
126             {schema: schema, action: 'createSchema'}) 
127         .then(
128             function() {
129                 return service.relayRequest(
130                     {schema: schema, action: 'connect'});
131             },
132             deferred.reject
133         ).then(
134             function() { 
135                 service.connectedSchemas.push(schema); 
136                 delete service.schemasInProgress[schema];
137                 deferred.resolve();
138             },
139             deferred.reject
140         );
141
142         return service.schemasInProgress[schema] = deferred.promise;
143     }
144
145     service.isCacheGood = function (type) {
146         return service.request({
147             schema: 'cache',
148             table: 'CacheDate',
149             action: 'selectWhereEqual',
150             field: 'type',
151             value: type
152         }).then(
153             function(result) {
154                 var row = result[0];
155                 if (!row) { return false; }
156                 // hard-coded 1 day offline cache timeout
157                 return (new Date().getTime() - row.cachedate.getTime()) <= 86400000;
158             }
159         );
160     }
161
162     // Remove all pending offline transactions and delete the cached
163     // offline transactions date to indicate no transactions remain.
164     service.destroyPendingOfflineXacts = function () {
165         return service.request({
166             schema: 'offline',
167             table: 'OfflineXact',
168             action: 'deleteAll'
169         }).then(function() {
170             return service.request({
171                 schema: 'cache',
172                 table: 'CacheDate',
173                 action: 'deleteWhereEqual',
174                 field: 'type',
175                 value: '_offlineXact'
176             });
177         });
178     }
179
180     // Returns the cache date when xacts exit, null otherwise.
181     service.havePendingOfflineXacts = function () {
182         return service.request({
183             schema: 'cache',
184             table: 'CacheDate',
185             action: 'selectWhereEqual',
186             field: 'type',
187             value: '_offlineXact'
188         }).then(function(results) {
189             return results[0] ? results[0].cachedate : null;
190         });
191     }
192
193     service.retrievePendingOfflineXacts = function () {
194         return service.request({
195             schema: 'offline',
196             table: 'OfflineXact',
197             action: 'selectAll'
198         }).then(function(resp) {
199             return resp.map(function(x) { return x.value });
200         });
201     }
202
203     // Add an offline transaction and update the cache indicating
204     // now() as the most recent addition of an offline xact.
205     service.addOfflineXact = function (obj) {
206         return service.request({
207             schema: 'offline',
208             table: 'OfflineXact',
209             action: 'insertOrReplace',
210             rows: [{value: obj}]
211         }).then(function() {
212             return service.request({
213                 schema: 'cache',
214                 table: 'CacheDate',
215                 action: 'insertOrReplace',
216                 rows: [{type: '_offlineXact', cachedate : new Date()}]
217             });
218         });
219     }
220
221     service.populateBlockList = function() {
222         return service.request({
223             action: 'populateBlockList',
224             authtoken: egCore.auth.token()
225         });
226     }
227
228     // Returns a promise with true for blocked, false for not blocked
229     service.testOfflineBlock = function (barcode) {
230         return service.request({
231             schema: 'offline',
232             table: 'OfflineBlocks',
233             action: 'selectWhereEqual',
234             field: 'barcode',
235             value: barcode
236         }).then(function(resp) {
237             if (resp.length === 0) return null;
238             return resp[0].reason;
239         });
240     }
241
242     service.setStatCatsCache = function (statcats) {
243         if (lf.isOffline || !statcats || statcats.length === 0) 
244             return $q.when();
245
246         var rows = statcats.map(function(cat) {
247             return {id: cat.id(), value: egCore.idl.toHash(cat)}
248         });
249
250         return service.request({
251             schema: 'cache',
252             table: 'StatCat',
253             action: 'insertOrReplace',
254             rows: rows
255         });
256     }
257
258     service.getStatCatsCache = function () {
259
260         return service.request({
261             schema: 'cache',
262             table: 'StatCat',
263             action: 'selectAll'
264         }).then(function(list) {
265             var result = [];
266             list.forEach(function(s) {
267                 var sc = egCore.idl.fromHash('actsc', s.value);
268
269                 if (Array.isArray(sc.default_entries())) {
270                     sc.default_entries(
271                         sc.default_entries().map( function (k) {
272                             return egCore.idl.fromHash('actsced', k);
273                         })
274                     );
275                 }
276
277                 if (Array.isArray(sc.entries())) {
278                     sc.entries(
279                         sc.entries().map( function (k) {
280                             return egCore.idl.fromHash('actsce', k);
281                         })
282                     );
283                 }
284
285                 result.push(sc);
286             });
287
288             return result;
289         });
290     }
291
292     service.setSettingsCache = function (settings) {
293         if (lf.isOffline) return $q.when();
294
295         var rows = [];
296         angular.forEach(settings, function (val, key) {
297             rows.push({name  : key, value : JSON.stringify(val)});
298         });
299
300         return service.request({
301             schema: 'cache',
302             table: 'Setting',
303             action: 'insertOrReplace',
304             rows: rows
305         });
306     }
307
308     service.getSettingsCache = function (settings) {
309
310         var promise;
311
312         if (settings && settings.length) {
313             promise = service.request({
314                 schema: 'cache',
315                 table: 'Setting',
316                 action: 'selectWhereIn',
317                 field: 'name',
318                 value: settings
319             });
320         } else {
321             promise = service.request({
322                 schema: 'cache',
323                 table: 'Setting',
324                 action: 'selectAll'
325             });
326         }
327
328         return promise.then(
329             function(resp) {
330                 resp.forEach(function(s) { s.value = JSON.parse(s.value); });
331                 return resp;
332             }
333         );
334     }
335
336     service.setListInOfflineCache = function (type, list) {
337         if (lf.isOffline) return $q.when();
338
339         return service.isCacheGood(type).then(function(good) {
340             if (good) { return };  // already cached
341
342             var pkey = egCore.idl.classes[type].pkey;
343             var rows = Object.values(list).map(function(item) {
344                 return {
345                     type: type, 
346                     id: '' + item[pkey](), 
347                     object: egCore.idl.toHash(item)
348                 };
349             });
350
351             return service.request({
352                 schema: 'cache',
353                 table: 'Object',
354                 action: 'insertOrReplace',
355                 rows: rows
356             }).then(function(resp) {
357                 return service.request({
358                     schema: 'cache',
359                     table: 'CacheDate',
360                     action: 'insertOrReplace',
361                     rows: [{type: type, cachedate : new Date()}]
362                 });
363             });
364         });
365     }
366
367     service.getListFromOfflineCache = function(type) {
368         return service.request({
369             schema: 'cache',
370             table: 'Object',
371             action: 'selectWhereEqual',
372             field: 'type',
373             value: type
374         }).then(function(resp) {
375             return resp.map(function(item) {
376                 return egCore.idl.fromHash(type,item['object']);
377             });
378         });
379     }
380
381     service.reconstituteList = function(type) {
382         if (lf.isOffline) {
383             console.debug('egLovefield reading ' + type + ' list');
384             return service.getListFromOfflineCache(type).then(function (list) {
385                 egCore.env.absorbList(list, type, true)
386                 return $q.when(true);
387             });
388         }
389         return $q.when(false);
390     }
391
392     service.reconstituteTree = function(type) {
393         if (lf.isOffline) {
394             console.debug('egLovefield reading ' + type + ' tree');
395
396             var pkey = egCore.idl.classes[type].pkey;
397             var parent_field = 'parent';
398
399             if (type == 'aou') {
400                 parent_field = 'parent_ou';
401             }
402
403             return service.getListFromOfflineCache(type).then(function (list) {
404                 var hash = {};
405                 var top = null;
406                 angular.forEach(list, function (item) {
407
408                     // Special case for aou, to reconstitue ou_type
409                     if (type == 'aou') {
410                         if (item.ou_type()) {
411                             item.ou_type( egCore.idl.fromHash('aout', item.ou_type()) );
412                         }
413                     }
414
415                     hash[''+item[pkey]()] = item;
416                     if (!item[parent_field]()) {
417                         top = item;
418                     } else if (angular.isObject(item[parent_field]())) {
419                         // un-objectify the parent
420                         item[parent_field](
421                             item[parent_field]()[pkey]()
422                         );
423                     }
424                 });
425
426                 angular.forEach(list, function (item) {
427                     item.children([]); // just clear it out if there's junk in there
428
429                     item.children( list.filter(function (kid) {
430                         return kid[parent_field]() == item[pkey]();
431                     }) );
432                 });
433
434                 angular.forEach(list, function (item) {
435                     if (item[parent_field]()) {
436                         item[parent_field]( hash[''+item[parent_field]()] );
437                     }
438                 });
439
440                 egCore.env.absorbTree(top, type, true)
441                 return $q.when(true)
442             });
443         }
444         return $q.when(false);
445     }
446
447     return service;
448 }]);
449