]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/offline-db-worker.js
LP#1768947 Offline xact presence is cached; show date
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / offline-db-worker.js
1 importScripts('/js/ui/default/staff/build/js/lovefield.min.js');
2
3 // Collection of schema tracking objects.
4 var schemas = {};
5
6 // Create the DB schema / tables
7 // synchronous
8 function createSchema(schemaName) {
9     if (schemas[schemaName]) return;
10
11     var meta = lf.schema.create(schemaName, 2);
12     schemas[schemaName] = {name: schemaName, meta: meta};
13
14     switch (schemaName) {
15         case 'cache':
16             createCacheTables(meta);
17             break;
18         case 'offline':
19             createOfflineTables(meta);
20             break;
21         default:
22             console.error('No schema definition for ' + schemaName);
23     }
24 }
25
26 // Offline cache tables are globally available in the staff client
27 // for on-demand caching.
28 function createCacheTables(meta) {
29
30     meta.createTable('Setting').
31         addColumn('name', lf.Type.STRING).
32         addColumn('value', lf.Type.STRING).
33         addPrimaryKey(['name']);
34
35     meta.createTable('Object').
36         addColumn('type', lf.Type.STRING).         // class hint
37         addColumn('id', lf.Type.STRING).           // obj id
38         addColumn('object', lf.Type.OBJECT).
39         addPrimaryKey(['type','id']);
40
41     meta.createTable('CacheDate').
42         addColumn('type', lf.Type.STRING).          // class hint
43         addColumn('cachedate', lf.Type.DATE_TIME).  // when was it last updated
44         addPrimaryKey(['type']);
45
46     meta.createTable('StatCat').
47         addColumn('id', lf.Type.INTEGER).
48         addColumn('value', lf.Type.OBJECT).
49         addPrimaryKey(['id']);
50 }
51
52 // Offline transaction and block list tables.  These can be bulky and
53 // are only used in the offline UI.
54 function createOfflineTables(meta) {
55
56     meta.createTable('OfflineXact').
57         addColumn('seq', lf.Type.INTEGER).
58         addColumn('value', lf.Type.OBJECT).
59         addPrimaryKey(['seq'], true);
60
61     meta.createTable('OfflineBlocks').
62         addColumn('barcode', lf.Type.STRING).
63         addColumn('reason', lf.Type.STRING).
64         addPrimaryKey(['barcode']);
65 }
66
67 // Connect to the database for a given schema
68 function connect(schemaName) {
69
70     var schema = schemas[schemaName];
71     if (!schema) {
72         return Promise.reject('createSchema(' +
73             schemaName + ') call required');
74     }
75
76     if (schema.db) { // already connected.
77         return Promise.resolve();
78     }
79
80     return new Promise(function(resolve, reject) {
81         try {
82             schema.meta.connect().then(
83                 function(db) {
84                     schema.db = db;
85                     resolve();
86                 },
87                 function(err) {
88                     reject('Error connecting to schema ' +
89                         schemaName + ' : ' + err);
90                 }
91             );
92         } catch (E) {
93             reject('Error connecting to schema ' + schemaName + ' : ' + E);
94         }
95     });
96 }
97
98 function getTableInfo(schemaName, tableName) {
99     var schema = schemas[schemaName];
100     var info = {};
101
102     if (!schema) {
103         info.error = 'createSchema(' + schemaName + ') call required';
104
105     } else if (!schema.db) {
106         info.error = 'connect(' + schemaName + ') call required';
107
108     } else {
109         info.schema = schema;
110         info.table = schema.meta.getSchema().table(tableName);
111
112         if (!info.table) {
113             info.error = 'no such table ' + tableName;
114         }
115     }
116
117     return info;
118 }
119
120 // Returns a promise resolved with true on success
121 // Note insert .exec() returns rows, but that can get bulky on large
122 // inserts, hence the boolean return;
123 function insertOrReplace(schemaName, tableName, objects) {
124
125     var info = getTableInfo(schemaName, tableName);
126     if (info.error) { return Promise.reject(info.error); }
127
128     var rows = objects.map(function(r) { return info.table.createRow(r) });
129     return info.schema.db.insertOrReplace().into(info.table)
130         .values(rows).exec().then(function() { return true; });
131 }
132
133 // Returns a promise resolved with true on success
134 // Note insert .exec() returns rows, but that can get bulky on large
135 // inserts, hence the boolean return;
136 function insert(schemaName, tableName, objects) {
137
138     var info = getTableInfo(schemaName, tableName);
139     if (info.error) { return Promise.reject(info.error); }
140
141     var rows = objects.map(function(r) { return info.table.createRow(r) });
142     return info.schema.db.insert().into(info.table)
143         .values(rows).exec().then(function() { return true; });
144 }
145
146 // Returns rows where the selected field equals the provided value.
147 function selectWhereEqual(schemaName, tableName, field, value) {
148
149     var info = getTableInfo(schemaName, tableName);
150     if (info.error) { return Promise.reject(info.error); }
151
152     return info.schema.db.select().from(info.table)
153         .where(info.table[field].eq(value)).exec();
154 }
155
156 // Returns rows where the selected field equals the provided value.
157 function selectWhereIn(schemaName, tableName, field, value) {
158
159     var info = getTableInfo(schemaName, tableName);
160     if (info.error) { return Promise.reject(info.error); }
161
162     return info.schema.db.select().from(info.table)
163         .where(info.table[field].in(value)).exec();
164 }
165
166 // Returns all rows in the selected table
167 function selectAll(schemaName, tableName) {
168
169     var info = getTableInfo(schemaName, tableName);
170     if (info.error) { return Promise.reject(info.error); }
171
172     return info.schema.db.select().from(info.table).exec();
173 }
174
175 // Deletes all rows in the selected table.
176 function deleteAll(schemaName, tableName) {
177
178     var info = getTableInfo(schemaName, tableName);
179     if (info.error) { return Promise.reject(info.error); }
180
181     return info.schema.db.delete().from(info.table).exec();
182 }
183
184 // Delete rows from selected table where field equals value
185 function deleteWhereEqual(schemaName, tableName, field, value) {
186     var info = getTableInfo(schemaName, tableName);
187     if (info.error) { return Promise.reject(info.error); }
188
189     return info.schema.db.delete().from(info.table)
190         .where(info.table[field].eq(value)).exec();
191 }
192
193 // Resolves to true if the selected table contains any rows.
194 function hasRows(schemaName, tableName) {
195
196     var info = getTableInfo(schemaName, tableName);
197     if (info.error) { return Promise.reject(info.error); }
198
199     return info.schema.db.select().from(info.table).limit(1).exec()
200         .then(function(rows) { return rows.length > 0 });
201 }
202
203
204 // Prevent parallel block list building calls, since it does a lot.
205 var buildingBlockList = false;
206
207 // Fetches the offline block list and rebuilds the offline blocks
208 // table from the new data.
209 function populateBlockList(authtoken) {
210     if (buildingBlockList) return;
211     buildingBlockList = true;
212
213     var url = '/standalone/list.txt?ses=' + 
214         authtoken + '&' + new Date().getTime();
215
216     console.debug('Fetching offline block list from: ' + url);
217
218     return new Promise(function(resolve, reject) {
219
220         var xhttp = new XMLHttpRequest();
221         xhttp.onreadystatechange = function() {
222             if (this.readyState === 4) {
223                 if (this.status === 200) {
224                     var blocks = xhttp.responseText;
225                     var lines = blocks.split('\n');
226                     insertOfflineBlocks(lines).then(
227                         function() {
228                             buildingBlockList = false;
229                             resolve();
230                         },
231                         function(e) {
232                             buildingBlockList = false;
233                             reject(e);
234                         }
235                     );
236                 } else {
237                     reject('Error fetching offline block list');
238                 }
239             }
240         };
241
242         xhttp.open('GET', url, true);
243         xhttp.send();
244     });
245 }
246
247 // Rebuild the offline blocks table with the provided blocks, one per line.
248 function insertOfflineBlocks(lines) {
249     console.debug('Fetched ' + lines.length + ' blocks');
250
251     // Clear the table first
252     return deleteAll('offline', 'OfflineBlocks').then(
253         function() { 
254
255             console.debug('Cleared existing offline blocks');
256
257             // Create a single batch of rows for insertion.
258             var chunks = [];
259             var currentChunk = [];
260             var chunkSize = 10000;
261             var seen = {bc: {}}; // for easier delete
262
263             chunks.push(currentChunk);
264             lines.forEach(function(line) {
265                 // slice/substring instead of split(' ') to handle barcodes
266                 // with trailing spaces.
267                 var barcode = line.slice(0, -2);
268                 var reason = line.substring(line.length - 1);
269                 
270                 // Trim duplicate barcodes, since only one version of each 
271                 // block per barcode is kept in the offline block list
272                 if (seen.bc[barcode]) return;
273                 seen.bc[barcode] = true;
274
275                 if (currentChunk.length >= chunkSize) {
276                     currentChunk = [];
277                     chunks.push(currentChunk);
278                 }
279
280                 currentChunk.push({barcode: barcode, reason: reason});
281             });
282
283             delete seen.bc; // allow this hunk to be reclaimed
284
285             console.debug('offline data broken into ' + 
286                 chunks.length + ' chunks of size ' + chunkSize);
287
288             return new Promise(function(resolve, reject) {
289                 insertOfflineChunks(chunks, 0, resolve, reject);
290             });
291         }, 
292
293         function(err) {
294             console.error('Error clearing offline table: ' + err);
295             return Promise.reject(err);
296         }
297     );
298 }
299
300 function insertOfflineChunks(chunks, offset, resolve, reject) {
301     var chunk = chunks[offset];
302     if (!chunk || chunk.length === 0) {
303         console.debug('Block list successfully stored');
304         return resolve();
305     }
306
307     insertOrReplace('offline', 'OfflineBlocks', chunk).then(
308         function() { 
309             console.debug('Block list successfully stored chunk ' + offset);
310             insertOfflineChunks(chunks, offset + 1, resolve, reject);
311         },
312         reject
313     );
314 }
315
316
317 // Routes inbound WebWorker message to the correct handler.
318 // Replies include the original request plus added response info.
319 function dispatchRequest(port, data) {
320
321     console.debug('Lovefield worker received', 
322         'action=' + (data.action || ''), 
323         'schema=' + (data.schema || ''), 
324         'table=' + (data.table || ''),
325         'field=' + (data.field || ''),
326         'value=' + (data.value || '')
327     );
328
329     function replySuccess(result) {
330         data.status = 'OK';
331         data.result = result;
332         port.postMessage(data);
333     }
334
335     function replyError(err) {
336         console.error('shared worker replying with error', err);
337         data.status = 'ERR';
338         data.error = err;
339         port.postMessage(data);
340     }
341
342     switch (data.action) {
343         case 'createSchema':
344             // Schema creation is synchronous and apparently throws
345             // no exceptions, at least until connect() is called.
346             createSchema(data.schema);
347             replySuccess();
348             break;
349
350         case 'connect':
351             connect(data.schema).then(replySuccess, replyError);
352             break;
353
354         case 'insertOrReplace':
355             insertOrReplace(data.schema, data.table, data.rows)
356                 .then(replySuccess, replyError);
357             break;
358
359         case 'insert':
360             insert(data.schema, data.table, data.rows)
361                 .then(replySuccess, replyError);
362             break;
363
364         case 'selectWhereEqual':
365             selectWhereEqual(data.schema, data.table, data.field, data.value)
366                 .then(replySuccess, replyError);
367             break;
368
369         case 'selectWhereIn':
370             selectWhereIn(data.schema, data.table, data.field, data.value)
371                 .then(replySuccess, replyError);
372             break;
373
374         case 'selectAll':
375             selectAll(data.schema, data.table).then(replySuccess, replyError);
376             break;
377
378         case 'deleteAll':
379             deleteAll(data.schema, data.table).then(replySuccess, replyError);
380             break;
381
382         case 'deleteWhereEqual':
383             deleteWhereEqual(data.schema, data.table, data.field, data.value)
384                 .then(replySuccess, replyError);
385             break;
386
387         case 'hasRows':
388             hasRows(data.schema, data.table).then(replySuccess, replyError);
389             break;
390
391         case 'populateBlockList':
392             populateBlockList(data.authtoken).then(replySuccess, replyError);
393             break;
394
395         default:
396             console.error('no such DB action ' + data.action);
397     }
398 }
399
400 onconnect = function(e) {
401     var port = e.ports[0];
402     port.addEventListener('message',
403         function(e) {dispatchRequest(port, e.data);});
404     port.start();
405 }
406
407
408