[open-ils-commits] [GIT] Evergreen ILS branch rel_3_0 updated. 273458c51cd9b0ba12085ac0f0062fd5d395de40

Evergreen Git git at git.evergreen-ils.org
Fri Aug 10 11:20:48 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, rel_3_0 has been updated
       via  273458c51cd9b0ba12085ac0f0062fd5d395de40 (commit)
       via  9b0fa1aab258395e70123b4e82c4cedb9895b27f (commit)
       via  6984e2f724f355dd8a2f09df5d22dcda65ad60cb (commit)
       via  b65a9ca3c52f45e9f386f8bb65e54c960faf8101 (commit)
      from  5972999bc79b15333e9c58caf843de3ba55abc0b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 273458c51cd9b0ba12085ac0f0062fd5d395de40
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 9 10:20:54 2018 -0400

    LP#1775719 Modify array check for Phantomjs; reduce logging;
    
    PhantomJS doesn't support the handy [1,2,3].includes(1) function, so
    replace it with the tried-and-true indexOf();
    
    Avoid logging errors on failure to connect to the shared offline worker
    when running within phantomjs.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/lovefield.js b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
index cfd787f..561f900 100644
--- a/Open-ILS/web/js/ui/default/staff/services/lovefield.js
+++ b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
@@ -33,7 +33,10 @@ angular.module('egCoreMod')
         }
 
         service.worker.onerror = function(err) {
-            console.error('Error loading shared worker', err);
+            // avoid spamming unit test runner on failure to connect.
+            if (!navigator.userAgent.match(/PhantomJS/)) {
+                console.error('Error loading shared worker', err);
+            }
             service.cannotConnect = true;
         }
 
@@ -108,7 +111,7 @@ angular.module('egCoreMod')
     // Create and connect to the give schema
     service.connectToSchema = function(schema) {
 
-        if (service.connectedSchemas.includes(schema)) {
+        if (service.connectedSchemas.indexOf(schema) >= 0) {
             // already connected
             return $q.when();
         }

commit 9b0fa1aab258395e70123b4e82c4cedb9895b27f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Aug 7 12:10:17 2018 -0400

    LP#1768947 Disable offline download button, improve logging
    
    Once the offline block list download button is clicked, it's disabled to
    prevent any possibility of double-clicks.  The shared worker also
    reports a more meaningful error in case a double-click sneaks past.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/offline-interface.tt2 b/Open-ILS/src/templates/staff/offline-interface.tt2
index 725b7c3..a6076fc 100644
--- a/Open-ILS/src/templates/staff/offline-interface.tt2
+++ b/Open-ILS/src/templates/staff/offline-interface.tt2
@@ -35,6 +35,7 @@
         </button>
         <button
           class="btn btn-default"
+          ng-disabled="buildingBlockList"
           ng-if="logged_in"
           ng-click="downloadBlockList()">
             [% l('Download block list') %]
diff --git a/Open-ILS/web/js/ui/default/staff/offline-db-worker.js b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
index 6c2e1ad..3239900 100644
--- a/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
+++ b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
@@ -207,7 +207,11 @@ var buildingBlockList = false;
 // Fetches the offline block list and rebuilds the offline blocks
 // table from the new data.
 function populateBlockList(authtoken) {
-    if (buildingBlockList) return;
+
+    if (buildingBlockList) {
+        return Promise.reject('Block list download already in progress');
+    }
+
     buildingBlockList = true;
 
     var url = '/standalone/list.txt?ses=' + 
@@ -234,6 +238,7 @@ function populateBlockList(authtoken) {
                         }
                     );
                 } else {
+                    buildingBlockList = false;
                     reject('Error fetching offline block list');
                 }
             }
@@ -300,7 +305,7 @@ function insertOfflineBlocks(lines) {
 function insertOfflineChunks(chunks, offset, resolve, reject) {
     var chunk = chunks[offset];
     if (!chunk || chunk.length === 0) {
-        console.debug('Block list successfully stored');
+        console.debug('Block list store completed');
         return resolve();
     }
 
diff --git a/Open-ILS/web/js/ui/default/staff/offline.js b/Open-ILS/web/js/ui/default/staff/offline.js
index 6b0f41f..d2a1350 100644
--- a/Open-ILS/web/js/ui/default/staff/offline.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -393,7 +393,9 @@ function($routeProvider , $locationProvider , $compileProvider) {
             } 
         });
 
