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