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 const DB_NAME = "remote-settings";
9 * Wrap IndexedDB errors to catch them more easily.
11 class IndexedDBError extends Error {
12 constructor(error, method = "", identifier = "") {
13 if (typeof error == "string") {
14 error = new Error(error);
16 super(`IndexedDB: ${identifier} ${method} ${error && error.message}`);
17 this.name = error.name;
18 this.stack = error.stack;
22 class ShutdownError extends IndexedDBError {
23 constructor(error, method = "", identifier = "") {
24 super(error, method, identifier);
28 // We batch operations in order to reduce round-trip latency to the IndexedDB
29 // database thread. The trade-offs are that the more records in the batch, the
30 // more time we spend on this thread in structured serialization, and the
31 // greater the chance to jank PBackground and this thread when the responses
32 // come back. The initial choice of 250 was made targeting 2-3ms on a fast
33 // machine and 10-15ms on a slow machine.
34 // Every chunk waits for success before starting the next, and
35 // the final chunk's completion will fire transaction.oncomplete .
36 function bulkOperationHelper(
38 { reject, completion },
44 const CHUNK_LENGTH = 250;
45 const max = Math.min(listIndex + CHUNK_LENGTH, list.length);
47 for (; listIndex < max; listIndex++) {
48 request = store[operation](list[listIndex]);
50 if (listIndex < list.length) {
51 // On error, `transaction.onerror` is called.
52 request.onsuccess = bulkOperationHelper.bind(
55 { reject, completion },
60 } else if (completion) {
63 // otherwise, we're done, and the transaction will complete on its own.
65 // The executeIDB callsite has a try... catch, but it will not catch
66 // errors in subsequent bulkOperationHelper calls chained through
67 // request.onsuccess above. We do want to catch those, so we have to
68 // feed them through manually. We cannot use an async function with
69 // promises, because if we wait a microtask after onsuccess fires to
70 // put more requests on the transaction, the transaction will auto-commit
71 // before we can add more requests.
77 * Helper to wrap some IDBObjectStore operations into a promise.
79 * @param {IDBDatabase} db
80 * @param {String|String[]} storeNames - either a string or an array of strings.
81 * @param {String} mode
82 * @param {function} callback
83 * @param {String} description of the operation for error handling purposes.
85 function executeIDB(db, storeNames, mode, callback, desc) {
86 if (!Array.isArray(storeNames)) {
87 storeNames = [storeNames];
89 const transaction = db.transaction(storeNames, mode);
90 let promise = new Promise((resolve, reject) => {
91 let stores = storeNames.map(name => transaction.objectStore(name));
93 let rejectWrapper = e => {
94 reject(new IndexedDBError(e, desc || "execute()", storeNames.join(", ")));
101 // Add all the handlers before using the stores.
102 transaction.onerror = event =>
103 reject(new IndexedDBError(event.target.error, desc || "execute()"));
104 transaction.onabort = event =>
107 event.target.error || transaction.error || "IDBTransaction aborted",
111 transaction.oncomplete = event => resolve(result);
112 // Simplify access to a single datastore:
113 if (stores.length == 1) {
117 // Although this looks sync, once the callback places requests
118 // on the datastore, it can independently keep the transaction alive and
119 // keep adding requests. Even once we exit this try.. catch, we may
120 // therefore experience errors which should abort the transaction.
121 // This is why we pass the rejection handler - then the consumer can
122 // continue to ensure that errors are handled appropriately.
123 // In theory, exceptions thrown from onsuccess handlers should also
124 // cause IndexedDB to abort the transaction, so this is a belt-and-braces
126 result = callback(stores, rejectWrapper);
131 return { promise, transaction };
135 * Helper to wrap indexedDB.open() into a promise.
137 async function openIDB(allowUpgrades = true) {
138 return new Promise((resolve, reject) => {
139 const request = indexedDB.open(DB_NAME, DB_VERSION);
140 request.onupgradeneeded = event => {
141 if (!allowUpgrades) {
144 `IndexedDB: Error accessing ${DB_NAME} IDB at version ${DB_VERSION}`
149 // When an upgrade is needed, a transaction is started.
150 const transaction = event.target.transaction;
151 transaction.onabort = event => {
153 event.target.error ||
155 new DOMException("The operation has been aborted", "AbortError");
156 reject(new IndexedDBError(error, "open()"));
159 const db = event.target.result;
160 db.onerror = event => reject(new IndexedDBError(event.target.error));
162 if (event.oldVersion < 1) {
164 const recordsStore = db.createObjectStore("records", {
165 keyPath: ["_cid", "id"],
167 // An index to obtain all the records in a collection.
168 recordsStore.createIndex("cid", "_cid");
169 // Last modified field
170 recordsStore.createIndex("last_modified", ["_cid", "last_modified"]);
172 db.createObjectStore("timestamps", {
176 if (event.oldVersion < 2) {
178 db.createObjectStore("collections", {
182 if (event.oldVersion < 3) {
184 db.createObjectStore("attachments", {
185 keyPath: ["cid", "attachmentId"],
189 request.onerror = event => reject(new IndexedDBError(event.target.error));
190 request.onsuccess = event => {
191 const db = event.target.result;
197 function destroyIDB() {
198 const request = indexedDB.deleteDatabase(DB_NAME);
199 return new Promise((resolve, reject) => {
200 request.onerror = event => reject(new IndexedDBError(event.target.error));
201 request.onsuccess = () => resolve();
205 export var IDBHelpers = {