]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/hatch.js
467091aaae30be54a7af74123aaca0120e372e29
[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  * If Hatch is configured to honor the request -- current request types
7  * are 'settings', 'offline', and 'printing' -- the request will be
8  * relayed to the Hatch service.  Otherwise, the request is handled
9  * locally.
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','$cookies','egNet','$injector',
29     function($q , $window , $timeout , $interpolate , $cookies , egNet , $injector ) {
30
31     var service = {};
32     service.msgId = 1;
33     service.messages = {};
34     service.hatchAvailable = false;
35     service.auth = null;  // ref to egAuth loaded on-demand to avoid circular ref.
36     service.disableServerSettings = false;
37
38     // key/value cache -- avoid unnecessary Hatch extension requests.
39     // Only affects *RemoteItem calls.
40     service.keyCache = {}; 
41
42     // Keep a local copy of all retrieved setting summaries, which indicate
43     // which setting types exist for each setting.  
44     service.serverSettingSummaries = {};
45
46     /**
47      * List string prefixes for On-Call storage keys. On-Call keys
48      * are those that can be set/get/remove'd from localStorage when
49      * Hatch is not avaialable, even though Hatch is configured as the
50      * primary storage location for the key in question.  On-Call keys
51      * are those that allow the user to login and perform basic admin
52      * tasks (like disabling Hatch) even when Hatch is down.
53      * AKA Browser Staff Run Level 3.
54      * Note that no attempt is made to synchronize data between Hatch
55      * and localStorage for On-Call keys.  Only one destation is active 
56      * at a time and each maintains its own data separately.
57      */
58     service.onCallPrefixes = ['eg.workstation'];
59     
60     // Returns true if the key can be set/get in localStorage even when 
61     // Hatch is not available.
62     service.keyIsOnCall = function(key) {
63         var oncall = false;
64         angular.forEach(service.onCallPrefixes, function(pfx) {
65             if (key.match(new RegExp('^' + pfx))) 
66                 oncall = true;
67         });
68         return oncall;
69     }
70
71     /**
72      * Settings with these prefixes will always live in the browser.
73      */
74     service.browserOnlyPrefixes = [
75         'eg.workstation', 
76         'eg.hatch',
77         'eg.cache',
78         'current_tag_table_marc21_biblio',
79         'FFPos',
80         'FFValue'
81     ];
82
83     service.keyStoredInBrowser = function(key) {
84
85         if (service.disableServerSettings) {
86             // When server-side storage is disabled, treat every
87             // setting like it's stored locally.
88             return true;
89         }
90
91         var browserOnly = false;
92         service.browserOnlyPrefixes.forEach(function(pfx) {
93             if (key.match(new RegExp('^' + pfx))) 
94                 browserOnly = true;
95         });
96
97         return browserOnly;
98     }
99
100     // write a message to the Hatch port
101     service.sendToHatch = function(msg) {
102         var msg2 = {};
103
104         // shallow copy and scrub msg before sending
105         angular.forEach(msg, function(val, key) {
106             if (key.match(/deferred/)) return;
107             msg2[key] = val;
108         });
109
110         console.debug("sending to Hatch: " + JSON.stringify(msg2));
111
112         msg2.from = 'page';
113         $window.postMessage(msg2, $window.location.origin);
114     }
115
116     // Send request to Hatch or reject if Hatch is unavailable
117     service.attemptHatchDelivery = function(msg) {
118         msg.msgid = service.msgId++;
119         msg.deferred = $q.defer();
120
121         if (service.hatchAvailable) {
122             service.messages[msg.msgid] = msg;
123             service.sendToHatch(msg);
124
125         } else {
126             console.error(
127                 'Hatch request attempted but Hatch is not available');
128             msg.deferred.reject(msg);
129         }
130
131         return msg.deferred.promise;
132     }
133
134
135     // resolve the promise on the given request and remove
136     // it from our tracked requests.
137     service.resolveRequest = function(msg) {
138
139         if (!service.messages[msg.msgid]) {
140             console.error('no cached message for id = ' + msg.msgid);
141             return;
142         }
143
144         // for requests sent through Hatch, only the cached 
145         // request will have the original promise attached
146         msg.deferred = service.messages[msg.msgid].deferred;
147         delete service.messages[msg.msgid]; // un-cache
148
149         if (msg.status == 200) {
150             msg.deferred.resolve(msg.content);
151         } else {
152             console.warn("Hatch command failed with status=" 
153                 + msg.status + " and message=" + msg.message);
154             msg.deferred.reject();
155         }
156     }
157
158     service.openHatch = function() {
159
160         // When the Hatch extension loads, it tacks an attribute onto
161         // the top-level documentElement to indicate it's available.
162         if (!$window.document.documentElement.getAttribute('hatch-is-open')) {
163             //console.debug("Hatch is not available");
164             return;
165         }
166
167         $window.addEventListener("message", function(event) {
168             // We only accept messages from our own content script.
169             if (event.source != window) return;
170
171             // We only care about messages from the Hatch extension.
172             if (event.data && event.data.from == 'extension') {
173
174                 // Avoid logging full Hatch responses. they can get large.
175                 console.debug(
176                     'Hatch responded to message ID ' + event.data.msgid);
177
178                 service.resolveRequest(event.data);
179             }
180         }); 
181
182         service.hatchAvailable = true; // public flag
183     }
184
185     service.remotePrint = function(
186         context, contentType, content, withDialog) {
187
188         return service.getPrintConfig(context).then(
189             function(config) {
190                 // print configuration retrieved; print
191                 return service.attemptHatchDelivery({
192                     action : 'print',
193                     settings : config,
194                     content : content, 
195                     contentType : contentType,
196                     showDialog : withDialog,
197                 });
198             }
199         );
200     }
201
202     service.getPrintConfig = function(context) {
203         return service.getRemoteItem('eg.print.config.' + context);
204     }
205
206     service.setPrintConfig = function(context, config) {
207         return service.setRemoteItem('eg.print.config.' + context, config);
208     }
209
210     service.getPrinterOptions = function(name) {
211         return service.attemptHatchDelivery({
212             action : 'printer-options',
213             printer : name
214         });
215     }
216
217     service.getPrinters = function() {
218         if (service.printers) // cached printers
219             return $q.when(service.printers);
220
221         return service.attemptHatchDelivery({action : 'printers'}).then(
222
223             // we have remote printers; sort by name and return
224             function(printers) {
225                 service.printers = printers.sort(
226                     function(a,b) {return a.name < b.name ? -1 : 1});
227                 return service.printers;
228             },
229
230             // remote call failed and there is no such thing as local
231             // printers; return empty set.
232             function() { return [] } 
233         );
234     }
235
236     // TODO: once Hatch is printing-only, should probably store
237     // this preference on the server.
238     service.usePrinting = function() {
239         return service.getLocalItem('eg.hatch.enable.printing');
240     }
241
242     service.useSettings = function() {
243         return service.getLocalItem('eg.hatch.enable.settings');
244     }
245
246     service.useOffline = function() {
247         return service.getLocalItem('eg.hatch.enable.offline');
248     }
249
250     // get the value for a stored item
251     service.getItem = function(key) {
252
253         console.debug('getting item: ' + key);
254
255         if (!service.keyStoredInBrowser(key)) {
256             return service.getServerItem(key);
257         }
258
259         var deferred = $q.defer();
260
261         service.getBrowserItem(key).then(
262             function(val) { deferred.resolve(val); },
263
264             function() { // Hatch error
265                 if (service.keyIsOnCall(key)) {
266                     console.warn("Unable to getItem from Hatch: " + key + 
267                         ". Retrieving item from local storage instead");
268                     deferred.resolve(service.getLocalItem(key));
269                 }
270
271                 deferred.reject("Unable to getItem from Hatch: " + key);
272             }
273         );
274
275         return deferred.promise;
276     }
277
278     // Collect values in batch.
279     // For server-stored values espeically, this is more efficient 
280     // than a series of one-off calls.
281     service.getItemBatch = function(keys) {
282         var browserKeys = [];
283         var serverKeys = [];
284
285         // To take full advantage of the getServerItemBatch call,
286         // we have to know in advance which keys to send to the server
287         // vs those to handle in the browser.
288         keys.forEach(function(key) {
289             if (service.keyStoredInBrowser(key)) {
290                 browserKeys.push(key);
291             } else {
292                 serverKeys.push(key);
293             }
294         });
295
296         var settings = {};
297
298         var serverPromise = serverKeys.length === 0 ? $q.when() : 
299             service.getServerItemBatch(serverKeys).then(function(values) {
300                 angular.forEach(values, function(val, key) {
301                     settings[key] = val;
302                 });
303             });
304
305         var browserPromises = [];
306         browserKeys.forEach(function(key) {
307             browserPromises.push(
308                 service.getBrowserItem(key).then(function(val) {
309                     settings[key] = val;
310                 })
311             );
312         });
313
314         return $q.all(browserPromises.concat(serverPromise))
315             .then(function() {return settings});
316     }
317
318     service.getBrowserItem = function(key) {
319         if (service.useSettings()) {
320             if (service.hatchAvailable) {
321                 return service.getRemoteItem(key);
322             }
323         } else {
324             return $q.when(service.getLocalItem(key));
325         }
326         return $q.reject();
327     }
328
329     service.getRemoteItem = function(key) {
330         
331         if (service.keyCache[key] != undefined)
332             return $q.when(service.keyCache[key])
333
334         return service.attemptHatchDelivery({
335             key : key,
336             action : 'get'
337         }).then(function(content) {
338             return service.keyCache[key] = content;
339         });
340     }
341
342     service.getLocalItem = function(key) {
343         var val = $window.localStorage.getItem(key);
344         if (val === null || val === undefined) return;
345         try {
346             return JSON.parse(val);
347         } catch(E) {
348             console.error(
349                 "Deleting invalid JSON for localItem: " + key + " => " + val);
350             service.removeLocalItem(key);
351             return null;
352         }
353     }
354
355     // Force auth cookies to live under path "/" instead of "/eg/staff"
356     // so they may be shared with the Angular app.
357     // There's no way to tell under what path a cookie is stored in
358     // the browser, all we can do is migrate it regardless.
359     service.migrateAuthCookies = function() {
360         [   'eg.auth.token', 
361             'eg.auth.time', 
362             'eg.auth.token.oc', 
363             'eg.auth.time.oc'
364         ].forEach(function(key) {
365             var val = service.getLoginSessionItem(key);
366             if (val) {
367                 $cookies.remove(key, {path: '/eg/staff/'});
368                 service.setLoginSessionItem(key, val);
369             }
370         });
371     }
372
373     service.getLoginSessionItem = function(key) {
374         var val = $cookies.get(key);
375         if (val == null) return;
376         return JSON.parse(val);
377     }
378
379     service.getSessionItem = function(key) {
380         var val = $window.sessionStorage.getItem(key);
381         if (val == null) return;
382         return JSON.parse(val);
383     }
384
385     /**
386      * @param tmp bool Store the value as a session cookie only.  
387      * tmp values are removed during logout or browser close.
388      */
389     service.setItem = function(key, value) {
390
391         if (!service.keyStoredInBrowser(key)) {
392             return service.setServerItem(key, value);
393         }
394
395         var deferred = $q.defer();
396         return service.setBrowserItem(key, value).then(
397             function(val) {deferred.resolve(val);},
398
399             function() { // Hatch error
400
401                 if (service.keyIsOnCall(key)) {
402                     console.warn("Unable to setItem in Hatch: " + 
403                         key + ". Setting in local storage instead");
404
405                     deferred.resolve(service.setLocalItem(key, value));
406                 }
407                 deferred.reject("Unable to setItem in Hatch: " + key);
408             }
409         );
410     }
411
412     service.setBrowserItem = function(key, value) {
413         if (service.useSettings()) {
414             if (service.hatchAvailable) {
415                 return service.setRemoteItem(key, value);
416             } else {
417                 return $q.reject('Unable to get item from hatch');
418             }
419         } else {
420             return $q.when(service.setLocalItem(key, value));
421         }
422     }
423
424     service.setServerItem = function(key, value) {
425         if (!service.auth) service.auth = $injector.get('egAuth');
426         if (!service.auth.token()) return $q.when();
427
428         // If we have already attempted to retrieve a value for this
429         // setting, then we can tell up front whether applying a value
430         // at the server will be an option.  If not, store locally.
431         var summary = service.serverSettingSummaries[key];
432         if (summary && !summary.has_staff_setting) {
433
434             if (summary.has_org_setting === 't') {
435                 // When no user/ws setting types exist but an org unit
436                 // setting type does, it means the value cannot be
437                 // applied by an individual user.  Nothing left to do.
438                 return $q.when();
439             }
440
441             // No setting types of any flavor exist.
442             // Fall back to local storage.
443
444             if (value === null) {
445                 // a null value means clear the server setting.
446                 return service.removeBrowserItem(key);
447             } else {
448                 console.warn('No server setting type exists for ' + key);
449                 return service.setBrowserItem(key, value); 
450             }
451         }
452
453         var settings = {};
454         settings[key] = value;
455
456         return egNet.request(
457             'open-ils.actor',
458             'open-ils.actor.settings.apply.user_or_ws',
459             service.auth.token(), settings
460         ).then(function(appliedCount) {
461
462             if (appliedCount == 0) {
463                 console.warn('No server setting type exists for ' + key);
464                 // We were unable to store the setting on the server,
465                 // presumably becuase no server-side setting type exists.
466                 // Add to local storage instead.
467                 service.setLocalItem(key, value);
468             }
469
470             service.keyCache[key] = value;
471             return appliedCount;
472         });
473     }
474
475     service.getServerItem = function(key) {
476         if (key in service.keyCache) {
477             return $q.when(service.keyCache[key])
478         }
479
480         if (!service.auth) service.auth = $injector.get('egAuth');
481         if (!service.auth.token()) return $q.when(null);
482
483         return egNet.request(
484             'open-ils.actor',
485             'open-ils.actor.settings.retrieve.atomic',
486             [key], service.auth.token()
487         ).then(function(settings) {
488             return service.handleServerItemResponse(settings[0]);
489         });
490     }
491
492     service.handleServerItemResponse = function(summary) {
493         var key = summary.name;
494         var val = summary.value;
495
496         // For our purposes, we only care if a setting can be stored
497         // as an org setting or a user-or-workstation setting.
498         summary.has_staff_setting = (
499             summary.has_user_setting === 't' || 
500             summary.has_workstation_setting === 't'
501         );
502
503         summary.value = null; // avoid duplicate value caches
504         service.serverSettingSummaries[key] = summary;
505
506         if (val !== null) {
507             // We have a server setting.  Nothing left to do.
508             return $q.when(service.keyCache[key] = val);
509         }
510
511         if (!summary.has_staff_setting) {
512
513             if (summary.has_org_setting === 't') {
514                 // An org unit setting type exists but no value is applied
515                 // that this workstation has access to.  The existence of 
516                 // an org unit setting type and no user/ws setting type 
517                 // means applying a value locally is not allowed.  
518                 return $q.when(service.keyCache[key] = undefined);
519             }
520
521             console.warn('No server setting type exists for ' 
522                 + key + ', using local value.');
523
524             return service.getBrowserItem(key);
525         }
526
527         // A user/ws setting type exists, but no server value exists.
528         // Migrate the local setting to the server.
529
530         var deferred = $q.defer();
531         service.getBrowserItem(key).then(function(browserVal) {
532
533             if (browserVal === null || browserVal === undefined) {
534                 // No local value to migrate.
535                 return deferred.resolve(service.keyCache[key] = undefined);
536             }
537
538             // Migrate the local value to the server.
539
540             service.setServerItem(key, browserVal).then(
541                 function(appliedCount) {
542                     if (appliedCount == 1) {
543                         console.info('setting ' + key + ' successfully ' +
544                             'migrated to a server setting');
545                         service.removeBrowserItem(key); // fire & forget
546                     } else {
547                         console.error('error migrating setting to server,' 
548                             + ' falling back to local value');
549                     }
550                     deferred.resolve(service.keyCache[key] = browserVal);
551                 }
552             );
553         });
554
555         return deferred.promise;
556     }
557
558     service.getServerItemBatch = function(keys) {
559         // no cache checking for now.  assumes batch mode is only
560         // called once on page load.  maybe add cache checking later.
561         if (!service.auth) service.auth = $injector.get('egAuth');
562         if (!service.auth.token()) return $q.when({});
563
564         var foundValues = {};
565         return egNet.request(
566             'open-ils.actor',
567             'open-ils.actor.settings.retrieve.atomic',
568             keys, service.auth.token()
569         ).then(
570             function(settings) { 
571                 //return foundValues; 
572
573                 var deferred = $q.defer();
574                 function checkOne(setting) {
575                     if (!setting) {
576                         deferred.resolve(foundValues);
577                         return;
578                     }
579                     service.handleServerItemResponse(setting)
580                     .then(function(resp) {
581                         if (resp !== undefined) {
582                             foundValues[setting.name] = resp;
583                         }
584                         settings.shift();
585                         checkOne(settings[0]);
586                     });
587                 }
588
589                 checkOne(settings[0]);
590                 return deferred.promise;
591             }
592         );
593     }
594
595
596     // set the value for a stored or new item
597     service.setRemoteItem = function(key, value) {
598         service.keyCache[key] = value;
599         return service.attemptHatchDelivery({
600             key : key, 
601             content : value, 
602             action : 'set',
603         });
604     }
605
606     // Set the value for the given key.
607     // "Local" items persist indefinitely.
608     // If the value is raw, pass it as 'value'.  If it was
609     // externally JSONified, pass it via jsonified.
610     service.setLocalItem = function(key, value, jsonified) {
611         if (jsonified === undefined ) {
612             jsonified = JSON.stringify(value);
613         } else if (value === undefined) {
614             return;
615         }
616         try {
617             $window.localStorage.setItem(key, jsonified);
618         } catch (e) {
619             console.log('localStorage.setItem (overwrite) failed for '+key+': ', e);
620         }
621     }
622
623     service.appendItem = function(key, value) {
624         if (!service.useSettings())
625             return $q.when(service.appendLocalItem(key, value));
626
627         if (service.hatchAvailable)
628             return service.appendRemoteItem(key, value);
629
630         if (service.keyIsOnCall(key)) {
631             console.warn("Unable to appendItem in Hatch: " + 
632                 key + ". Setting in local storage instead");
633
634             return $q.when(service.appendLocalItem(key, value));
635         }
636
637         console.error("Unable to appendItem in Hatch: " + key);
638         return $q.reject();
639     }
640
641     // append the value to a stored or new item
642     service.appendRemoteItem = function(key, value) {
643         service.keyCache[key] = value;
644         return service.attemptHatchDelivery({
645             key : key, 
646             content : value, 
647             action : 'append',
648         });
649     }
650
651     service.appendLocalItem = function(key, value, jsonified) {
652         if (jsonified === undefined ) 
653             jsonified = JSON.stringify(value);
654
655         var old_value = $window.localStorage.getItem(key) || '';
656         try {
657             $window.localStorage.setItem( key, old_value + jsonified );
658         } catch (e) {
659             console.log('localStorage.setItem (append) failed for '+key+': ', e);
660         }
661     }
662
663     // Set the value for the given key.  
664     // "LoginSession" items are removed when the user logs out or the 
665     // browser is closed.
666     // If the value is raw, pass it as 'value'.  If it was
667     // externally JSONified, pass it via jsonified.
668     service.setLoginSessionItem = function(key, value, jsonified) {
669         service.addLoginSessionKey(key);
670         if (jsonified === undefined ) 
671             jsonified = JSON.stringify(value);
672         $cookies.put(key, jsonified, {path: '/'});
673     }
674
675     // Set the value for the given key.  
676     // "Session" items are browser tab-specific and are removed when the
677     // tab is closed.
678     // If the value is raw, pass it as 'value'.  If it was
679     // externally JSONified, pass it via jsonified.
680     service.setSessionItem = function(key, value, jsonified) {
681         if (jsonified === undefined ) 
682             jsonified = JSON.stringify(value);
683         $window.sessionStorage.setItem(key, jsonified);
684     }
685
686     // remove a stored item
687     service.removeItem = function(key) {
688
689         if (!service.keyStoredInBrowser(key)) {
690             return service.removeServerItem(key);
691         }
692
693         var deferred = $q.defer();
694         service.removeBrowserItem(key).then(
695             function(response) {deferred.resolve(response);},
696             function() { // Hatch error
697
698                 if (service.keyIsOnCall(key)) {
699                     console.warn("Unable to removeItem from Hatch: " + key + 
700                         ". Removing item from local storage instead");
701
702                     deferred.resolve(service.removeLocalItem(key));
703                 }
704
705                 deferred.reject("Unable to removeItem from Hatch: " + key);
706             }
707         );
708
709         return deferred.promise;
710     }
711
712     service.removeBrowserItem = function(key) {
713         if (service.useSettings()) {
714             if (service.hatchAvailable) {
715                 return service.removeRemoteItem(key);
716             } else {
717                 return $q.reject('error talking to Hatch');
718             }
719         } else {
720             return $q.when(service.removeLocalItem(key));
721         }
722     }
723
724     service.removeServerItem = function(key) {
725         return service.setServerItem(key, null);
726     }
727
728     service.removeRemoteItem = function(key) {
729         delete service.keyCache[key];
730         return service.attemptHatchDelivery({
731             key : key,
732             action : 'remove'
733         });
734     }
735
736     service.removeLocalItem = function(key) {
737         $window.localStorage.removeItem(key);
738     }
739
740     service.removeLoginSessionItem = function(key) {
741         service.removeLoginSessionKey(key);
742         $cookies.remove(key, {path: '/'});
743     }
744
745     service.removeSessionItem = function(key) {
746         $window.sessionStorage.removeItem(key);
747     }
748
749     /**
750      * Remove all "LoginSession" items.
751      */
752     service.clearLoginSessionItems = function() {
753         angular.forEach(service.getLoginSessionKeys(), function(key) {
754             service.removeLoginSessionItem(key);
755         });
756
757         // remove the keys cache.
758         service.removeLocalItem('eg.hatch.login_keys');
759     }
760
761     // if set, prefix limits the return set to keys starting with 'prefix'
762     service.getKeys = function(prefix) {
763         var promise = service.getServerKeys(prefix);
764         return service.getBrowserKeys(prefix).then(function(browserKeys) {
765             return promise.then(function(serverKeys) {
766                 return serverKeys.concat(browserKeys);
767             });
768         });
769     }
770
771     service.getRemoteKeys = function(prefix) {
772         return service.attemptHatchDelivery({
773             key : prefix,
774             action : 'keys'
775         });
776     }
777
778     service.getBrowserKeys = function(prefix) {
779         if (service.useSettings()) 
780             return service.getRemoteKeys(prefix);
781         return $q.when(service.getLocalKeys(prefix));
782     }
783
784     service.getServerKeys = function(prefix, options) {
785         if (!service.auth) service.auth = $injector.get('egAuth');
786         if (!service.auth.token()) return $q.when({});
787         return egNet.request(
788             'open-ils.actor',
789             'open-ils.actor.settings.staff.applied.names.authoritative.atomic',
790             service.auth.token(), prefix, options
791         );
792     }
793
794     service.getLocalKeys = function(prefix) {
795         var keys = [];
796         var idx = 0;
797         while ( (k = $window.localStorage.key(idx++)) !== null) {
798             // key prefix match test
799             if (prefix && k.substr(0, prefix.length) != prefix) continue; 
800             keys.push(k);
801         }
802         return keys;
803     }
804
805
806     /**
807      * Array of "LoginSession" keys.
808      * Note we have to store these as "Local" items so browser tabs can
809      * share them.  We could store them as cookies, but it's more data
810      * that has to go back/forth to the server.  A "LoginSession" key name is
811      * not private, though, so it's OK if they are left in localStorage
812      * until the next login.
813      */
814     service.getLoginSessionKeys = function(prefix) {
815         var keys = [];
816         var idx = 0;
817         var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
818         angular.forEach(login_keys, function(k) {
819             // key prefix match test
820             if (prefix && k.substr(0, prefix.length) != prefix) return;
821             keys.push(k);
822         });
823         return keys;
824     }
825
826     service.addLoginSessionKey = function(key) {
827         var keys = service.getLoginSessionKeys();
828         if (keys.indexOf(key) < 0) {
829             keys.push(key);
830             service.setLocalItem('eg.hatch.login_keys', keys);
831         }
832     }
833
834     service.removeLoginSessionKey = function(key) {
835         var keys = service.getLoginSessionKeys().filter(function(k) {
836             return k != key;
837         });
838         service.setLocalItem('eg.hatch.login_keys', keys);
839     }
840
841     // Copy all stored settings from localStorage to Hatch.
842     // If 'move' is true, delete the local settings once cloned.
843     service.copySettingsToHatch = function(move) {
844         var deferred = $q.defer();
845         var keys = service.getLocalKeys();
846
847         angular.forEach(keys, function(key) {
848
849             // Hatch keys are local-only
850             if (key.match(/^eg.hatch/)) return;
851
852             console.debug("Copying to Hatch Storage: " + key);
853             service.setRemoteItem(key, service.getLocalItem(key))
854             .then(function() { // key successfully cloned.
855
856                 // delete the local copy if requested.
857                 if (move) service.removeLocalItem(key);
858
859                 // resolve the promise after processing the last key.
860                 if (key == keys[keys.length-1]) 
861                     deferred.resolve();
862             });
863         });
864
865         return deferred.promise;
866     }
867
868     // Copy all stored settings from Hatch to localStorage.
869     // If 'move' is true, delete the Hatch settings once cloned.
870     service.copySettingsToLocal = function(move) {
871         var deferred = $q.defer();
872
873         service.getRemoteKeys().then(function(keys) {
874             angular.forEach(keys, function(key) {
875                 service.getRemoteItem(key).then(function(val) {
876
877                     console.debug("Copying to Local Storage: " + key);
878                     service.setLocalItem(key, val);
879
880                     // delete the remote copy if requested.
881                     if (move) service.removeRemoteItem(key);
882
883                     // resolve the promise after processing the last key.
884                     if (key == keys[keys.length-1]) 
885                         deferred.resolve();
886                 });
887             });
888         });
889
890         return deferred.promise;
891     }
892
893     // The only requirement for opening Hatch is that the DOM be loaded.
894     // Open the connection now so its state will be immediately available.
895     service.openHatch();
896
897     return service;
898 }])
899