Bug 1746711 Part 2: Ensure the enqueued surface has a color space. r=gfx-reviewers...
[gecko.git] / services / settings / RemoteSettingsWorker.js
blobc31a478a0d397ae36def2ed853c4713474db9f91
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-env mozilla/chrome-worker */
7 "use strict";
9 /**
10  * A worker dedicated to Remote Settings.
11  */
13 importScripts(
14   "resource://gre/modules/workers/require.js",
15   "resource://gre/modules/CanonicalJSON.jsm",
16   "resource://services-settings/IDBHelpers.jsm",
17   "resource://services-settings/SharedUtils.jsm",
18   "resource://gre/modules/third_party/jsesc/jsesc.js"
21 const IDB_RECORDS_STORE = "records";
22 const IDB_TIMESTAMPS_STORE = "timestamps";
24 let gShutdown = false;
26 const Agent = {
27   /**
28    * Return the canonical JSON serialization of the specified records.
29    * It has to match what is done on the server (See Kinto/kinto-signer).
30    *
31    * @param {Array<Object>} records
32    * @param {String} timestamp
33    * @returns {String}
34    */
35   async canonicalStringify(records, timestamp) {
36     // Sort list by record id.
37     let allRecords = records.sort((a, b) => {
38       if (a.id < b.id) {
39         return -1;
40       }
41       return a.id > b.id ? 1 : 0;
42     });
43     // All existing records are replaced by the version from the server
44     // and deleted records are removed.
45     for (let i = 0; i < allRecords.length /* no increment! */; ) {
46       const rec = allRecords[i];
47       const next = allRecords[i + 1];
48       if ((next && rec.id == next.id) || rec.deleted) {
49         allRecords.splice(i, 1); // remove local record
50       } else {
51         i++;
52       }
53     }
54     const toSerialize = {
55       last_modified: "" + timestamp,
56       data: allRecords,
57     };
58     return CanonicalJSON.stringify(toSerialize, jsesc);
59   },
61   /**
62    * If present, import the JSON file into the Remote Settings IndexedDB
63    * for the specified bucket and collection.
64    * (eg. blocklists/certificates, main/onboarding)
65    * @param {String} bucket
66    * @param {String} collection
67    * @returns {int} Number of records loaded from dump or -1 if no dump found.
68    */
69   async importJSONDump(bucket, collection) {
70     const { data: records } = await SharedUtils.loadJSONDump(
71       bucket,
72       collection
73     );
74     if (records === null) {
75       // Return -1 if file is missing.
76       return -1;
77     }
78     if (gShutdown) {
79       throw new Error("Can't import when we've started shutting down.");
80     }
81     await importDumpIDB(bucket, collection, records);
82     return records.length;
83   },
85   /**
86    * Check that the specified file matches the expected size and SHA-256 hash.
87    * @param {String} fileUrl file URL to read from
88    * @param {Number} size expected file size
89    * @param {String} size expected file SHA-256 as hex string
90    * @returns {boolean}
91    */
92   async checkFileHash(fileUrl, size, hash) {
93     let resp;
94     try {
95       resp = await fetch(fileUrl);
96     } catch (e) {
97       // File does not exist.
98       return false;
99     }
100     const buffer = await resp.arrayBuffer();
101     return SharedUtils.checkContentHash(buffer, size, hash);
102   },
104   async prepareShutdown() {
105     gShutdown = true;
106     // Ensure we can iterate and abort (which may delete items) by cloning
107     // the list.
108     let transactions = Array.from(gPendingTransactions);
109     for (let transaction of transactions) {
110       try {
111         transaction.abort();
112       } catch (ex) {
113         // We can hit this case if the transaction has finished but
114         // we haven't heard about it yet.
115       }
116     }
117   },
119   _test_only_import(bucket, collection, records) {
120     return importDumpIDB(bucket, collection, records);
121   },
125  * Wrap worker invocations in order to return the `callbackId` along
126  * the result. This will allow to transform the worker invocations
127  * into promises in `RemoteSettingsWorker.jsm`.
128  */
129 self.onmessage = event => {
130   const { callbackId, method, args = [] } = event.data;
131   Agent[method](...args)
132     .then(result => {
133       self.postMessage({ callbackId, result });
134     })
135     .catch(error => {
136       console.log(`RemoteSettingsWorker error: ${error}`);
137       self.postMessage({ callbackId, error: "" + error });
138     });
141 let gPendingTransactions = new Set();
144  * Import the records into the Remote Settings Chrome IndexedDB.
146  * Note: This duplicates some logics from `kinto-offline-client.js`.
148  * @param {String} bucket
149  * @param {String} collection
150  * @param {Array<Object>} records
151  */
152 async function importDumpIDB(bucket, collection, records) {
153   // Open the DB. It will exist since if we are running this, it means
154   // we already tried to read the timestamp in `remote-settings.js`
155   const db = await IDBHelpers.openIDB(false /* do not allow upgrades */);
157   // try...finally to ensure we always close the db.
158   try {
159     if (gShutdown) {
160       throw new Error("Can't import when we've started shutting down.");
161     }
163     // Each entry of the dump will be stored in the records store.
164     // They are indexed by `_cid`.
165     const cid = bucket + "/" + collection;
166     // We can just modify the items in-place, as we got them from SharedUtils.loadJSONDump().
167     records.forEach(item => {
168       item._cid = cid;
169     });
170     // Store the highest timestamp as the collection timestamp (or zero if dump is empty).
171     const timestamp =
172       records.length === 0
173         ? 0
174         : Math.max(...records.map(record => record.last_modified));
175     let { transaction, promise } = IDBHelpers.executeIDB(
176       db,
177       [IDB_RECORDS_STORE, IDB_TIMESTAMPS_STORE],
178       "readwrite",
179       ([recordsStore, timestampStore], rejectTransaction) => {
180         // Wipe before loading
181         recordsStore.delete(IDBKeyRange.bound([cid], [cid, []], false, true));
182         IDBHelpers.bulkOperationHelper(
183           recordsStore,
184           {
185             reject: rejectTransaction,
186             completion() {
187               timestampStore.put({ cid, value: timestamp });
188             },
189           },
190           "put",
191           records
192         );
193       }
194     );
195     gPendingTransactions.add(transaction);
196     promise = promise.finally(() => gPendingTransactions.delete(transaction));
197     await promise;
198   } finally {
199     // Close now that we're done.
200     db.close();
201   }