LPXXX Angular IndexedDB pass-through service
authorBill Erickson <berickxx@gmail.com>
Mon, 2 Dec 2019 17:26:08 +0000 (12:26 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 2 Dec 2019 17:26:08 +0000 (12:26 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/core/db.service.ts [new file with mode: 0644]

diff --git a/Open-ILS/src/eg2/src/app/core/db.service.ts b/Open-ILS/src/eg2/src/app/core/db.service.ts
new file mode 100644 (file)
index 0000000..5323880
--- /dev/null
@@ -0,0 +1,169 @@
+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'
+        });
+    }
+}
+
+