2 * Core Service - egHatch
4 * Dispatches print and data storage requests to the appropriate handler.
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.
11 * Most handlers also provide direct remote and local variants to the
12 * application can decide to which to use as needed.
14 * Local storage requests are handled by $window.localStorage.
16 * Note that all top-level and remote requests return promises. All
17 * local requests return immediate values, since local requests are
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()
25 angular.module('egCoreMod')
28 ['$q','$window','$timeout','$interpolate','$http','$cookies',
29 function($q , $window , $timeout , $interpolate , $http , $cookies) {
33 service.messages = {};
35 service.socket = null;
36 service.hatchAvailable = null;
37 service.defaultHatchURL = 'wss://localhost:8443/hatch';
39 // write a message to the Hatch websocket
40 service.sendToHatch = function(msg) {
43 // shallow copy and scrub msg before sending
44 angular.forEach(msg, function(val, key) {
45 if (key.match(/deferred/)) return;
49 console.debug("sending to Hatch: " + JSON.stringify(msg2,null,2));
50 service.socket.send(JSON.stringify(msg2));
53 // Send the request to Hatch if it's available.
54 // Otherwise handle the request locally.
55 service.attemptHatchDelivery = function(msg) {
57 msg.msgid = service.msgId++;
58 msg.deferred = $q.defer();
60 if (service.hatchAvailable === false) { // Hatch is closed
61 msg.deferred.reject(msg);
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);
68 } else { // Hatch status unknown; attempt to connect
69 service.messages[msg.msgid] = msg;
70 service.pending.push(msg);
71 service.hatchConnect();
74 return msg.deferred.promise;
78 // resolve the promise on the given request and remove
79 // it from our tracked requests.
80 service.resolveRequest = function(msg) {
82 if (!service.messages[msg.msgid]) {
83 console.warn('no cached message for '
84 + msg.msgid + ' : ' + JSON.stringify(msg, null, 2));
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
96 "egHatch command failed : "
97 + JSON.stringify(msg.error, null, 2));
99 msg.deferred.resolve(msg.content);
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];
111 if (service.onHatchClose)
112 service.onHatchClose();
115 service.hatchURL = function() {
116 return service.getLocalItem('eg.hatch.url')
117 || service.defaultHatchURL;
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();
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');
132 service.hatchConnect = function() {
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
142 service.socket = new WebSocket(service.hatchURL());
144 service.hatchAvailable = false;
145 service.hatchClosed();
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);
159 service.socket.onclose = function() {
160 if (service.hatchAvailable === false) return; // already registered
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();
168 service.socket.onerror = function() {
169 if (service.hatchAvailable === false) return; // already registered
170 service.hatchAvailable = false;
172 "unable to connect to Hatch server at " + service.hatchURL());
173 service.hatchClosed();
176 service.socket.onmessage = function(evt) {
177 var msgStr = evt.data;
178 if (!msgStr) throw new Error("Hatch returned empty message");
180 var msgObj = JSON.parse(msgStr);
181 console.debug('Hatch says ' + JSON.stringify(msgObj, null, 2));
182 service.resolveRequest(msgObj);
186 service.getPrintConfig = function() {
187 if (service.printConfig)
188 return $q.when(service.printConfig);
190 return service.getRemoteItem('eg.print.config')
191 .then(function(conf) {
192 return (service.printConfig = conf || {})
196 service.setPrintConfig = function(conf) {
197 service.printConfig = conf;
198 return service.setRemoteItem('eg.print.config', conf);
202 service.remotePrint = function(
203 context, contentType, content, withDialog) {
205 return service.getPrintConfig().then(
207 // print configuration retrieved; print
208 return service.attemptHatchDelivery({
210 config : conf[context],
212 contentType : contentType,
213 showDialog : withDialog,
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) {
223 // load current settings
224 return service.getPrintConfig()
226 // dispatch the print configuration request
227 .then(function(config) {
229 // loaded remote config
230 if (!config[context]) config[context] = {};
231 config[context].printer = printer;
232 return service.attemptHatchDelivery({
234 action : 'print-config',
235 config : config[context]
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;
245 console.warn("configurePrinter() returned " + newconf);
249 // store the newly linked settings
251 service.setItem('eg.print.config', service.printConfig);
254 // return the final settings to the caller
255 .then(function() {return service.printConfig});
258 service.getPrinters = function() {
259 if (service.printers) // cached printers
260 return $q.when(service.printers);
262 return service.attemptHatchDelivery({action : 'printers'}).then(
264 // we have remote printers; sort by name and return
266 service.printers = printers.sort(
267 function(a,b) {return a.name < b.name ? -1 : 1});
268 return service.printers;
271 // remote call failed and there is no such thing as local
272 // printers; return empty set.
273 function() { return [] }
277 // get the value for a stored item
278 service.getItem = function(key) {
279 return service.getRemoteItem(key)['catch'](
281 if (service.hatchRequired()) {
282 console.error("Unable to getItem: " + key
283 + "; hatchRequired=true, but hatch is not connected");
286 return service.getLocalItem(msg.key);
291 service.getRemoteItem = function(key) {
292 return service.attemptHatchDelivery({
298 service.getLocalItem = function(key) {
299 var val = $window.localStorage.getItem(key);
300 if (val == null) return;
301 return JSON.parse(val);
304 service.getLoginSessionItem = function(key) {
305 var val = $cookies.get(key);
306 if (val == null) return;
307 return JSON.parse(val);
310 service.getSessionItem = function(key) {
311 var val = $window.sessionStorage.getItem(key);
312 if (val == null) return;
313 return JSON.parse(val);
317 * @param tmp bool Store the value as a session cookie only.
318 * tmp values are removed during logout or browser close.
320 service.setItem = function(key, value) {
321 var str = JSON.stringify(value);
323 return service.setRemoteItem(key, str)['catch'](
325 if (service.hatchRequired()) {
326 console.error("Unable to setItem: " + key
327 + "; hatchRequired=true, but hatch is not connected");
330 return service.setLocalItem(msg.key, null, str);
335 // set the value for a stored or new item
336 service.setRemoteItem = function(key, value) {
337 return service.attemptHatchDelivery({
344 // Set the value for the given key.
345 // "Local" items persist indefinitely.
346 // If the value is raw, pass it as 'value'. If it was
347 // externally JSONified, pass it via jsonified.
348 service.setLocalItem = function(key, value, jsonified) {
349 if (jsonified === undefined )
350 jsonified = JSON.stringify(value);
351 $window.localStorage.setItem(key, jsonified);
354 // Set the value for the given key.
355 // "LoginSession" items are removed when the user logs out or the
356 // browser is closed.
357 // If the value is raw, pass it as 'value'. If it was
358 // externally JSONified, pass it via jsonified.
359 service.setLoginSessionItem = function(key, value, jsonified) {
360 service.addLoginSessionKey(key);
361 if (jsonified === undefined )
362 jsonified = JSON.stringify(value);
363 $cookies.put(key, jsonified);
366 // Set the value for the given key.
367 // "Session" items are browser tab-specific and are removed when the
369 // If the value is raw, pass it as 'value'. If it was
370 // externally JSONified, pass it via jsonified.
371 service.setSessionItem = function(key, value, jsonified) {
372 if (jsonified === undefined )
373 jsonified = JSON.stringify(value);
374 $window.sessionStorage.setItem(key, jsonified);
377 // appends the value to the existing item stored at key.
378 // If not item is found at key, this behaves just like setItem()
379 service.appendItem = function(key, value) {
380 return service.appendRemoteItem(key, value)['catch'](
382 if (service.hatchRequired()) {
383 console.error("Unable to appendItem: " + key
384 + "; hatchRequired=true, but hatch is not connected");
387 service.appendLocalItem(msg.key, msg.value);
392 service.appendRemoteItem = function(key, value) {
393 return service.attemptHatchDelivery({
400 // assumes the appender and appendee are both strings
401 // TODO: support arrays as well
402 service.appendLocalItem = function(key, value) {
403 var item = service.getLocalItem(key);
405 if (typeof item != 'string') {
406 logger.warn("egHatch.appendLocalItem => "
407 + "cannot append to a non-string item: " + key);
410 value = item + value; // concatenate our value
412 service.setLocalitem(key, value);
415 // remove a stored item
416 service.removeItem = function(key) {
417 return service.removeRemoteItem(key)['catch'](
419 return service.removeLocalItem(msg.key)
424 service.removeRemoteItem = function(key) {
425 return service.attemptHatchDelivery({
431 service.removeLocalItem = function(key) {
432 $window.localStorage.removeItem(key);
435 service.removeLoginSessionItem = function(key) {
436 service.removeLoginSessionKey(key);
437 $cookies.remove(key);
440 service.removeSessionItem = function(key) {
441 $window.sessionStorage.removeItem(key);
445 * Remove all "LoginSession" items.
447 service.clearLoginSessionItems = function() {
448 angular.forEach(service.getLoginSessionKeys(), function(key) {
449 service.removeLoginSessionItem(key);
452 // remove the keys cache.
453 service.removeLocalItem('eg.hatch.login_keys');
456 // if set, prefix limits the return set to keys starting with 'prefix'
457 service.getKeys = function(prefix) {
458 return service.getRemoteKeys(prefix)['catch'](
460 if (service.hatchRequired()) {
461 console.error("Unable to get pref keys; "
462 + "hatchRequired=true, but hatch is not connected");
465 return service.getLocalKeys(prefix)
470 service.getRemoteKeys = function(prefix) {
471 return service.attemptHatchDelivery({
477 service.getLocalKeys = function(prefix) {
480 while ( (k = $window.localStorage.key(idx++)) !== null) {
481 // key prefix match test
482 if (prefix && k.substr(0, prefix.length) != prefix) continue;
490 * Array of "LoginSession" keys.
491 * Note we have to store these as "Local" items so browser tabs can
492 * share them. We could store them as cookies, but it's more data
493 * that has to go back/forth to the server. A "LoginSession" key name is
494 * not private, though, so it's OK if they are left in localStorage
495 * until the next login.
497 service.getLoginSessionKeys = function(prefix) {
500 var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
501 angular.forEach(login_keys, function(k) {
502 // key prefix match test
503 if (prefix && k.substr(0, prefix.length) != prefix) return;
509 service.addLoginSessionKey = function(key) {
510 var keys = service.getLoginSessionKeys();
511 if (keys.indexOf(key) < 0) {
513 service.setLocalItem('eg.hatch.login_keys', keys);
517 service.removeLoginSessionKey = function(key) {
518 var keys = service.getLoginSessionKeys().filter(function(k) {
521 service.setLocalItem('eg.hatch.login_keys', keys);