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 // Resolves to true if the selected table contains any rows.
185 function hasRows(schemaName, tableName) {
187 var info = getTableInfo(schemaName, tableName);
188 if (info.error) { return Promise.reject(info.error); }
190 return info.schema.db.select().from(info.table).limit(1).exec()
191 .then(function(rows) { return rows.length > 0 });
195 // Prevent parallel block list building calls, since it does a lot.
196 var buildingBlockList = false;
198 // Fetches the offline block list and rebuilds the offline blocks
199 // table from the new data.
200 function populateBlockList(authtoken) {
201 if (buildingBlockList) return;
202 buildingBlockList = true;
204 var url = '/standalone/list.txt?ses=' +
205 authtoken + '&' + new Date().getTime();
207 console.debug('Fetching offline block list from: ' + url);
209 return new Promise(function(resolve, reject) {
211 var xhttp = new XMLHttpRequest();
212 xhttp.onreadystatechange = function() {
213 if (this.readyState === 4) {
214 if (this.status === 200) {
215 var blocks = xhttp.responseText;
216 var lines = blocks.split('\n');
217 insertOfflineBlocks(lines).then(
219 buildingBlockList = false;
223 buildingBlockList = false;
228 reject('Error fetching offline block list');
233 xhttp.open('GET', url, true);
238 // Rebuild the offline blocks table with the provided blocks, one per line.
239 function insertOfflineBlocks(lines) {
240 console.debug('Fetched ' + lines.length + ' blocks');
242 // Clear the table first
243 return deleteAll('offline', 'OfflineBlocks').then(
246 console.debug('Cleared existing offline blocks');
248 // Create a single batch of rows for insertion.
250 var currentChunk = [];
251 var chunkSize = 10000;
252 var seen = {bc: {}}; // for easier delete
254 chunks.push(currentChunk);
255 lines.forEach(function(line) {
256 // slice/substring instead of split(' ') to handle barcodes
257 // with trailing spaces.
258 var barcode = line.slice(0, -2);
259 var reason = line.substring(line.length - 1);
261 // Trim duplicate barcodes, since only one version of each
262 // block per barcode is kept in the offline block list
263 if (seen.bc[barcode]) return;
264 seen.bc[barcode] = true;
266 if (currentChunk.length >= chunkSize) {
268 chunks.push(currentChunk);
271 currentChunk.push({barcode: barcode, reason: reason});
274 delete seen.bc; // allow this hunk to be reclaimed
276 console.debug('offline data broken into ' +
277 chunks.length + ' chunks of size ' + chunkSize);
279 return new Promise(function(resolve, reject) {
280 insertOfflineChunks(chunks, 0, resolve, reject);
285 console.error('Error clearing offline table: ' + err);
286 return Promise.reject(err);
291 function insertOfflineChunks(chunks, offset, resolve, reject) {
292 var chunk = chunks[offset];
293 if (!chunk || chunk.length === 0) {
294 console.debug('Block list successfully stored');
298 insertOrReplace('offline', 'OfflineBlocks', chunk).then(
300 console.debug('Block list successfully stored chunk ' + offset);
301 insertOfflineChunks(chunks, offset + 1, resolve, reject);
308 // Routes inbound WebWorker message to the correct handler.
309 // Replies include the original request plus added response info.
310 function dispatchRequest(port, data) {
312 console.debug('Lovefield worker received',
313 'action=' + (data.action || ''),
314 'schema=' + (data.schema || ''),
315 'table=' + (data.table || ''),
316 'field=' + (data.field || ''),
317 'value=' + (data.value || '')
320 function replySuccess(result) {
322 data.result = result;
323 port.postMessage(data);
326 function replyError(err) {
327 console.error('shared worker replying with error', err);
330 port.postMessage(data);
333 switch (data.action) {
335 // Schema creation is synchronous and apparently throws
336 // no exceptions, at least until connect() is called.
337 createSchema(data.schema);
342 connect(data.schema).then(replySuccess, replyError);
345 case 'insertOrReplace':
346 insertOrReplace(data.schema, data.table, data.rows)
347 .then(replySuccess, replyError);
351 insert(data.schema, data.table, data.rows)
352 .then(replySuccess, replyError);
355 case 'selectWhereEqual':
356 selectWhereEqual(data.schema, data.table, data.field, data.value)
357 .then(replySuccess, replyError);
360 case 'selectWhereIn':
361 selectWhereIn(data.schema, data.table, data.field, data.value)
362 .then(replySuccess, replyError);
366 selectAll(data.schema, data.table).then(replySuccess, replyError);
370 deleteAll(data.schema, data.table).then(replySuccess, replyError);
374 hasRows(data.schema, data.table).then(replySuccess, replyError);
377 case 'populateBlockList':
378 populateBlockList(data.authtoken).then(replySuccess, replyError);
382 console.error('no such DB action ' + data.action);
386 onconnect = function(e) {
387 var port = e.ports[0];
388 port.addEventListener('message',
389 function(e) {dispatchRequest(port, e.data);});