+        $scope.buildingBlockList = false;
         $scope.downloadBlockList = function () {
+            $scope.buildingBlockList = true;
             egProgressDialog.open();
             egLovefield.populateBlockList().then(
                 function(){
@@ -403,7 +405,10 @@ function($routeProvider , $locationProvider , $compileProvider) {
                     ngToast.warning(egCore.strings.OFFLINE_BLOCKLIST_FAIL);
                     egCore.audio.play('warning.offline.blocklist_fail');
                 }
-            )['finally'](egProgressDialog.close);
+            )['finally'](function() {
+                $scope.buildingBlockList = false;
+                egProgressDialog.close();
+            });
         }
 
         $scope.createOfflineXactBlob = function () {

commit 6984e2f724f355dd8a2f09df5d22dcda65ad60cb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jun 22 10:56:02 2018 -0400

    LP#1768947 Offline xact presence is cached; show date
    
    Maintain an entry to the object date cache table indicating the time of
    the most recent offline transaction entry.  This data is used on the
    login page to determine if offline transactions exist, so the staff
    logging in can be notified.  We do this in lieu of checking the offline
    transaction table, since that table only exists in the offline UI.
    
    As a bonus, since we know the last transaction add time, display this
    information in the login page offline xact alert panel.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/t_login.tt2 b/Open-ILS/src/templates/staff/t_login.tt2
index e4d42cb..0f58cea 100644
--- a/Open-ILS/src/templates/staff/t_login.tt2
+++ b/Open-ILS/src/templates/staff/t_login.tt2
@@ -49,9 +49,14 @@
               </div>
             </div>
 
-            <div class="form-group">
-              <div class="col-md-offset-4 col-md-6">
-                <span ng-show="pendingXacts" class="label label-warning">[% l('Unprocessed offline transactions waiting for upload') %]</span>
+            <div class="form-group" ng-if="pendingXacts">
+              <div class="col-md-offset-4 col-md-8">
+                <div class="alert alert-warning">
+                  [% | l('{{pendingXacts | date:"short"}}') %]
+                  Unprocessed offline transactions waiting for upload.  
+                  Last transaction added at [_1].
+                  [% END %]
+                </div>
               </div>
             </div>
 
diff --git a/Open-ILS/web/js/ui/default/staff/offline-db-worker.js b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
index 0107dfd..6c2e1ad 100644
--- a/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
+++ b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
@@ -181,6 +181,15 @@ function deleteAll(schemaName, tableName) {
     return info.schema.db.delete().from(info.table).exec();
 }
 
+// Delete rows from selected table where field equals value
+function deleteWhereEqual(schemaName, tableName, field, value) {
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.delete().from(info.table)
+        .where(info.table[field].eq(value)).exec();
+}
+
 // Resolves to true if the selected table contains any rows.
 function hasRows(schemaName, tableName) {
 
@@ -370,6 +379,11 @@ function dispatchRequest(port, data) {
             deleteAll(data.schema, data.table).then(replySuccess, replyError);
             break;
 
+        case 'deleteWhereEqual':
+            deleteWhereEqual(data.schema, data.table, data.field, data.value)
+                .then(replySuccess, replyError);
+            break;
+
         case 'hasRows':
             hasRows(data.schema, data.table).then(replySuccess, replyError);
             break;
diff --git a/Open-ILS/web/js/ui/default/staff/services/lovefield.js b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
index b78d316..cfd787f 100644
--- a/Open-ILS/web/js/ui/default/staff/services/lovefield.js
+++ b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
@@ -83,6 +83,8 @@ angular.module('egCoreMod')
 
     // Connects if necessary to the active schemas then relays the request.
     service.request = function(args) {
+        // useful, but very chatty, leaving commented out.
+        // console.debug('egLovfield sending request: ', args);
         return service.connectToSchemas().then(
             function() {
                 return service.relayRequest(args);
@@ -154,19 +156,34 @@ angular.module('egCoreMod')
         );
     }
 
+    // Remove all pending offline transactions and delete the cached
+    // offline transactions date to indicate no transactions remain.
     service.destroyPendingOfflineXacts = function () {
         return service.request({
             schema: 'offline',
             table: 'OfflineXact',
             action: 'deleteAll'
+        }).then(function() {
+            return service.request({
+                schema: 'cache',
+                table: 'CacheDate',
+                action: 'deleteWhereEqual',
+                field: 'type',
+                value: '_offlineXact'
+            });
         });
     }
 
+    // Returns the cache date when xacts exit, null otherwise.
     service.havePendingOfflineXacts = function () {
         return service.request({
-            schema: 'offline',
-            table: 'OfflineXact',
-            action: 'hasRows'
+            schema: 'cache',
+            table: 'CacheDate',
+            action: 'selectWhereEqual',
+            field: 'type',
+            value: '_offlineXact'
+        }).then(function(results) {
+            return results[0] ? results[0].cachedate : null;
         });
     }
 
@@ -180,6 +197,24 @@ angular.module('egCoreMod')
         });
     }
 
+    // Add an offline transaction and update the cache indicating
+    // now() as the most recent addition of an offline xact.
+    service.addOfflineXact = function (obj) {
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineXact',
+            action: 'insertOrReplace',
+            rows: [{value: obj}]
+        }).then(function() {
+            return service.request({
+                schema: 'cache',
+                table: 'CacheDate',
+                action: 'insertOrReplace',
+                rows: [{type: '_offlineXact', cachedate : new Date()}]
+            });
+        });
+    }
+
     service.populateBlockList = function() {
         return service.request({
             action: 'populateBlockList',
@@ -201,15 +236,6 @@ angular.module('egCoreMod')
         });
     }
 
