From 8c9187edec9671c5a251d79bef874e8b9fe80d83 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 2 Dec 2019 12:26:08 -0500 Subject: [PATCH] LP1854850 Angular IndexedDB Shared Worker Communication Implements the client side of the communication to the Evergreen IndexedDB shared worker for Angular. From this may be built caching mechanisms, offline transaction handling, etc. Signed-off-by: Bill Erickson Signed-off-by: Chris Sharp --- Open-ILS/src/eg2/src/app/core/db-store.service.ts | 185 ++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 Open-ILS/src/eg2/src/app/core/db-store.service.ts diff --git a/Open-ILS/src/eg2/src/app/core/db-store.service.ts b/Open-ILS/src/eg2/src/app/core/db-store.service.ts new file mode 100644 index 0000000000..259e478f41 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/core/db-store.service.ts @@ -0,0 +1,185 @@ +import {Injectable} from '@angular/core'; + +/** Service to relay requests to/from our IndexedDB shared worker */ + +// 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; +}; +// --- + +// Requests in flight to the shared worker +interface ActiveRequest { + id: number; + resolve(response: any): any; + reject(error: any): any; +} + +// Shared worker request structure. This is the request that's +// relayed to the shared worker. +// DbStoreRequest.id === ActiveRequest.id +interface DbStoreRequest { + schema: string; + action: string; + field?: string; + value?: any; + table?: string; + rows?: any[]; + id?: number; +} + +// Expected response structure from the shared worker. +// Note callers only recive the 'result' content, which may +// be anything. +interface DbStoreResponse { + status: string; + result: any; + error?: string; + id?: number; +} + +@Injectable({providedIn: 'root'}) +export class DbStoreService { + + autoId = 0; // each request gets a unique id. + cannotConnect: boolean; + + activeRequests: {[id: number]: ActiveRequest} = {}; + + // Schemas we should connect to + activeSchemas: string[] = ['cache']; // add 'offline' in the offline UI + + // Schemas we are in the process of connecting to + schemasInProgress: {[schema: string]: Promise} = {}; + + // Schemas we have successfully connected to + schemasConnected: {[schema: string]: boolean} = {}; + + worker: SharedWorker = null; + + constructor() {} + + private 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 => this.handleMessage(evt)); + + this.worker.port.start(); + } + + private handleMessage(evt: MessageEvent) { + const response: DbStoreResponse = evt.data as DbStoreResponse; + const reqId = response.id; + const req = this.activeRequests[reqId]; + + if (!req) { + console.error('Recieved response for unknown request', reqId); + return; + } + + // Request is no longer active. + delete this.activeRequests[reqId]; + + if (response.status === 'OK') { + req.resolve(response.result); + } else { + console.error('worker request failed with', response.error); + req.reject(response.error); + } + } + + // 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 + // activeRequests list for linking. + private relayRequest(req: DbStoreRequest): Promise { + return new Promise((resolve, reject) => { + const id = req.id = this.autoId++; + this.activeRequests[id] = {id: id, resolve: resolve, reject: reject}; + this.worker.port.postMessage(req); + }); + } + + // Connect to all active schemas, requesting each be created + // when necessary. + private connectToSchemas(): Promise { + const promises = []; + + this.activeSchemas.forEach(schema => + promises.push(this.connectToOneSchema(schema))); + + return Promise.all(promises).then( + _ => {}, + err => this.cannotConnect = true + ); + } + + private connectToOneSchema(schema: string): Promise { + + 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; + } + + request(req: DbStoreRequest): Promise { + + // NO-OP if we're already connected. + this.connectToWorker(); + + // If we are unable to connect, it means we are in an + // environment that does not support shared workers. + // Treat all requests as a NO-OP. + if (this.cannotConnect) { return Promise.resolve(); } + + return this.connectToSchemas().then(_ => this.relayRequest(req)); + } +} + + -- 2.11.0