1 import {Injectable} from '@angular/core';
3 /** Service to relay requests to/from our IndexedDB shared worker */
5 // TODO: move to a more generic location.
6 const WORKER_URL = '/js/ui/default/staff/offline-db-worker.js';
8 // Tell TS about SharedWorkers
9 // https://stackoverflow.com/questions/13296549/typescript-enhanced-sharedworker-portmessage-channel-contracts
10 interface SharedWorker extends AbstractWorker {
14 declare var SharedWorker: {
15 prototype: SharedWorker;
16 new (scriptUrl: any, name?: any): SharedWorker;
20 // Requests in flight to the shared worker
21 interface ActiveRequest {
23 resolve(response: any): any;
24 reject(error: any): any;
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 {
40 // Expected response structure from the shared worker.
41 // Note callers only recive the 'result' content, which may
43 interface DbStoreResponse {
50 @Injectable({providedIn: 'root'})
51 export class DbStoreService {
53 autoId = 0; // each request gets a unique id.
54 cannotConnect: boolean;
56 activeRequests: {[id: number]: ActiveRequest} = {};
58 // Schemas we should connect to
59 activeSchemas: string[] = ['cache']; // add 'offline' in the offline UI
61 // Schemas we are in the process of connecting to
62 schemasInProgress: {[schema: string]: Promise<any>} = {};
64 // Schemas we have successfully connected to
65 schemasConnected: {[schema: string]: boolean} = {};
67 worker: SharedWorker = null;
71 private connectToWorker() {
72 if (this.worker || this.cannotConnect) { return; }
75 this.worker = new SharedWorker(WORKER_URL);
77 console.warn('SharedWorker() not supported', E);
78 this.cannotConnect = true;
82 this.worker.onerror = err => {
83 this.cannotConnect = true;
84 console.error('Cannot connect to DB shared worker', err);
87 // List for responses and resolve the matching pending request.
88 this.worker.port.addEventListener(
89 'message', evt => this.handleMessage(evt));
91 this.worker.port.start();
94 private handleMessage(evt: MessageEvent) {
95 const response: DbStoreResponse = evt.data as DbStoreResponse;
96 const reqId = response.id;
97 const req = this.activeRequests[reqId];
100 console.error('Recieved response for unknown request', reqId);
104 // Request is no longer active.
105 delete this.activeRequests[reqId];
107 if (response.status === 'OK') {
108 req.resolve(response.result);
110 console.error('worker request failed with', response.error);
111 req.reject(response.error);
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);
127 // Connect to all active schemas, requesting each be created
129 private connectToSchemas(): Promise<any> {
132 this.activeSchemas.forEach(schema =>
133 promises.push(this.connectToOneSchema(schema)));
135 return Promise.all(promises).then(
137 err => this.cannotConnect = true
141 private connectToOneSchema(schema: string): Promise<any> {
143 if (this.schemasConnected[schema]) {
144 return Promise.resolve();
147 if (this.schemasInProgress[schema]) {
148 return this.schemasInProgress[schema];
151 const promise = new Promise((resolve, reject) => {
153 this.relayRequest({schema: schema, action: 'createSchema'})
156 this.relayRequest({schema: schema, action: 'connect'}))
160 this.schemasConnected[schema] = true;
161 delete this.schemasInProgress[schema];
168 return this.schemasInProgress[schema] = promise;
171 request(req: DbStoreRequest): Promise<any> {
173 // NO-OP if we're already connected.
174 this.connectToWorker();
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(); }
181 return this.connectToSchemas().then(_ => this.relayRequest(req));