]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/core/db-store.service.ts
LP1901760 Improve SharedWorker non-support handling (AngJS)
[Evergreen.git] / Open-ILS / src / eg2 / src / app / core / db-store.service.ts
1 import {Injectable} from '@angular/core';
2
3 /** Service to relay requests to/from our IndexedDB shared worker */
4
5 // TODO: move to a more generic location.
6 const WORKER_URL = '/js/ui/default/staff/offline-db-worker.js';
7
8 // Tell TS about SharedWorkers
9 // https://stackoverflow.com/questions/13296549/typescript-enhanced-sharedworker-portmessage-channel-contracts
10 interface SharedWorker extends AbstractWorker {
11     port: MessagePort;
12 }
13
14 declare var SharedWorker: {
15     prototype: SharedWorker;
16     new (scriptUrl: any, name?: any): SharedWorker;
17 };
18 // ---
19
20 // Requests in flight to the shared worker
21 interface ActiveRequest {
22    id: number;
23    resolve(response: any): any;
24    reject(error: any): any;
25 }
26
27 // Shared worker request structure.  This is the request that's
28 // relayed to the shared worker.
29 // DbStoreRequest.id === ActiveRequest.id
30 interface DbStoreRequest {
31     schema: string;
32     action: string;
33     field?: string;
34     value?: any;
35     table?: string;
36     rows?: any[];
37     id?: number;
38 }
39
40 // Expected response structure from the shared worker.
41 // Note callers only recive the 'result' content, which may
42 // be anything.
43 interface DbStoreResponse {
44     status: string;
45     result: any;
46     error?: string;
47     id?: number;
48 }
49
50 @Injectable({providedIn: 'root'})
51 export class DbStoreService {
52
53     autoId = 0; // each request gets a unique id.
54     cannotConnect: boolean;
55
56     activeRequests: {[id: number]: ActiveRequest} = {};
57
58     // Schemas we should connect to
59     activeSchemas: string[] = ['cache']; // add 'offline' in the offline UI
60
61     // Schemas we are in the process of connecting to
62     schemasInProgress: {[schema: string]: Promise<any>} = {};
63
64     // Schemas we have successfully connected to
65     schemasConnected: {[schema: string]: boolean} = {};
66
67     worker: SharedWorker = null;
68
69     constructor() {}
70
71     private connectToWorker() {
72         if (this.worker || this.cannotConnect) { return; }
73
74         try {
75             this.worker = new SharedWorker(WORKER_URL);
76         } catch (E) {
77             console.warn('SharedWorker() not supported', E);
78             this.cannotConnect = true;
79             return;
80         }
81
82         this.worker.onerror = err => {
83             this.cannotConnect = true;
84             console.error('Cannot connect to DB shared worker', err);
85         };
86
87         // List for responses and resolve the matching pending request.
88         this.worker.port.addEventListener(
89             'message', evt => this.handleMessage(evt));
90
91         this.worker.port.start();
92     }
93
94     private handleMessage(evt: MessageEvent) {
95         const response: DbStoreResponse = evt.data as DbStoreResponse;
96         const reqId = response.id;
97         const req = this.activeRequests[reqId];
98
99         if (!req) {
100             console.error('Recieved response for unknown request', reqId);
101             return;
102         }
103
104         // Request is no longer active.
105         delete this.activeRequests[reqId];
106
107         if (response.status === 'OK') {
108             req.resolve(response.result);
109         } else {
110             console.error('worker request failed with', response.error);
111             req.reject(response.error);
112         }
113     }
114
115     // Send a request to the web worker and register the request
116     // for future resolution.  Store the request ID in the request
117     // arguments, so it's included in the response, and in the
118     // activeRequests list for linking.
119     private relayRequest(req: DbStoreRequest): Promise<any> {
120         return new Promise((resolve, reject) => {
121             const id = req.id = this.autoId++;
122             this.activeRequests[id] = {id: id, resolve: resolve, reject: reject};
123             this.worker.port.postMessage(req);
124         });
125     }
126
127     // Connect to all active schemas, requesting each be created
128     // when necessary.
129     private connectToSchemas(): Promise<any> {
130         const promises = [];
131
132         this.activeSchemas.forEach(schema =>
133             promises.push(this.connectToOneSchema(schema)));
134
135         return Promise.all(promises).then(
136             _ => {},
137             err => this.cannotConnect = true
138         );
139     }
140
141     private connectToOneSchema(schema: string): Promise<any> {
142
143         if (this.schemasConnected[schema]) {
144             return Promise.resolve();
145         }
146
147         if (this.schemasInProgress[schema]) {
148             return this.schemasInProgress[schema];
149         }
150
151         const promise = new Promise((resolve, reject) => {
152
153             this.relayRequest({schema: schema, action: 'createSchema'})
154
155             .then(_ =>
156                 this.relayRequest({schema: schema, action: 'connect'}))
157
158             .then(
159                 _ => {
160                     this.schemasConnected[schema] = true;
161                     delete this.schemasInProgress[schema];
162                     resolve();
163                 },
164                 err => reject(err)
165             );
166         });
167
168         return this.schemasInProgress[schema] = promise;
169     }
170
171     request(req: DbStoreRequest): Promise<any> {
172
173         // NO-OP if we're already connected.
174         this.connectToWorker();
175
176         // If we are unable to connect, it means we are in an
177         // environment that does not support shared workers.
178         // Treat all requests as a NO-OP.
179         if (this.cannotConnect) { return Promise.resolve(); }
180
181         return this.connectToSchemas().then(_ => this.relayRequest(req));
182     }
183 }
184
185