LP#1768947 Disable offline download button, improve logging
[working/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
211     if (buildingBlockList) {
212         return Promise.reject('Block list download already in progress');
213     }
214
215     buildingBlockList = true;
216
217     var url = '/standalone/list.txt?ses=' + 
218         authtoken + '&' + new Date().getTime();
219
220     console.debug('Fetching offline block list from: ' + url);
221
222     return new Promise(function(resolve, reject) {
223
224         var xhttp = new XMLHttpRequest();
225         xhttp.onreadystatechange = function() {
226             if (this.readyState === 4) {
227                 if (this.status === 200) {
228                     var blocks = xhttp.responseText;
229                     var lines = blocks.split('\n');
230                     insertOfflineBlocks(lines).then(
231                         function() {
232                             buildingBlockList = false;
233                             resolve();
234                         },
235                         function(e) {
236                             buildingBlockList = false;
237                             reject(e);
238                         }
239                     );
240                 } else {
241                     buildingBlockList = false;
242                     reject('Error fetching offline block list');
243                 }
244             }
245         };
246
247         xhttp.open('GET', url, true);
248         xhttp.send();
249     });
250 }
251
252 // Rebuild the offline blocks table with the provided blocks, one per line.
253 function insertOfflineBlocks(lines) {
254     console.debug('Fetched ' + lines.length + ' blocks');
255
256     // Clear the table first
257     return deleteAll('offline', 'OfflineBlocks').then(
258         function() { 
259
260             console.debug('Cleared existing offline blocks');
261
262             // Create a single batch of rows for insertion.
263             var chunks = [];
264             var currentChunk = [];
265             var chunkSize = 10000;
266             var seen = {bc: {}}; // for easier delete
267
268             chunks.push(currentChunk);
269             lines.forEach(function(line) {
270                 // slice/substring instead of split(' ') to handle barcodes
271                 // with trailing spaces.
272                 var barcode = line.slice(0, -2);
273                 var reason = line.substring(line.length - 1);
274                 
275                 // Trim duplicate barcodes, since only one version of each 
276                 // block per barcode is kept in the offline block list
277                 if (seen.bc[barcode]) return;
278                 seen.bc[barcode] = true;
279
280                 if (currentChunk.length >= chunkSize) {
281                     currentChunk = [];
282                     chunks.push(currentChunk);
283                 }
284
285                 currentChunk.push({barcode: barcode, reason: reason});
286             });
287
288             delete seen.bc; // allow this hunk to be reclaimed
289
290             console.debug('offline data broken into ' + 
291                 chunks.length + ' chunks of size ' + chunkSize);
292
293             return new Promise(function(resolve, reject) {
294                 insertOfflineChunks(chunks, 0, resolve, reject);
295             });
296         }, 
297
298         function(err) {
299             console.error('Error clearing offline table: ' + err);
300             return Promise.reject(err);
301         }
302     );
303 }
304
305 function insertOfflineChunks(chunks, offset, resolve, reject) {
306     var chunk = chunks[offset];
307     if (!chunk || chunk.length === 0) {
308         console.debug('Block list store completed');
309         return resolve();
310     }
311
312     insertOrReplace('offline', 'OfflineBlocks', chunk).then(
313         function() { 
314             console.debug('Block list successfully stored chunk ' + offset);
315             insertOfflineChunks(chunks, offset + 1, resolve, reject);
316         },
317         reject
318     );
319 }
320
321
322 // Routes inbound WebWorker message to the correct handler.
323 // Replies include the original request plus added response info.
324 function dispatchRequest(port, data) {
325
326     console.debug('Lovefield worker received', 
327         'action=' + (data.action || ''), 
328         'schema=' + (data.schema || ''), 
329         'table=' + (data.table || ''),
330         'field=' + (data.field || ''),
331         'value=' + (data.value || '')
332     );
333
334     function replySuccess(result) {
335         data.status = 'OK';
336         data.result = result;
337         port.postMessage(data);
338     }
339
340     function replyError(err) {
341         console.error('shared worker replying with error', err);
342         data.status = 'ERR';
343         data.error = err;
344         port.postMessage(data);
345     }
346
347     switch (data.action) {
348         case 'createSchema':
349             // Schema creation is synchronous and apparently throws
350             // no exceptions, at least until connect() is called.
351             createSchema(data.schema);
352             replySuccess();
353             break;
354
355         case 'connect':
356             connect(data.schema).then(replySuccess, replyError);
357             break;
358
359         case 'insertOrReplace':
360             insertOrReplace(data.schema, data.table, data.rows)
361                 .then(replySuccess, replyError);
362             break;
363
364         case 'insert':
365             insert(data.schema, data.table, data.rows)
366                 .then(replySuccess, replyError);
367             break;
368
369         case 'selectWhereEqual':
370             selectWhereEqual(data.schema, data.table, data.field, data.value)
371                 .then(replySuccess, replyError);
372             break;
373
374         case 'selectWhereIn':
375             selectWhereIn(data.schema, data.table, data.field, data.value)
376                 .then(replySuccess, replyError);
377             break;
378
379         case 'selectAll':
380             selectAll(data.schema, data.table).then(replySuccess, replyError);
381             break;
382
383         case 'deleteAll':
384             deleteAll(data.schema, data.table).then(replySuccess, replyError);
385             break;
386
387         case 'deleteWhereEqual':
388             deleteWhereEqual(data.schema, data.table, data.field, data.value)
389                 .then(replySuccess, replyError);
390             break;
391
392         case 'hasRows':
393             hasRows(data.schema, data.table).then(replySuccess, replyError);
394             break;
395
396         case 'populateBlockList':
397             populateBlockList(data.authtoken).then(replySuccess, replyError);
398             break;
399
400         default:
401             console.error('no such DB action ' + data.action);
402     }
403 }
404
405 onconnect = function(e) {
406     var port = e.ports[0];
407     port.addEventListener('message',
408         function(e) {dispatchRequest(port, e.data);});
409     port.start();
410 }
411
412
413