]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
LP#1350042 Browser client templates/scripts (phase 1)
[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',
29     function($q , $window , $timeout , $interpolate , $http) {
30
31     var service = {};
32     service.msgId = 0;
33     service.messages = {};
34     service.pending = [];
35     service.socket = null;
36     service.hatchAvailable = null;
37     service.defaultHatchURL = 'wss://localhost:8443/hatch'; 
38
39     // write a message to the Hatch websocket
40     service.sendToHatch = function(msg) {
41         var msg2 = {};
42
43         // shallow copy and scrub msg before sending
44         angular.forEach(msg, function(val, key) {
45             if (key.match(/deferred/)) return;
46             msg2[key] = val;
47         });
48
49         console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
50         service.socket.send(JSON.stringify(msg2));
51     }
52
53     // Send the request to Hatch if it's available.  
54     // Otherwise handle the request locally.
55     service.attemptHatchDelivery = function(msg) {
56
57         msg.msgid = service.msgId++;
58         msg.deferred = $q.defer();
59
60         if (service.hatchAvailable === false) { // Hatch is closed
61             msg.deferred.reject(msg);
62
63         } else if (service.hatchAvailable === true) { // Hatch is open
64             // Hatch is known to be open
65             service.messages[msg.msgid] = msg;
66             service.sendToHatch(msg);
67
68         } else {  // Hatch status unknown; attempt to connect
69             service.messages[msg.msgid] = msg;
70             service.pending.push(msg);
71             service.hatchConnect();
72         }
73
74         return msg.deferred.promise;
75     }
76
77
78     // resolve the promise on the given request and remove
79     // it from our tracked requests.
80     service.resolveRequest = function(msg) {
81
82         if (!service.messages[msg.msgid]) {
83             console.warn('no cached message for ' 
84                 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
85             return;
86         }
87
88         // for requests sent through Hatch, only the cached 
89         // request will have the original promise attached
90         msg.deferred = service.messages[msg.msgid].deferred;
91         delete service.messages[msg.msgid]; // un-cache
92
93         // resolve / reject
94         if (msg.error) {
95             throw new Error(
96             "egHatch command failed : " 
97                 + JSON.stringify(msg.error, null, 2));
98         } else {
99             msg.deferred.resolve(msg.content);
100         } 
101     }
102
103     service.hatchClosed = function() {
104         service.socket = null;
105         service.printers = [];
106         service.printConfig = {};
107         while ( (msg = service.pending.shift()) ) {
108             msg.deferred.reject(msg);
109             delete service.messages[msg.msgid];
110         }
111         if (service.onHatchClose)
112             service.onHatchClose();
113     }
114
115     service.hatchURL = function() {
116         return service.getLocalItem('eg.hatch.url') 
117             || service.defaultHatchURL;
118     }
119
120     // Returns true if Hatch is required or if we are currently
121     // communicating with the Hatch service. 
122     service.usingHatch = function() {
123         return service.hatchAvailable || service.hatchRequired();
124     }
125
126     // Returns true if this browser (via localStorage) is 
127     // configured to require Hatch.
128     service.hatchRequired = function() {
129         return service.getLocalItem('eg.hatch.required');
130     }
131
132     service.hatchConnect = function() {
133
134         if (service.socket && 
135             service.socket.readyState == service.socket.CONNECTING) {
136             // connection in progress.  Nothing to do.  Our queued
137             // message will be delivered when onopen() fires
138             return;
139         }
140
141         try {
142             service.socket = new WebSocket(service.hatchURL());
143         } catch(e) {
144             service.hatchAvailable = false;
145             service.hatchClosed();
146             return;
147         }
148
149         service.socket.onopen = function() {
150             console.debug('connected to Hatch');
151             service.hatchAvailable = true;
152             if (service.onHatchOpen) 
153                 service.onHatchOpen();
154             while ( (msg = service.pending.shift()) ) {
155                 service.sendToHatch(msg);
156             };
157         }
158
159         service.socket.onclose = function() {
160             if (service.hatchAvailable === false) return; // already registered
161
162             // onclose() will be called regularly as we disconnect from
163             // Hatch via timeouts.  Return hatchAvailable to its unknow state
164             service.hatchAvailable = null;
165             service.hatchClosed();
166         }
167
168         service.socket.onerror = function() {
169             if (service.hatchAvailable === false) return; // already registered
170             service.hatchAvailable = false;
171             console.debug(
172                 "unable to connect to Hatch server at " + service.hatchURL());
173             service.hatchClosed();
174         }
175
176         service.socket.onmessage = function(evt) {
177             var msgStr = evt.data;
178             if (!msgStr) throw new Error("Hatch returned empty message");
179
180             var msgObj = JSON.parse(msgStr);
181             console.debug('Hatch says ' + JSON.stringify(msgObj, null, 2));
182             service.resolveRequest(msgObj); 
183         }
184     }
185
186     service.getPrintConfig = function() {
187         if (service.printConfig) 
188             return $q.when(service.printConfig);
189
190         return service.getRemoteItem('eg.print.config')
191         .then(function(conf) { 
192             return (service.printConfig = conf || {}) 
193         });
194     }
195
196     service.setPrintConfig = function(conf) {
197         service.printConfig = conf;
198         return service.setRemoteItem('eg.print.config', conf);
199     }
200
201
202     service.remotePrint = function(
203         context, contentType, content, withDialog) {
204
205         return service.getPrintConfig().then(
206             function(conf) {
207                 // print configuration retrieved; print
208                 return service.attemptHatchDelivery({
209                     action : 'print',
210                     config : conf[context],
211                     content : content, 
212                     contentType : contentType,
213                     showDialog : withDialog,
214                 });
215             }
216         );
217     }
218
219     // launch the print dialog then attach the resulting configuration
220     // to the requested context, then store the final values.
221     service.configurePrinter = function(context, printer) {
222
223         // load current settings
224         return service.getPrintConfig()
225
226         // dispatch the print configuration request
227         .then(function(config) {
228
229             // loaded remote config
230             if (!config[context]) config[context] = {};
231             config[context].printer = printer;
232             return service.attemptHatchDelivery({
233                 key : 'no-op', 
234                 action : 'print-config',
235                 config : config[context]
236             })
237         })
238
239         // set the returned settings to the requested context
240         .then(function(newconf) {
241             if (angular.isObject(newconf)) {
242                 newconf.printer = printer;
243                 return service.printConfig[context] = newconf;
244             } else {
245                 console.warn("configurePrinter() returned " + newconf);
246             }
247         })
248
249         // store the newly linked settings
250         .then(function() {
251             service.setItem('eg.print.config', service.printConfig);
252         })
253
254         // return the final settings to the caller
255         .then(function() {return service.printConfig});
256     }
257
258     service.getPrinters = function() {
259         if (service.printers) // cached printers
260             return $q.when(service.printers);
261
262         return service.attemptHatchDelivery({action : 'printers'}).then(
263
264             // we have remote printers; sort by name and return
265             function(printers) {
266                 service.printers = printers.sort(
267                     function(a,b) {return a.name < b.name ? -1 : 1});
268                 return service.printers;
269             },
270
271             // remote call failed and there is no such thing as local
272             // printers; return empty set.
273             function() { return [] } 
274         );
275     }
276
277     // get the value for a stored item
278     service.getItem = function(key) {
279         return service.getRemoteItem(key)['catch'](
280             function(msg) {
281                 if (service.hatchRequired()) {
282                     console.error("Unable to getItem: " + key
283                      + "; hatchRequired=true, but hatch is not connected");
284                      return null;
285                 }
286                 return service.getLocalItem(msg.key);
287             }
288         );
289     }
290
291     service.getRemoteItem = function(key) {
292         return service.attemptHatchDelivery({
293             key : key,
294             action : 'get', 
295         });
296     }
297
298     service.getLocalItem = function(key) {
299         var val = $window.localStorage.getItem(key);
300         if (val == null) return;
301         return JSON.parse(val);
302     }
303
304     service.setItem = function(key, value) {
305         var str = JSON.stringify(value);
306         return service.setRemoteItem(key, str)['catch'](
307             function(msg) {
308                 if (service.hatchRequired()) {
309                     console.error("Unable to setItem: " + key
310                      + "; hatchRequired=true, but hatch is not connected");
311                      return null;
312                 }
313                 return service.setLocalItem(msg.key, null, str);
314             }
315         );
316     }
317
318     // set the value for a stored or new item
319     service.setRemoteItem = function(key, value) {
320         return service.attemptHatchDelivery({
321             key : key, 
322             value : value, 
323             action : 'set',
324         });
325     }
326
327     // Set the value for the given key
328     // If the value is raw, pass it as 'value'.  If it was
329     // externally JSONified, pass it via jsonified.
330     service.setLocalItem = function(key, value, jsonified) {
331         if (jsonified === undefined ) 
332             jsonified = JSON.stringify(value);
333         $window.localStorage.setItem(key, jsonified);
334     }
335
336     // appends the value to the existing item stored at key.
337     // If not item is found at key, this behaves just like setItem()
338     service.appendItem = function(key, value) {
339         return service.appendRemoteItem(key, value)['catch'](
340             function(msg) {
341                 if (service.hatchRequired()) {
342                     console.error("Unable to appendItem: " + key
343                      + "; hatchRequired=true, but hatch is not connected");
344                      return null;
345                 }
346                 service.appendLocalItem(msg.key, msg.value);
347             }
348         );
349     }
350
351     service.appendRemoteItem = function(key, value) {
352         return service.attemptHatchDelivery({
353             key : key, 
354             value : value, 
355             action : 'append',
356         });
357     }
358
359     // assumes the appender and appendee are both strings
360     // TODO: support arrays as well
361     service.appendLocalItem = function(key, value) {
362         var item = service.getLocalItem(key);
363         if (item) {
364             if (typeof item != 'string') {
365                 logger.warn("egHatch.appendLocalItem => "
366                     + "cannot append to a non-string item: " + key);
367                 return;
368             }
369             value = item + value; // concatenate our value
370         }
371         service.setLocalitem(key, value);
372     }
373
374     // remove a stored item
375     service.removeItem = function(key) {
376         return service.removeRemoteItem(key)['catch'](
377             function(msg) { 
378                 return service.removeLocalItem(msg.key) 
379             }
380         );
381     }
382
383     service.removeRemoteItem = function(key) {
384         return service.attemptHatchDelivery({
385             key : key,
386             action : 'remove'
387         });
388     }
389
390     service.removeLocalItem = function(key) {
391         $window.localStorage.removeItem(key);
392     }
393
394     // if set, prefix limits the return set to keys starting with 'prefix'
395     service.getKeys = function(prefix) {
396         return service.getRemoteKeys(prefix)['catch'](
397             function() { 
398                 if (service.hatchRequired()) {
399                     console.error("Unable to get pref keys; "
400                      + "hatchRequired=true, but hatch is not connected");
401                      return [];
402                 }
403                 return service.getLocalKeys(prefix) 
404             }
405         );
406     }
407
408     service.getRemoteKeys = function(prefix) {
409         return service.attemptHatchDelivery({
410             key : prefix,
411             action : 'keys'
412         });
413     }
414
415     service.getLocalKeys = function(prefix) {
416         var keys = [];
417         var idx = 0;
418         while ( (k = $window.localStorage.key(idx++)) !== null) {
419             // key prefix match test
420             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
421             keys.push(k);
422         }
423         return keys;
424     }
425
426     return service;
427 }])
428