1 importScripts('/js/ui/default/staff/build/js/lovefield.min.js');
3 // Collection of schema tracking objects.
6 // Create the DB schema / tables
8 function createSchema(schemaName) {
9 if (schemas[schemaName]) return;
11 var meta = lf.schema.create(schemaName, 2);
12 schemas[schemaName] = {name: schemaName, meta: meta};
16 createCacheTables(meta);
19 createOfflineTables(meta);
22 console.error('No schema definition for ' + schemaName);
26 // Offline cache tables are globally available in the staff client
27 // for on-demand caching.
28 function createCacheTables(meta) {
30 meta.createTable('Setting').
31 addColumn('name', lf.Type.STRING).
32 addColumn('value', lf.Type.STRING).
33 addPrimaryKey(['name']);
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']);
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']);
46 meta.createTable('StatCat').
47 addColumn('id', lf.Type.INTEGER).
48 addColumn('value', lf.Type.OBJECT).
49 addPrimaryKey(['id']);
52 // Offline transaction and block list tables. These can be bulky and
53 // are only used in the offline UI.
54 function createOfflineTables(meta) {
56 meta.createTable('OfflineXact').
57 addColumn('seq', lf.Type.INTEGER).
58 addColumn('value', lf.Type.OBJECT).
59 addPrimaryKey(['seq'], true);
61 meta.createTable('OfflineBlocks').
62 addColumn('barcode', lf.Type.STRING).
63 addColumn('reason', lf.Type.STRING).
64 addPrimaryKey(['barcode']);
67 // Connect to the database for a given schema
68 function connect(schemaName) {
70 var schema = schemas[schemaName];
72 return Promise.reject('createSchema(' +
73 schemaName + ') call required');
76 if (schema.db) { // already connected.
77 return Promise.resolve();
80 return new Promise(function(resolve, reject) {
82 schema.meta.connect().then(
88 reject('Error connecting to schema ' +
89 schemaName + ' : ' + err);
93 reject('Error connecting to schema ' + schemaName + ' : ' + E);
98 function getTableInfo(schemaName, tableName) {
99 var schema = schemas[schemaName];
103 info.error = 'createSchema(' + schemaName + ') call required';
105 } else if (!schema.db) {
106 info.error = 'connect(' + schemaName + ') call required';
109 info.schema = schema;
110 info.table = schema.meta.getSchema().table(tableName);
113 info.error = 'no such table ' + tableName;
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) {
125 var info = getTableInfo(schemaName, tableName);
126 if (info.error) { return Promise.reject(info.error); }
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; });
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) {
138 var info = getTableInfo(schemaName, tableName);
139 if (info.error) { return Promise.reject(info.error); }
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; });
146 // Returns rows where the selected field equals the provided value.
147 function selectWhereEqual(schemaName, tableName, field, value) {
149 var info = getTableInfo(schemaName, tableName);
150 if (info.error) { return Promise.reject(info.error); }
152 return info.schema.db.select().from(info.table)
153 .where(info.table[field].eq(value)).exec();
156 // Returns rows where the selected field equals the provided value.
157 function selectWhereIn(schemaName, tableName, field, value) {
159 var info = getTableInfo(schemaName, tableName);
160 if (info.error) { return Promise.reject(info.error); }
162 return info.schema.db.select().from(info.table)
163 .where(info.table[field].in(value)).exec();
166 // Returns all rows in the selected table
167 function selectAll(schemaName, tableName) {
169 var info = getTableInfo(schemaName, tableName);
170 if (info.error) { return Promise.reject(info.error); }
172 return info.schema.db.select().from(info.table).exec();
175 // Deletes all rows in the selected table.
176 function deleteAll(schemaName, tableName) {
178 var info = getTableInfo(schemaName, tableName);
179 if (info.error) { return Promise.reject(info.error); }
181 return info.schema.db.delete().from(info.table).exec();
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); }
189 return info.schema.db.delete().from(info.table)
190 .where(info.table[field].eq(value)).exec();
193 // Resolves to true if the selected table contains any rows.
194 function hasRows(schemaName, tableName) {
196 var info = getTableInfo(schemaName, tableName);
197 if (info.error) { return Promise.reject(info.error); }
199 return info.schema.db.select().from(info.table).limit(1).exec()
200 .then(function(rows) { return rows.length > 0 });
204 // Prevent parallel block list building calls, since it does a lot.
205 var buildingBlockList = false;
207 // Fetches the offline block list and rebuilds the offline blocks
208 // table from the new data.
209 function populateBlockList(authtoken) {
211 if (buildingBlockList) {
212 return Promise.reject('Block list download already in progress');
215 buildingBlockList = true;
217 var url = '/standalone/list.txt?ses=' +
218 authtoken + '&' + new Date().getTime();
220 console.debug('Fetching offline block list from: ' + url);
222 return new Promise(function(resolve, reject) {
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(
232 buildingBlockList = false;
236 buildingBlockList = false;
241 buildingBlockList = false;
242 reject('Error fetching offline block list');
247 xhttp.open('GET', url, true);
252 // Rebuild the offline blocks table with the provided blocks, one per line.
253 function insertOfflineBlocks(lines) {
254 console.debug('Fetched ' + lines.length + ' blocks');
256 // Clear the table first
257 return deleteAll('offline', 'OfflineBlocks').then(
260 console.debug('Cleared existing offline blocks');
262 // Create a single batch of rows for insertion.
264 var currentChunk = [];
265 var chunkSize = 10000;
266 var seen = {bc: {}}; // for easier delete
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);
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;
280 if (currentChunk.length >= chunkSize) {
282 chunks.push(currentChunk);
285 currentChunk.push({barcode: barcode, reason: reason});
288 delete seen.bc; // allow this hunk to be reclaimed
290 console.debug('offline data broken into ' +
291 chunks.length + ' chunks of size ' + chunkSize);
293 return new Promise(function(resolve, reject) {
294 insertOfflineChunks(chunks, 0, resolve, reject);
299 console.error('Error clearing offline table: ' + err);
300 return Promise.reject(err);
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');
312 insertOrReplace('offline', 'OfflineBlocks', chunk).then(
314 console.debug('Block list successfully stored chunk ' + offset);
315 insertOfflineChunks(chunks, offset + 1, resolve, reject);
322 // Routes inbound WebWorker message to the correct handler.
323 // Replies include the original request plus added response info.
324 function dispatchRequest(port, data) {
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 || '')
334 function replySuccess(result) {
336 data.result = result;
337 port.postMessage(data);
340 function replyError(err) {
341 console.error('shared worker replying with error', err);
344 port.postMessage(data);
347 switch (data.action) {
349 // Schema creation is synchronous and apparently throws
350 // no exceptions, at least until connect() is called.
351 createSchema(data.schema);
356 connect(data.schema).then(replySuccess, replyError);
359 case 'insertOrReplace':
360 insertOrReplace(data.schema, data.table, data.rows)
361 .then(replySuccess, replyError);
365 insert(data.schema, data.table, data.rows)
366 .then(replySuccess, replyError);
369 case 'selectWhereEqual':
370 selectWhereEqual(data.schema, data.table, data.field, data.value)
371 .then(replySuccess, replyError);
374 case 'selectWhereIn':
375 selectWhereIn(data.schema, data.table, data.field, data.value)
376 .then(replySuccess, replyError);
380 selectAll(data.schema, data.table).then(replySuccess, replyError);
384 deleteAll(data.schema, data.table).then(replySuccess, replyError);
387 case 'deleteWhereEqual':
388 deleteWhereEqual(data.schema, data.table, data.field, data.value)
389 .then(replySuccess, replyError);
393 hasRows(data.schema, data.table).then(replySuccess, replyError);
396 case 'populateBlockList':
397 populateBlockList(data.authtoken).then(replySuccess, replyError);
401 console.error('no such DB action ' + data.action);
405 onconnect = function(e) {
406 var port = e.ports[0];
407 port.addEventListener('message',
408 function(e) {dispatchRequest(port, e.data);});