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',
29 function($q , $window , $timeout , $interpolate , $http) {
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.getSessionItem = function(key) {
305 var val = $window.sessionStorage.getItem(key);
306 if (val == null) return;
307 return JSON.parse(val);
310 service.setItem = function(key, value) {
311 var str = JSON.stringify(value);
312 return service.setRemoteItem(key, str)['catch'](
314 if (service.hatchRequired()) {
315 console.error("Unable to setItem: " + key
316 + "; hatchRequired=true, but hatch is not connected");
319 return service.setLocalItem(msg.key, null, str);
324 // set the value for a stored or new item
325 service.setRemoteItem = function(key, value) {
326 return service.attemptHatchDelivery({
333 // Set the value for the given key
334 // If the value is raw, pass it as 'value'. If it was
335 // externally JSONified, pass it via jsonified.
336 service.setLocalItem = function(key, value, jsonified) {
337 if (jsonified === undefined )
338 jsonified = JSON.stringify(value);
339 $window.localStorage.setItem(key, jsonified);
342 service.setSessionItem = function(key, value, jsonified) {
343 if (jsonified === undefined )
344 jsonified = JSON.stringify(value);
345 $window.sessionStorage.setItem(key, jsonified);
348 // appends the value to the existing item stored at key.
349 // If not item is found at key, this behaves just like setItem()
350 service.appendItem = function(key, value) {
351 return service.appendRemoteItem(key, value)['catch'](
353 if (service.hatchRequired()) {
354 console.error("Unable to appendItem: " + key
355 + "; hatchRequired=true, but hatch is not connected");
358 service.appendLocalItem(msg.key, msg.value);
363 service.appendRemoteItem = function(key, value) {
364 return service.attemptHatchDelivery({
371 // assumes the appender and appendee are both strings
372 // TODO: support arrays as well
373 service.appendLocalItem = function(key, value) {
374 var item = service.getLocalItem(key);
376 if (typeof item != 'string') {
377 logger.warn("egHatch.appendLocalItem => "
378 + "cannot append to a non-string item: " + key);
381 value = item + value; // concatenate our value
383 service.setLocalitem(key, value);
386 // remove a stored item
387 service.removeItem = function(key) {
388 return service.removeRemoteItem(key)['catch'](
390 return service.removeLocalItem(msg.key)
395 service.removeRemoteItem = function(key) {
396 return service.attemptHatchDelivery({
402 service.removeLocalItem = function(key) {
403 $window.localStorage.removeItem(key);
406 service.removeSessionItem = function(key) {
407 $window.sessionStorage.removeItem(key);
410 // if set, prefix limits the return set to keys starting with 'prefix'
411 service.getKeys = function(prefix) {
412 return service.getRemoteKeys(prefix)['catch'](
414 if (service.hatchRequired()) {
415 console.error("Unable to get pref keys; "
416 + "hatchRequired=true, but hatch is not connected");
419 return service.getLocalKeys(prefix)
424 service.getRemoteKeys = function(prefix) {
425 return service.attemptHatchDelivery({
431 service.getLocalKeys = function(prefix) {
434 while ( (k = $window.localStorage.key(idx++)) !== null) {
435 // key prefix match test
436 if (prefix && k.substr(0, prefix.length) != prefix) continue;