-    service.addOfflineXact = function (obj) {
-        return service.request({
-            schema: 'offline',
-            table: 'OfflineXact',
-            action: 'insertOrReplace',
-            rows: [{value: obj}]
-        });
-    }
-
     service.setStatCatsCache = function (statcats) {
         if (lf.isOffline || !statcats || statcats.length === 0) 
             return $q.when();

commit b65a9ca3c52f45e9f386f8bb65e54c960faf8101
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jun 8 13:08:18 2018 -0400

    LP#1768947 Offline DB runs in shared web worker
    
    Move the lovefield database access logic into a shared web worker
    script.  This ensures the only one connection (per schema) can exist,
    avoiding data integrity problems caused by having multiple tabs writing
    to the database at the same time.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Jeff Davis <jdavis at sitka.bclibraries.ca>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/offline-db-worker.js b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
new file mode 100644
index 0000000..0107dfd
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/offline-db-worker.js
@@ -0,0 +1,394 @@
+importScripts('/js/ui/default/staff/build/js/lovefield.min.js');
+
+// Collection of schema tracking objects.
+var schemas = {};
+
+// Create the DB schema / tables
+// synchronous
+function createSchema(schemaName) {
+    if (schemas[schemaName]) return;
+
+    var meta = lf.schema.create(schemaName, 2);
+    schemas[schemaName] = {name: schemaName, meta: meta};
+
+    switch (schemaName) {
+        case 'cache':
+            createCacheTables(meta);
+            break;
+        case 'offline':
+            createOfflineTables(meta);
+            break;
+        default:
+            console.error('No schema definition for ' + schemaName);
+    }
+}
+
+// Offline cache tables are globally available in the staff client
+// for on-demand caching.
+function createCacheTables(meta) {
+
+    meta.createTable('Setting').
+        addColumn('name', lf.Type.STRING).
+        addColumn('value', lf.Type.STRING).
+        addPrimaryKey(['name']);
+
+    meta.createTable('Object').
+        addColumn('type', lf.Type.STRING).         // class hint
+        addColumn('id', lf.Type.STRING).           // obj id
+        addColumn('object', lf.Type.OBJECT).
+        addPrimaryKey(['type','id']);
+
+    meta.createTable('CacheDate').
+        addColumn('type', lf.Type.STRING).          // class hint
+        addColumn('cachedate', lf.Type.DATE_TIME).  // when was it last updated
+        addPrimaryKey(['type']);
+
+    meta.createTable('StatCat').
+        addColumn('id', lf.Type.INTEGER).
+        addColumn('value', lf.Type.OBJECT).
+        addPrimaryKey(['id']);
+}
+
+// Offline transaction and block list tables.  These can be bulky and
+// are only used in the offline UI.
+function createOfflineTables(meta) {
+
+    meta.createTable('OfflineXact').
+        addColumn('seq', lf.Type.INTEGER).
+        addColumn('value', lf.Type.OBJECT).
+        addPrimaryKey(['seq'], true);
+
+    meta.createTable('OfflineBlocks').
+        addColumn('barcode', lf.Type.STRING).
+        addColumn('reason', lf.Type.STRING).
+        addPrimaryKey(['barcode']);
+}
+
+// Connect to the database for a given schema
+function connect(schemaName) {
+
+    var schema = schemas[schemaName];
+    if (!schema) {
+        return Promise.reject('createSchema(' +
+            schemaName + ') call required');
+    }
+
+    if (schema.db) { // already connected.
+        return Promise.resolve();
+    }
+
+    return new Promise(function(resolve, reject) {
+        try {
+            schema.meta.connect().then(
+                function(db) {
+                    schema.db = db;
+                    resolve();
+                },
+                function(err) {
+                    reject('Error connecting to schema ' +
+                        schemaName + ' : ' + err);
+                }
+            );
+        } catch (E) {
+            reject('Error connecting to schema ' + schemaName + ' : ' + E);
+        }
+    });
+}
+
+function getTableInfo(schemaName, tableName) {
+    var schema = schemas[schemaName];
+    var info = {};
+
+    if (!schema) {
+        info.error = 'createSchema(' + schemaName + ') call required';
+
+    } else if (!schema.db) {
+        info.error = 'connect(' + schemaName + ') call required';
+
+    } else {
+        info.schema = schema;
+        info.table = schema.meta.getSchema().table(tableName);
+
+        if (!info.table) {
+            info.error = 'no such table ' + tableName;
+        }
+    }
+
+    return info;
+}
+
+// Returns a promise resolved with true on success
+// Note insert .exec() returns rows, but that can get bulky on large
+// inserts, hence the boolean return;
+function insertOrReplace(schemaName, tableName, objects) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    var rows = objects.map(function(r) { return info.table.createRow(r) });
+    return info.schema.db.insertOrReplace().into(info.table)
+        .values(rows).exec().then(function() { return true; });
+}
+
+// Returns a promise resolved with true on success
+// Note insert .exec() returns rows, but that can get bulky on large
+// inserts, hence the boolean return;
+function insert(schemaName, tableName, objects) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    var rows = objects.map(function(r) { return info.table.createRow(r) });
+    return info.schema.db.insert().into(info.table)
+        .values(rows).exec().then(function() { return true; });
+}
+
+// Returns rows where the selected field equals the provided value.
+function selectWhereEqual(schemaName, tableName, field, value) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.select().from(info.table)
+        .where(info.table[field].eq(value)).exec();
+}
+
+// Returns rows where the selected field equals the provided value.
+function selectWhereIn(schemaName, tableName, field, value) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.select().from(info.table)
+        .where(info.table[field].in(value)).exec();
+}
+
+// Returns all rows in the selected table
+function selectAll(schemaName, tableName) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.select().from(info.table).exec();
+}
+
+// Deletes all rows in the selected table.
+function deleteAll(schemaName, tableName) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.delete().from(info.table).exec();
+}
+
+// Resolves to true if the selected table contains any rows.
+function hasRows(schemaName, tableName) {
+
+    var info = getTableInfo(schemaName, tableName);
+    if (info.error) { return Promise.reject(info.error); }
+
+    return info.schema.db.select().from(info.table).limit(1).exec()
+        .then(function(rows) { return rows.length > 0 });
+}
+
+
+// Prevent parallel block list building calls, since it does a lot.
+var buildingBlockList = false;
+
+// Fetches the offline block list and rebuilds the offline blocks
+// table from the new data.
+function populateBlockList(authtoken) {
+    if (buildingBlockList) return;
+    buildingBlockList = true;
+
+    var url = '/standalone/list.txt?ses=' + 
+        authtoken + '&' + new Date().getTime();
+
+    console.debug('Fetching offline block list from: ' + url);
+
+    return new Promise(function(resolve, reject) {
+
+        var xhttp = new XMLHttpRequest();
+        xhttp.onreadystatechange = function() {
+            if (this.readyState === 4) {
+                if (this.status === 200) {
+                    var blocks = xhttp.responseText;
+                    var lines = blocks.split('\n');
+                    insertOfflineBlocks(lines).then(
+                        function() {
+                            buildingBlockList = false;
+                            resolve();
+                        },
+                        function(e) {
+                            buildingBlockList = false;
+                            reject(e);
+                        }
+                    );
+                } else {
+                    reject('Error fetching offline block list');
+                }
+            }
+        };
+
+        xhttp.open('GET', url, true);
+        xhttp.send();
+    });
+}
+
+// Rebuild the offline blocks table with the provided blocks, one per line.
+function insertOfflineBlocks(lines) {
+    console.debug('Fetched ' + lines.length + ' blocks');
+
+    // Clear the table first
+    return deleteAll('offline', 'OfflineBlocks').then(
+        function() { 
+
+            console.debug('Cleared existing offline blocks');
+
+            // Create a single batch of rows for insertion.
+            var chunks = [];
+            var currentChunk = [];
+            var chunkSize = 10000;
+            var seen = {bc: {}}; // for easier delete
+
+            chunks.push(currentChunk);
+            lines.forEach(function(line) {
+                // slice/substring instead of split(' ') to handle barcodes
+                // with trailing spaces.
+                var barcode = line.slice(0, -2);
+                var reason = line.substring(line.length - 1);
+                
+                // Trim duplicate barcodes, since only one version of each 
+                // block per barcode is kept in the offline block list
+                if (seen.bc[barcode]) return;
+                seen.bc[barcode] = true;
+
+                if (currentChunk.length >= chunkSize) {
+                    currentChunk = [];
+                    chunks.push(currentChunk);
+                }
+
+                currentChunk.push({barcode: barcode, reason: reason});
+            });
+
+            delete seen.bc; // allow this hunk to be reclaimed
+
+            console.debug('offline data broken into ' + 
+                chunks.length + ' chunks of size ' + chunkSize);
+
+            return new Promise(function(resolve, reject) {
+                insertOfflineChunks(chunks, 0, resolve, reject);
+            });
+        }, 
+
+        function(err) {
+            console.error('Error clearing offline table: ' + err);
+            return Promise.reject(err);
+        }
+    );
+}
+
+function insertOfflineChunks(chunks, offset, resolve, reject) {
+    var chunk = chunks[offset];
+    if (!chunk || chunk.length === 0) {
+        console.debug('Block list successfully stored');
+        return resolve();
+    }
+
+    insertOrReplace('offline', 'OfflineBlocks', chunk).then(
+        function() { 
+            console.debug('Block list successfully stored chunk ' + offset);
+            insertOfflineChunks(chunks, offset + 1, resolve, reject);
+        },
+        reject
+    );
+}
+
+
+// Routes inbound WebWorker message to the correct handler.
+// Replies include the original request plus added response info.
+function dispatchRequest(port, data) {
+
+    console.debug('Lovefield worker received', 
+        'action=' + (data.action || ''), 
+        'schema=' + (data.schema || ''), 
+        'table=' + (data.table || ''),
+        'field=' + (data.field || ''),
+        'value=' + (data.value || '')
+    );
+
+    function replySuccess(result) {
+        data.status = 'OK';
+        data.result = result;
+        port.postMessage(data);
+    }
+
+    function replyError(err) {
+        console.error('shared worker replying with error', err);
+        data.status = 'ERR';
+        data.error = err;
+        port.postMessage(data);
+    }
+
+    switch (data.action) {
+        case 'createSchema':
+            // Schema creation is synchronous and apparently throws
+            // no exceptions, at least until connect() is called.
+            createSchema(data.schema);
+            replySuccess();
+            break;
+
+        case 'connect':
+            connect(data.schema).then(replySuccess, replyError);
+            break;
+
+        case 'insertOrReplace':
+            insertOrReplace(data.schema, data.table, data.rows)
+                .then(replySuccess, replyError);
+            break;
+
+        case 'insert':
+            insert(data.schema, data.table, data.rows)
+                .then(replySuccess, replyError);
+            break;
+
+        case 'selectWhereEqual':
+            selectWhereEqual(data.schema, data.table, data.field, data.value)
+                .then(replySuccess, replyError);
+            break;
+
+        case 'selectWhereIn':
+            selectWhereIn(data.schema, data.table, data.field, data.value)
+                .then(replySuccess, replyError);
+            break;
+
+        case 'selectAll':
+            selectAll(data.schema, data.table).then(replySuccess, replyError);
+            break;
+
+        case 'deleteAll':
+            deleteAll(data.schema, data.table).then(replySuccess, replyError);
+            break;
+
+        case 'hasRows':
+            hasRows(data.schema, data.table).then(replySuccess, replyError);
+            break;
+
+        case 'populateBlockList':
+            populateBlockList(data.authtoken).then(replySuccess, replyError);
+            break;
+
+        default:
+            console.error('no such DB action ' + data.action);
+    }
+}
+
+onconnect = function(e) {
+    var port = e.ports[0];
+    port.addEventListener('message',
+        function(e) {dispatchRequest(port, e.data);});
+    port.start();
+}
+
+
+
diff --git a/Open-ILS/web/js/ui/default/staff/offline.js b/Open-ILS/web/js/ui/default/staff/offline.js
index fcf1a3d..6b0f41f 100644
--- a/Open-ILS/web/js/ui/default/staff/offline.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -17,8 +17,10 @@ function($routeProvider , $locationProvider , $compileProvider) {
      * Route resolvers allow us to run async commands
      * before the page controller is instantiated.
      */
-    var resolver = {delay : ['egCore', 
-        function(egCore) {
+    var resolver = {delay : ['egCore', 'egLovefield',
+        function(egCore, egLovefield) {
+            // the 'offline' schema is only active in the offline UI.
+            egLovefield.activeSchemas.push('offline');
             return egCore.startup.go();
         }
     ]};
@@ -251,8 +253,12 @@ function($routeProvider , $locationProvider , $compileProvider) {
 ])
 
 .controller('OfflineCtrl', 
-           ['$q','$scope','$window','$location','$rootScope','egCore','egLovefield','$routeParams','$timeout','$http','ngToast','egConfirmDialog','egUnloadPrompt',
-    function($q , $scope , $window , $location , $rootScope , egCore , egLovefield , $routeParams , $timeout , $http , ngToast , egConfirmDialog , egUnloadPrompt) {
+           ['$q','$scope','$window','$location','$rootScope','egCore',
+            'egLovefield','$routeParams','$timeout','$http','ngToast',
+            'egConfirmDialog','egUnloadPrompt','egProgressDialog',
+    function($q , $scope , $window , $location , $rootScope , egCore , 
+             egLovefield , $routeParams , $timeout , $http , ngToast , 
+             egConfirmDialog , egUnloadPrompt, egProgressDialog) {
 
         // Immediately redirect if we're really offline
         if (!$window.navigator.onLine) {
@@ -388,28 +394,16 @@ function($routeProvider , $locationProvider , $compileProvider) {
         });
 
         $scope.downloadBlockList = function () {
-            var url = '/standalone/list.txt?ses='
-                + egCore.auth.token()
-                + '&' + new Date().getTime();
-            return $http.get(url).then(
-                function (res) {
-                    if (res.data) {
-                        var lines = res.data.split('\n');
-                        egLovefield.destroyOfflineBlocks().then(function(){
-                            angular.forEach(lines, function (l) {
-                                var parts = l.split(' ');
-                                egLovefield.addOfflineBlock(parts[0], parts[1]);
-                            });
-                            return $q.when();
-                        }).then(function(){
-                            ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
-                        });
-                    }
-                },function(){
+            egProgressDialog.open();
+            egLovefield.populateBlockList().then(
+                function(){
+                    ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
+                },
+                function(){
                     ngToast.warning(egCore.strings.OFFLINE_BLOCKLIST_FAIL);
                     egCore.audio.play('warning.offline.blocklist_fail');
                 }
-            );
+            )['finally'](egProgressDialog.close);
         }
 
         $scope.createOfflineXactBlob = function () {
@@ -847,7 +841,7 @@ function($routeProvider , $locationProvider , $compileProvider) {
                         return egLovefield.reconstituteList('asva');
                     }).then(function() {
                         angular.forEach(egCore.env.asv.list, function (s) {
-                            s.questions( egCore.env.asva.list.filter( function (a) {
+                            s.questions( egCore.env.asvq.list.filter( function (q) {
                                 return q.survey().id == s.id();
                             }));
                         });
diff --git a/Open-ILS/web/js/ui/default/staff/services/lovefield.js b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
index d930199..b78d316 100644
--- a/Open-ILS/web/js/ui/default/staff/services/lovefield.js
+++ b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
@@ -1,36 +1,3 @@
-var osb = lf.schema.create('offline', 2);
-
-osb.createTable('Object').
-    addColumn('type', lf.Type.STRING).          // class hint
-    addColumn('id', lf.Type.STRING).           // obj id
-    addColumn('object', lf.Type.OBJECT).
-    addPrimaryKey(['type','id']);
-
-osb.createTable('CacheDate').
-    addColumn('type', lf.Type.STRING).          // class hint
-    addColumn('cachedate', lf.Type.DATE_TIME).  // when was it last updated
-    addPrimaryKey(['type']);
-
-osb.createTable('Setting').
-    addColumn('name', lf.Type.STRING).
-    addColumn('value', lf.Type.STRING).
-    addPrimaryKey(['name']);
-
-osb.createTable('StatCat').
-    addColumn('id', lf.Type.INTEGER).
-    addColumn('value', lf.Type.OBJECT).
-    addPrimaryKey(['id']);
-
-osb.createTable('OfflineXact').
-    addColumn('seq', lf.Type.INTEGER).
-    addColumn('value', lf.Type.OBJECT).
-    addPrimaryKey(['seq'], true);
-
-osb.createTable('OfflineBlocks').
-    addColumn('barcode', lf.Type.STRING).
-    addColumn('reason', lf.Type.STRING).
-    addPrimaryKey(['barcode']);
-
 /**
  * Core Service - egLovefield
  *
@@ -42,321 +9,349 @@ angular.module('egCoreMod')
 .factory('egLovefield', ['$q','$rootScope','egCore','$timeout', 
                  function($q , $rootScope , egCore , $timeout) { 
 
-    var service = {};
+    var service = {
+        autoId: 0, // each request gets a unique id.
+        cannotConnect: false,
+        pendingRequests: [],
+        activeSchemas: ['cache'], // add 'offline' in the offline UI
+        schemasInProgress: {},
+        connectedSchemas: [],
+        // TODO: relative path would be more portable
+        workerUrl: '/js/ui/default/staff/offline-db-worker.js'
+    };
 
-    function connectOrGo() {
+    service.connectToWorker = function() {
+        if (service.worker) return;
 
-        if (lf.offlineDB) { // offline DB connected
-            return $q.when();
+        try {
+            // relative path would be better...
+            service.worker = new SharedWorker(service.workerUrl);
+        } catch (E) {
+            console.error('SharedWorker() not supported', E);
+            service.cannotConnect = true;
+            return;
         }
 
-        if (service.cannotConnect) { // connection will never happen
-            return $q.reject();
+        service.worker.onerror = function(err) {
+            console.error('Error loading shared worker', err);
+            service.cannotConnect = true;
         }
 
-        if (service.connectPromise) { // connection in progress
-            return service.connectPromise;
-        }
+        // List for responses and resolve the matching pending request.
+        service.worker.port.addEventListener('message', function(evt) {
+            var response = evt.data;
+            var reqId = response.id;
+            var req = service.pendingRequests.filter(
+                function(r) { return r.id === reqId})[0];
 
-        // start a new connection attempt
-        
-        var deferred = $q.defer();
+            if (!req) {
+                console.error('Recieved response for unknown request ' + reqId);
+                return;
+            }
 
-        console.debug('attempting offline DB connection');
-        try {
-            osb.connect().then(
-                function(db) {
-                    console.debug('successfully connected to offline DB');
-                    service.connectPromise = null;
-                    lf.offlineDB = db;
-                    deferred.resolve();
-                },
-                function(err) {
-                    // assumes that a single connection failure means
-                    // a connection will never succeed.
-                    service.cannotConnect = true;
-                    console.error('Cannot connect to offline DB: ' + err);
-                }
-            );
-        } catch (e) {
-            // .connect() will throw an error if it detects that a connection
-            // attempt is already in progress; this can happen with PhantomJS
-            console.error('Cannot connect to offline DB: ' + e);
-            service.cannotConnect = true;
-        }
+            if (response.status === 'OK') {
+                req.deferred.resolve(response.result);
+            } else {
+                console.error('worker request failed with ' + response.error);
+                req.deferred.reject(response.error);
+            }
+        });
 
-        service.connectPromise = deferred.promise;
-        return service.connectPromise;
+        service.worker.port.start();
     }
 
-    service.isCacheGood = function (type) {
+    service.connectToSchemas = function() {
 
-        return connectOrGo().then(function() {
-            var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
+        if (service.cannotConnect) { 
+            // This can happen in certain environments
+            return $q.reject();
+        }
+        
+        service.connectToWorker(); // no-op if already connected
 
-            return lf.offlineDB.
-                select(cacheDate.cachedate).
-                from(cacheDate).
-                where(cacheDate.type.eq(type)).
-                exec().then(function(results) {
-                    if (results.length == 0) {
-                        return $q.when(false);
-                    }
+        var promises = [];
 
-                    var now = new Date();
-    
-                    // hard-coded 1 day offline cache timeout
-                    return $q.when((now.getTime() - results[0]['cachedate'].getTime()) <= 86400000);
-                })
+        service.activeSchemas.forEach(function(schema) {
+            promises.push(service.connectToSchema(schema));
         });
+
+        return $q.all(promises).then(
+            function() {},
+            function() {service.cannotConnect = true}
+        );
+    }
+
+    // Connects if necessary to the active schemas then relays the request.
+    service.request = function(args) {
+        return service.connectToSchemas().then(
+            function() {
+                return service.relayRequest(args);
+            }
+        );
+    }
+
+    // Send a request to the web worker and register the request for
+    // future resolution.
+    // Store the request ID in the request arguments, so it's included
+    // in the response, and in the pendingRequests list for linking.
+    service.relayRequest = function(args) {
+        var deferred = $q.defer();
+        var reqId = service.autoId++;
+        args.id = reqId;
+        service.pendingRequests.push({id : reqId, deferred: deferred});
+        service.worker.port.postMessage(args);
+        return deferred.promise;
+    }
+
+    // Create and connect to the give schema
+    service.connectToSchema = function(schema) {
+
+        if (service.connectedSchemas.includes(schema)) {
+            // already connected
+            return $q.when();
+        }
+
+        if (service.schemasInProgress[schema]) {
+            return service.schemasInProgress[schema];
+        }
+
+        var deferred = $q.defer();
+
+        service.relayRequest(
+            {schema: schema, action: 'createSchema'}) 
+        .then(
+            function() {
+                return service.relayRequest(
+                    {schema: schema, action: 'connect'});
+            },
+            deferred.reject
+        ).then(
+            function() { 
+                service.connectedSchemas.push(schema); 
+                delete service.schemasInProgress[schema];
+                deferred.resolve();
+            },
+            deferred.reject
+        );
+
+        return service.schemasInProgress[schema] = deferred.promise;
+    }
+
+    service.isCacheGood = function (type) {
+        return service.request({
+            schema: 'cache',
+            table: 'CacheDate',
+            action: 'selectWhereEqual',
+            field: 'type',
+            value: type
+        }).then(
+            function(result) {
+                var row = result[0];
+                if (!row) { return false; }
+                // hard-coded 1 day offline cache timeout
+                return (new Date().getTime() - row.cachedate.getTime()) <= 86400000;
+            }
+        );
     }
 
     service.destroyPendingOfflineXacts = function () {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineXact');
-            return lf.offlineDB.
-                delete().
-                from(table).
-                exec();
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineXact',
+            action: 'deleteAll'
         });
     }
 
     service.havePendingOfflineXacts = function () {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineXact');
-            return lf.offlineDB.
-                select(table.reason).
-                from(table).
-                exec().
-                then(function(list) {
-                    return $q.when(Boolean(list.length > 0))
-                });
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineXact',
+            action: 'hasRows'
         });
     }
 
     service.retrievePendingOfflineXacts = function () {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineXact');
-            return lf.offlineDB.
-                select(table.value).
-                from(table).
-                exec().
-                then(function(list) {
-                    return $q.when(list.map(function(x) { return x.value }))
-                });
-        });
-    }
-
-    service.destroyOfflineBlocks = function () {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
-            return $q.when(
-                lf.offlineDB.
-                    delete().
-                    from(table).
-                    exec()
-            );
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineXact',
+            action: 'selectAll'
+        }).then(function(resp) {
+            return resp.map(function(x) { return x.value });
         });
     }
 
-    service.addOfflineBlock = function (barcode, reason) {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
-            return $q.when(
-                lf.offlineDB.
-                    insertOrReplace().
-                    into(table).
-                    values([ table.createRow({ barcode : barcode, reason : reason }) ]).
-                    exec()
-            );
+    service.populateBlockList = function() {
+        return service.request({
+            action: 'populateBlockList',
+            authtoken: egCore.auth.token()
         });
     }
 
     // Returns a promise with true for blocked, false for not blocked
     service.testOfflineBlock = function (barcode) {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
-            return lf.offlineDB.
-                select(table.reason).
-                from(table).
-                where(table.barcode.eq(barcode)).
-                exec().then(function(list) {
-                    if(list.length > 0) return $q.when(list[0].reason);
-                    return $q.when(null);
-                });
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineBlocks',
+            action: 'selectWhereEqual',
+            field: 'barcode',
+            value: barcode
+        }).then(function(resp) {
+            if (resp.length === 0) return null;
+            return resp[0].reason;
         });
     }
 
     service.addOfflineXact = function (obj) {
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('OfflineXact');
-            return $q.when(
-                lf.offlineDB.
-                    insertOrReplace().
-                    into(table).
-                    values([ table.createRow({ value : obj }) ]).
-                    exec()
-            );
+        return service.request({
+            schema: 'offline',
+            table: 'OfflineXact',
+            action: 'insertOrReplace',
+            rows: [{value: obj}]
         });
     }
 
     service.setStatCatsCache = function (statcats) {
-        if (lf.isOffline) return $q.when();
+        if (lf.isOffline || !statcats || statcats.length === 0) 
+            return $q.when();
 
-        return connectOrGo().then(function() {
-            var table = lf.offlineDB.getSchema().table('StatCat');
-            var rlist = [];
+        var rows = statcats.map(function(cat) {
+            return {id: cat.id(), value: egCore.idl.toHash(cat)}
+        });
 
-            angular.forEach(statcats, function (val) {
-                rlist.push(table.createRow({
-                    id    : val.id(),
-                    value : egCore.idl.toHash(val)
-                }));
-            });
-            return lf.offlineDB.
-                insertOrReplace().
-                into(table).
-                values(rlist).
-                exec();
+        return service.request({
+            schema: 'cache',
+            table: 'StatCat',
+            action: 'insertOrReplace',
+            rows: rows
         });
     }
 
     service.getStatCatsCache = function () {
-        return connectOrGo().then(function() {
 
-            var table = lf.offlineDB.getSchema().table('StatCat');
+        return service.request({
+            schema: 'cache',
+            table: 'StatCat',
+            action: 'selectAll'
+        }).then(function(list) {
             var result = [];
-            return lf.offlineDB.
-                select(table.value).
-                from(table).
-                exec().then(function(list) {
-                    angular.forEach(list, function (s) {
-                        var sc = egCore.idl.fromHash('actsc', s.value);
-    
-                        if (angular.isArray(sc.default_entries())) {
-                            sc.default_entries(
-                                sc.default_entries().map( function (k) {
-                                    return egCore.idl.fromHash('actsced', k);
-                                })
-                            );
-                        }
-    
-                        if (angular.isArray(sc.entries())) {
-                            sc.entries(
-                                sc.entries().map( function (k) {
-                                    return egCore.idl.fromHash('actsce', k);
-                                })
-                            );
-                        }
-    
-                        result.push(sc);
-                    });
-                    return $q.when(result);
-                });
-    
+            list.forEach(function(s) {
+                var sc = egCore.idl.fromHash('actsc', s.value);
+
+                if (Array.isArray(sc.default_entries())) {
+                    sc.default_entries(
+                        sc.default_entries().map( function (k) {
+                            return egCore.idl.fromHash('actsced', k);
+                        })
+                    );
+                }
+
+                if (Array.isArray(sc.entries())) {
+                    sc.entries(
+                        sc.entries().map( function (k) {
+                            return egCore.idl.fromHash('actsce', k);
+                        })
+                    );
+                }
+
+                result.push(sc);
+            });
+
+            return result;
         });
     }
 
     service.setSettingsCache = function (settings) {
         if (lf.isOffline) return $q.when();
 
-        return connectOrGo().then(function() {
-
-            var table = lf.offlineDB.getSchema().table('Setting');
-            var rlist = [];
-
-            angular.forEach(settings, function (val, key) {
-                rlist.push(
-                    table.createRow({
-                        name  : key,
-                        value : JSON.stringify(val)
-                    })
-                );
-            });
+        var rows = [];
+        angular.forEach(settings, function (val, key) {
+            rows.push({name  : key, value : JSON.stringify(val)});
+        });
 
-            return lf.offlineDB.
-                insertOrReplace().
-                into(table).
-                values(rlist).
-                exec();
+        return service.request({
+            schema: 'cache',
+            table: 'Setting',
+            action: 'insertOrReplace',
+            rows: rows
         });
     }
 
     service.getSettingsCache = function (settings) {
-        return connectOrGo().then(function() {
 
-            var table = lf.offlineDB.getSchema().table('Setting');
+        var promise;
+
+        if (settings && settings.length) {
+            promise = service.request({
+                schema: 'cache',
+                table: 'Setting',
+                action: 'selectWhereIn',
+                field: 'name',
+                value: settings
+            });
+        } else {
+            promise = service.request({
+                schema: 'cache',
+                table: 'Setting',
+                action: 'selectAll'
+            });
+        }
 
-            var search_pred = table.name.isNotNull();
-            if (settings && settings.length) {
-                search_pred = table.name.in(settings);
+        return promise.then(
+            function(resp) {
+                resp.forEach(function(s) { s.value = JSON.parse(s.value); });
+                return resp;
             }
-                
-            return lf.offlineDB.
-                select(table.name, table.value).
-                from(table).
-                where(search_pred).
-                exec().then(function(list) {
-                    angular.forEach(list, function (s) {
-                        s.value = JSON.parse(s.value)
-                    });
-                    return $q.when(list);
-                });
-        });
+        );
     }
 
     service.setListInOfflineCache = function (type, list) {
         if (lf.isOffline) return $q.when();
 
-        return connectOrGo().then(function() {
+        return service.isCacheGood(type).then(function(good) {
+            if (good) { return };  // already cached
 
-            service.isCacheGood(type).then(function(good) {
-                if (!good) {
-                    var object = lf.offlineDB.getSchema().table('Object');
-                    var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
-                    var pkey = egCore.idl.classes[type].pkey;
-        
-                    angular.forEach(list, function(item) {
-                        var row = object.createRow({
-                            type    : type,
-                            id      : '' + item[pkey](),
-                            object  : egCore.idl.toHash(item)
-                        });
-                        lf.offlineDB.insertOrReplace().into(object).values([row]).exec();
-                    });
-        
-                    var row = cacheDate.createRow({
-                        type      : type,
-                        cachedate : new Date()
-                    });
-        
-                    console.log('egLovefield saving ' + type + ' list');
-                    lf.offlineDB.insertOrReplace().into(cacheDate).values([row]).exec();
-                }
-            })
+            var pkey = egCore.idl.classes[type].pkey;
+            var rows = Object.values(list).map(function(item) {
+                return {
+                    type: type, 
+                    id: '' + item[pkey](), 
+                    object: egCore.idl.toHash(item)
+                };
+            });
+
+            return service.request({
+                schema: 'cache',
+                table: 'Object',
+                action: 'insertOrReplace',
+                rows: rows
+            }).then(function(resp) {
+                return service.request({
+                    schema: 'cache',
+                    table: 'CacheDate',
+                    action: 'insertOrReplace',
+                    rows: [{type: type, cachedate : new Date()}]
+                });
+            });
         });
     }
 
     service.getListFromOfflineCache = function(type) {
-        return connectOrGo().then(function() {
-
-            var object = lf.offlineDB.getSchema().table('Object');
-
-            return lf.offlineDB.
-                select(object.object).
-                from(object).
-                where(object.type.eq(type)).
-                exec().then(function(results) {
-                    return $q.when(results.map(function(item) {
-                        return egCore.idl.fromHash(type,item['object'])
-                    }));
-                });
+        return service.request({
+            schema: 'cache',
+            table: 'Object',
+            action: 'selectWhereEqual',
+            field: 'type',
+            value: type
+        }).then(function(resp) {
+            return resp.map(function(item) {
+                return egCore.idl.fromHash(type,item['object']);
+            });
         });
     }
 
     service.reconstituteList = function(type) {
         if (lf.isOffline) {
-            console.log('egLovefield reading ' + type + ' list');
+            console.debug('egLovefield reading ' + type + ' list');
             return service.getListFromOfflineCache(type).then(function (list) {
                 egCore.env.absorbList(list, type, true)
                 return $q.when(true);
@@ -367,7 +362,7 @@ angular.module('egCoreMod')
 
     service.reconstituteTree = function(type) {
         if (lf.isOffline) {
-            console.log('egLovefield reading ' + type + ' tree');
+            console.debug('egLovefield reading ' + type + ' tree');
 
             var pkey = egCore.idl.classes[type].pkey;
             var parent_field = 'parent';

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/src/templates/staff/offline-interface.tt2 |    1 +
 Open-ILS/src/templates/staff/t_login.tt2           |   11 +-
 .../web/js/ui/default/staff/offline-db-worker.js   |  413 ++++++++++++++
 Open-ILS/web/js/ui/default/staff/offline.js        |   47 +-
 .../web/js/ui/default/staff/services/lovefield.js  |  566 ++++++++++----------
 5 files changed, 740 insertions(+), 298 deletions(-)
 create mode 100644 Open-ILS/web/js/ui/default/staff/offline-db-worker.js


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list