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