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