--- /dev/null
+import {Injectable} from '@angular/core';
+
+// TODO: move to a more generic location.
+const WORKER_URL = '/js/ui/default/staff/offline-db-worker.js';
+
+// Tell TS about SharedWorkers
+// https://stackoverflow.com/questions/13296549/typescript-enhanced-sharedworker-portmessage-channel-contracts
+interface SharedWorker extends AbstractWorker {
+ port: MessagePort;
+};
+
+declare var SharedWorker: {
+ prototype: SharedWorker;
+ new (scriptURL: any, name: any): SharedWorker;
+ new (scriptURL: any): SharedWorker;
+}
+
+interface PendingRequest {
+ id: number;
+ resolve(response: any): any;
+ reject(error: any): any;
+}
+
+interface DbRequest {
+ schema: string;
+ action: string;
+ table?: string;
+ id?: number;
+}
+
+@Injectable({providedIn: 'root'})
+export class DbService {
+
+ autoId = 0; // each request gets a unique id.
+ cannotConnect: boolean;
+ pendingRequests: {[id: number]: PendingRequest} = {};
+
+ activeSchemas: string[] = ['cache']; // add 'offline' in the offline UI
+
+ schemasInProgress: {[schema: string]: Promise<any>} = {};
+ schemasConnected: {[schema: string]: boolean} = {};
+
+ worker: SharedWorker = null;
+
+ constructor() {}
+
+ connectToWorker() {
+ if (this.worker || this.cannotConnect) { return; }
+
+ try {
+ this.worker = new SharedWorker(WORKER_URL);
+ } catch (E) {
+ console.warn('SharedWorker() not supported', E);
+ this.cannotConnect = true;
+ return;
+ }
+
+ this.worker.onerror = err => {
+ this.cannotConnect = true;
+ console.error('Cannot connect to DB shared worker', err);
+ };
+
+ // List for responses and resolve the matching pending request.
+ this.worker.port.addEventListener('message', evt => {
+ const response = evt.data;
+ const reqId = response.id;
+ const req = this.pendingRequests[reqId];
+
+ if (!req) {
+ console.error('Recieved response for unknown request ' + reqId);
+ return;
+ }
+
+ // Request is no longer pending.
+ delete this.pendingRequests[reqId];
+
+ if (response.status === 'OK') {
+ req.resolve(response.result);
+ } else {
+ console.error('worker request failed with ' + response.error);
+ req.reject(response.error);
+ }
+ });
+
+ this.worker.port.start();
+ console.log('PORT OPENED');
+ }
+
+ // Send a request to the web worker and register the request for
+ // future resolution.
+ // Store the request ID in the request arguments, so it's included
+ // in the response, and in the pendingRequests list for linking.
+ relayRequest(req: DbRequest): Promise<any> {
+
+ return new Promise((resolve, reject) => {
+ const id = this.autoId++;
+ req.id = id;
+
+ this.pendingRequests[id] =
+ {id : id, resolve: resolve, reject: reject};
+
+ this.worker.port.postMessage(req);
+ });
+ }
+
+ request(req: DbRequest): Promise<any> {
+
+ this.connectToWorker();
+
+ if (this.cannotConnect) { return Promise.resolve(); }
+
+ return this.connectToSchemas().then(_ => this.relayRequest(req));
+ }
+
+ connectToSchemas(): Promise<any> {
+ const promises = [];
+
+ this.activeSchemas.forEach(schema =>
+ promises.push(this.connectToOneSchema(schema))
+ );
+
+ return Promise.all(promises).then(
+ _ => {},
+ err => this.cannotConnect = true
+ );
+ }
+
+ connectToOneSchema(schema: string): Promise<any> {
+
+ console.log('connecting to a schema', schema);
+
+ if (this.schemasConnected[schema]) {
+ return Promise.resolve();
+ }
+
+ if (this.schemasInProgress[schema]) {
+ return this.schemasInProgress[schema];
+ }
+
+ const promise = new Promise((resolve, reject) => {
+
+ this.relayRequest({schema: schema, action: 'createSchema'}
+
+ ).then(_ =>
+ this.relayRequest({schema: schema, action: 'connect'})
+
+ ).then(
+ _ => {
+ this.schemasConnected[schema] = true;
+ delete this.schemasInProgress[schema];
+ resolve();
+ },
+ err => reject(err)
+ );
+ });
+
+ return this.schemasInProgress[schema] = promise;
+ }
+
+ clearSettingsCache() {
+ return this.request({
+ schema: 'cache',
+ table: 'Setting',
+ action: 'deleteAll'
+ });
+ }
+}
+
+