]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
LP#1646166 Hatch extension connect via DOM, remote cache
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / hatch.js
1 /**
2  * Core Service - egHatch
3  *
4  * Dispatches print and data storage requests to the appropriate handler.
5  *
6  * With each top-level request, if a connection to Hatch is established,
7  * the request is relayed.  If a connection has not been attempted, an
8  * attempt is made then the request is handled.  If Hatch is known to be
9  * inaccessible, requests are routed to local handlers.
10  *
11  * Most handlers also provide direct remote and local variants to the
12  * application can decide to which to use as needed.
13  *
14  * Local storage requests are handled by $window.localStorage.
15  *
16  * Note that all top-level and remote requests return promises.  All
17  * local requests return immediate values, since local requests are
18  * never asynchronous.
19  *
20  * BEWARE: never store "fieldmapper" objects, since their structure
21  * may change over time as the IDL changes.  Always flatten objects
22  * into key/value pairs before calling set*Item()
23  *
24  */
25 angular.module('egCoreMod')
26
27 .factory('egHatch',
28            ['$q','$window','$timeout','$interpolate','$http','$cookies',
29     function($q , $window , $timeout , $interpolate , $http , $cookies) {
30
31     var service = {};
32     service.msgId = 1;
33     service.messages = {};
34     service.pending = [];
35     service.hatchAvailable = null;
36
37     // key/value cache -- avoid unnecessary Hatch extension requests.
38     // Only affects *RemoteItem calls.
39     service.keyCache = {}; 
40
41     // write a message to the Hatch port
42     service.sendToHatch = function(msg) {
43         var msg2 = {};
44
45         // shallow copy and scrub msg before sending
46         angular.forEach(msg, function(val, key) {
47             if (key.match(/deferred/)) return;
48             msg2[key] = val;
49         });
50
51         console.debug("sending to Hatch: " + JSON.stringify(msg2));
52
53         msg2.from = 'page';
54         $window.postMessage(msg2, $window.location.origin);
55     }
56
57     // Send the request to Hatch if it's available.  
58     // Otherwise handle the request locally.
59     service.attemptHatchDelivery = function(msg) {
60
61         msg.msgid = service.msgId++;
62         msg.deferred = $q.defer();
63
64         if (service.hatchAvailable === false) { // Hatch is closed.
65             msg.deferred.reject(msg);
66
67         } else if (service.hatchAvailable === true) { // Hatch is open.
68             service.messages[msg.msgid] = msg;
69             service.sendToHatch(msg);
70
71         } else { // Hatch state unknown
72             service.messages[msg.msgid] = msg;
73             service.pending.push(msg);
74             $timeout(service.openHatch);
75         }
76
77         /*
78         console.debug(
79             Object.keys(service.messages).length + " pending Hatch requests");
80         */
81
82         return msg.deferred.promise;
83     }
84
85
86     // resolve the promise on the given request and remove
87     // it from our tracked requests.
88     service.resolveRequest = function(msg) {
89
90         if (!service.messages[msg.msgid]) {
91             console.error('no cached message for id = ' + msg.msgid);
92             return;
93         }
94
95         // for requests sent through Hatch, only the cached 
96         // request will have the original promise attached
97         msg.deferred = service.messages[msg.msgid].deferred;
98         delete service.messages[msg.msgid]; // un-cache
99
100         if (msg.status == 200) {
101             msg.deferred.resolve(msg.content);
102         } else {
103             console.warn("Hatch command failed with status=" 
104                 + msg.status + " and message=" + msg.message);
105             msg.deferred.reject();
106         }
107     }
108
109     service.openHatch = function() {
110
111         // When the Hatch extension loads, it tacks an attribute onto
112         // the page body to indicate it's available.
113
114         if (!$window.document.body.getAttribute('hatch-is-open')) {
115             service.hatchWontOpen('Hatch is not available');
116             return;
117         }
118
119         $window.addEventListener("message", function(event) {
120             // We only accept messages from our own content script.
121             if (event.source != window) return;
122
123             // We only care about messages from the Hatch extension.
124             if (event.data && event.data.from == 'extension') {
125
126                 // Avoid logging full Hatch responses. they can get large.
127                 console.debug(
128                     'Hatch responded to message ID ' + event.data.msgid);
129
130                 service.resolveRequest(event.data);
131             }
132         }); 
133
134         service.hatchAvailable = true; // public flag
135         service.hatchOpened();
136     }
137
138     service.hatchWontOpen = function(err) {
139         console.debug("Hatch connection failed: " + err);
140         service.hatchAvailable = false;
141         while ( (msg = service.pending.shift()) ) {
142             msg.deferred.reject(msg);
143             delete service.messages[msg.msgid];
144         }
145     }
146
147     // Returns true if Hatch is required or if we are currently
148     // communicating with the Hatch service. 
149     service.usingHatch = function() {
150         return service.hatchAvailable || service.hatchRequired();
151     }
152
153     // Returns true if this browser (via localStorage) is 
154     // configured to require Hatch.
155     service.hatchRequired = function() {
156         return service.getLocalItem('eg.hatch.required');
157     }
158
159     service.hatchOpened = function() {
160         // let others know we're connected
161         if (service.onHatchOpen) service.onHatchOpen();
162
163         // Deliver any previously queued requests 
164         while ( (msg = service.pending.shift()) ) {
165             service.sendToHatch(msg);
166         };
167     }
168
169     service.remotePrint = function(
170         context, contentType, content, withDialog) {
171
172         return service.getPrintConfig(context).then(
173             function(config) {
174                 // print configuration retrieved; print
175                 return service.attemptHatchDelivery({
176                     action : 'print',
177                     settings : config,
178                     content : content, 
179                     contentType : contentType,
180                     showDialog : withDialog,
181                 });
182             }
183         );
184     }
185
186     service.getPrintConfig = function(context) {
187         return service.getRemoteItem('eg.print.config.' + context);
188     }
189
190     service.setPrintConfig = function(context, config) {
191         return service.setRemoteItem('eg.print.config.' + context, config);
192     }
193
194     service.getPrinterOptions = function(name) {
195         return service.attemptHatchDelivery({
196             action : 'printer-options',
197             printer : name
198         });
199     }
200
201     service.getPrinters = function() {
202         if (service.printers) // cached printers
203             return $q.when(service.printers);
204
205         return service.attemptHatchDelivery({action : 'printers'}).then(
206
207             // we have remote printers; sort by name and return
208             function(printers) {
209                 service.printers = printers.sort(
210                     function(a,b) {return a.name < b.name ? -1 : 1});
211                 return service.printers;
212             },
213
214             // remote call failed and there is no such thing as local
215             // printers; return empty set.
216             function() { return [] } 
217         );
218     }
219
220     // get the value for a stored item
221     service.getItem = function(key) {
222         return service.getRemoteItem(key)['catch'](
223             function(msg) {
224                 if (service.hatchRequired()) {
225                     console.error("getRemoteItem() failed for key " + key);
226                     return null;
227                 }
228                 return service.getLocalItem(msg.key);
229             }
230         );
231     }
232
233     service.getRemoteItem = function(key) {
234         
235         if (service.keyCache[key] != undefined)
236             return $q.when(service.keyCache[key])
237
238         return service.attemptHatchDelivery({
239             key : key,
240             action : 'get'
241         }).then(function(content) {
242             return service.keyCache[key] = content;
243         });
244     }
245
246     service.getLocalItem = function(key) {
247         var val = $window.localStorage.getItem(key);
248         if (val == null) return;
249         return JSON.parse(val);
250     }
251
252     service.getLoginSessionItem = function(key) {
253         var val = $cookies.get(key);
254         if (val == null) return;
255         return JSON.parse(val);
256     }
257
258     service.getSessionItem = function(key) {
259         var val = $window.sessionStorage.getItem(key);
260         if (val == null) return;
261         return JSON.parse(val);
262     }
263
264     /**
265      * @param tmp bool Store the value as a session cookie only.  
266      * tmp values are removed during logout or browser close.
267      */
268     service.setItem = function(key, value) {
269         return service.setRemoteItem(key, value)['catch'](
270             function(msg) {
271                 if (service.hatchRequired()) {
272                     console.error("setRemoteItem() failed for key " + key);
273                      return null;
274                 }
275                 return service.setLocalItem(msg.key, value);
276             }
277         );
278     }
279
280     // set the value for a stored or new item
281     service.setRemoteItem = function(key, value) {
282         service.keyCache[key] = value;
283         return service.attemptHatchDelivery({
284             key : key, 
285             content : value, 
286             action : 'set',
287         });
288     }
289
290     // Set the value for the given key.
291     // "Local" items persist indefinitely.
292     // If the value is raw, pass it as 'value'.  If it was
293     // externally JSONified, pass it via jsonified.
294     service.setLocalItem = function(key, value, jsonified) {
295         if (jsonified === undefined ) 
296             jsonified = JSON.stringify(value);
297         $window.localStorage.setItem(key, jsonified);
298     }
299
300     // Set the value for the given key.  
301     // "LoginSession" items are removed when the user logs out or the 
302     // browser is closed.
303     // If the value is raw, pass it as 'value'.  If it was
304     // externally JSONified, pass it via jsonified.
305     service.setLoginSessionItem = function(key, value, jsonified) {
306         service.addLoginSessionKey(key);
307         if (jsonified === undefined ) 
308             jsonified = JSON.stringify(value);
309         $cookies.put(key, jsonified);
310     }
311
312     // Set the value for the given key.  
313     // "Session" items are browser tab-specific and are removed when the
314     // tab is closed.
315     // If the value is raw, pass it as 'value'.  If it was
316     // externally JSONified, pass it via jsonified.
317     service.setSessionItem = function(key, value, jsonified) {
318         if (jsonified === undefined ) 
319             jsonified = JSON.stringify(value);
320         $window.sessionStorage.setItem(key, jsonified);
321     }
322
323     // assumes the appender and appendee are both strings
324     // TODO: support arrays as well
325     service.appendLocalItem = function(key, value) {
326         var item = service.getLocalItem(key);
327         if (item) {
328             if (typeof item != 'string') {
329                 logger.warn("egHatch.appendLocalItem => "
330                     + "cannot append to a non-string item: " + key);
331                 return;
332             }
333             value = item + value; // concatenate our value
334         }
335         service.setLocalitem(key, value);
336     }
337
338     // remove a stored item
339     service.removeItem = function(key) {
340         return service.removeRemoteItem(key)['catch'](
341             function(msg) { 
342                 return service.removeLocalItem(msg.key) 
343             }
344         );
345     }
346
347     service.removeRemoteItem = function(key) {
348         delete service.keyCache[key];
349         return service.attemptHatchDelivery({
350             key : key,
351             action : 'remove'
352         });
353     }
354
355     service.removeLocalItem = function(key) {
356         $window.localStorage.removeItem(key);
357     }
358
359     service.removeLoginSessionItem = function(key) {
360         service.removeLoginSessionKey(key);
361         $cookies.remove(key);
362     }
363
364     service.removeSessionItem = function(key) {
365         $window.sessionStorage.removeItem(key);
366     }
367
368     /**
369      * Remove all "LoginSession" items.
370      */
371     service.clearLoginSessionItems = function() {
372         angular.forEach(service.getLoginSessionKeys(), function(key) {
373             service.removeLoginSessionItem(key);
374         });
375
376         // remove the keys cache.
377         service.removeLocalItem('eg.hatch.login_keys');
378     }
379
380     // if set, prefix limits the return set to keys starting with 'prefix'
381     service.getKeys = function(prefix) {
382         return service.getRemoteKeys(prefix)['catch'](
383             function() { 
384                 if (service.hatchRequired()) {
385                     console.error("getRemoteKeys() failed");
386                     return [];
387                 }
388                 return service.getLocalKeys(prefix) 
389             }
390         );
391     }
392
393     service.getRemoteKeys = function(prefix) {
394         return service.attemptHatchDelivery({
395             key : prefix,
396             action : 'keys'
397         });
398     }
399
400     service.getLocalKeys = function(prefix) {
401         var keys = [];
402         var idx = 0;
403         while ( (k = $window.localStorage.key(idx++)) !== null) {
404             // key prefix match test
405             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
406             keys.push(k);
407         }
408         return keys;
409     }
410
411
412     /**
413      * Array of "LoginSession" keys.
414      * Note we have to store these as "Local" items so browser tabs can
415      * share them.  We could store them as cookies, but it's more data
416      * that has to go back/forth to the server.  A "LoginSession" key name is
417      * not private, though, so it's OK if they are left in localStorage
418      * until the next login.
419      */
420     service.getLoginSessionKeys = function(prefix) {
421         var keys = [];
422         var idx = 0;
423         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
424         angular.forEach(login_keys, function(k) {
425             // key prefix match test
426             if (prefix && k.substr(0, prefix.length) != prefix) return;
427             keys.push(k);
428         });
429         return keys;
430     }
431
432     service.addLoginSessionKey = function(key) {
433         var keys = service.getLoginSessionKeys();
434         if (keys.indexOf(key) < 0) {
435             keys.push(key);
436             service.setLocalItem('eg.hatch.login_keys', keys);
437         }
438     }
439
440     service.removeLoginSessionKey = function(key) {
441         var keys = service.getLoginSessionKeys().filter(function(k) {
442             return k != key;
443         });
444         service.setLocalItem('eg.hatch.login_keys', keys);
445     }
446
447     return service;
448 }])
449