[open-ils-commits] [GIT] Evergreen ILS branch master updated. 457d7f65aeba76f7deb4581e8f3e1930aad4f592

Evergreen Git git at git.evergreen-ils.org
Wed Aug 8 10:32:28 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, master has been updated
       via  457d7f65aeba76f7deb4581e8f3e1930aad4f592 (commit)
       via  3256fd462cf2a7a943638b8122ec7f3e818a9419 (commit)
       via  071a5492e33bda4e51abe43ff3fe2f327c402153 (commit)
      from  beee20c5b0a63bf8248a630686f2fb262d4789ac (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 457d7f65aeba76f7deb4581e8f3e1930aad4f592
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 16424ce..fafc74e 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 0e990a1..7ab361f 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 3256fd462cf2a7a943638b8122ec7f3e818a9419
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 071a5492e33bda4e51abe43ff3fe2f327c402153
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/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 51d9c42..98650a7 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -40,6 +40,7 @@ UpUp.start({
     '[% ctx.media_prefix %]/js/dojo/opensrf/md5.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-with-locales.min.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js',
     '[% ctx.media_prefix %]/js/ui/default/common/build/js/jquery.min.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/vendor.bundle.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/fonts/glyphicons-halflings-regular.woff',
@@ -83,6 +84,13 @@ UpUp.start({
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js"></script>
 
 <!--
+  Load the lovefield libs as a standaline file so both the main
+  application and the offline shared worker can reference (and cache)
+  the same file
+-->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js"></script>
+
+<!--
   Load iframeResize via script tag
   https://bugs.launchpad.net/evergreen/+bug/1753008
 -->
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 947aa42..0e990a1 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 d0cd9c1..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';
diff --git a/Open-ILS/web/js/ui/default/staff/webpack.config.js b/Open-ILS/web/js/ui/default/staff/webpack.config.js
index 32e97b1..da66461 100644
--- a/Open-ILS/web/js/ui/default/staff/webpack.config.js
+++ b/Open-ILS/web/js/ui/default/staff/webpack.config.js
@@ -44,7 +44,10 @@ const JS_FILES = [
   './node_modules/moment/min/moment-with-locales.min.js',
   './node_modules/moment-timezone/builds/moment-timezone-with-data.min.js',
   './node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
-  './node_modules/iframe-resizer/js/iframeResizer.min.js'
+  './node_modules/iframe-resizer/js/iframeResizer.min.js',
+  // lovefield is loaded from multiple locations.  Make it stand-alone
+  // so we only need a single copy.
+  './node_modules/lovefield/dist/lovefield.min.js'
 ]
 
 
@@ -102,7 +105,6 @@ const vendorJsFiles = [
   'angular-tree-control',
   'angular-tree-control/context-menu.js',
   'angular-order-object-by',
-  'lovefield',
   'angular-tablesort'
 ];
 

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

Summary of changes:
 Open-ILS/src/templates/staff/base_js.tt2           |    8 +
 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  |  563 ++++++++++----------
 Open-ILS/web/js/ui/default/staff/webpack.config.js |    6 +-
 7 files changed, 749 insertions(+), 300 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