From 071a5492e33bda4e51abe43ff3fe2f327c402153 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 8 Jun 2018 13:08:18 -0400 Subject: [PATCH] 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 Signed-off-by: Jeff Davis Signed-off-by: Kathy Lussier --- Open-ILS/src/templates/staff/base_js.tt2 | 8 + .../web/js/ui/default/staff/offline-db-worker.js | 394 +++++++++++++++ Open-ILS/web/js/ui/default/staff/offline.js | 42 +- .../web/js/ui/default/staff/services/lovefield.js | 535 ++++++++++----------- Open-ILS/web/js/ui/default/staff/webpack.config.js | 6 +- 5 files changed, 689 insertions(+), 296 deletions(-) create mode 100644 Open-ILS/web/js/ui/default/staff/offline-db-worker.js diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2 index 51d9c42dc0..98650a7cd3 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({ + + + 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 0000000000..0107dfd581 --- /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 947aa42858..0e990a1cc2 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 d0cd9c12ee..b78d3164ed 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 32e97b115d..da66461b4a 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' ]; -- 2.11.0