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) {
210 if (buildingBlockList) return;
211 buildingBlockList = true;
213 var url = '/standalone/list.txt?ses=' +
214 authtoken + '&' + new Date().getTime();
216 console.debug('Fetching offline block list from: ' + url);
218 return new Promise(function(resolve, reject) {
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(
228 buildingBlockList = false;
232 buildingBlockList = false;
237 reject('Error fetching offline block list');
242 xhttp.open('GET', url, true);
247 // Rebuild the offline blocks table with the provided blocks, one per line.
248 function insertOfflineBlocks(lines) {
249 console.debug('Fetched ' + lines.length + ' blocks');
251 // Clear the table first
252 return deleteAll('offline', 'OfflineBlocks').then(
255 console.debug('Cleared existing offline blocks');
257 // Create a single batch of rows for insertion.
259 var currentChunk = [];
260 var chunkSize = 10000;
261 var seen = {bc: {}}; // for easier delete
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);
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;
275 if (currentChunk.length >= chunkSize) {
277 chunks.push(currentChunk);
280 currentChunk.push({barcode: barcode, reason: reason});
283 delete seen.bc; // allow this hunk to be reclaimed
285 console.debug('offline data broken into ' +
286 chunks.length + ' chunks of size ' + chunkSize);
288 return new Promise(function(resolve, reject) {
289 insertOfflineChunks(chunks, 0, resolve, reject);
294 console.error('Error clearing offline table: ' + err);
295 return Promise.reject(err);
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');
307 insertOrReplace('offline', 'OfflineBlocks', chunk).then(
309 console.debug('Block list successfully stored chunk ' + offset);
310 insertOfflineChunks(chunks, offset + 1, resolve, reject);
317 // Routes inbound WebWorker message to the correct handler.
318 // Replies include the original request plus added response info.
319 function dispatchRequest(port, data) {
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 || '')
329 function replySuccess(result) {
331 data.result = result;
332 port.postMessage(data);
335 function replyError(err) {
336 console.error('shared worker replying with error', err);
339 port.postMessage(data);
342 switch (data.action) {
344 // Schema creation is synchronous and apparently throws
345 // no exceptions, at least until connect() is called.
346 createSchema(data.schema);
351 connect(data.schema).then(replySuccess, replyError);
354 case 'insertOrReplace':
355 insertOrReplace(data.schema, data.table, data.rows)
356 .then(replySuccess, replyError);
360 insert(data.schema, data.table, data.rows)
361 .then(replySuccess, replyError);
364 case 'selectWhereEqual':
365 selectWhereEqual(data.schema, data.table, data.field, data.value)
366 .then(replySuccess, replyError);
369 case 'selectWhereIn':
370 selectWhereIn(data.schema, data.table, data.field, data.value)
371 .then(replySuccess, replyError);
375 selectAll(data.schema, data.table).then(replySuccess, replyError);
379 deleteAll(data.schema, data.table).then(replySuccess, replyError);
382 case 'deleteWhereEqual':
383 deleteWhereEqual(data.schema, data.table, data.field, data.value)
384 .then(replySuccess, replyError);
388 hasRows(data.schema, data.table).then(replySuccess, replyError);
391 case 'populateBlockList':
392 populateBlockList(data.authtoken).then(replySuccess, replyError);
396 console.error('no such DB action ' + data.action);
400 onconnect = function(e) {
401 var port = e.ports[0];
402 port.addEventListener('message',
403 function(e) {dispatchRequest(port, e.data);});