gecko-dev/services/settings/IDBHelpers.sys.mjs

213 lines
6.9 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const DB_NAME = "remote-settings";
const DB_VERSION = 3;
/**
* Wrap IndexedDB errors to catch them more easily.
*/
class IndexedDBError extends Error {
constructor(error, method = "", identifier = "") {
if (typeof error == "string") {
error = new Error(error);
}
super(`IndexedDB: ${identifier} ${method} ${error && error.message}`);
this.name = error.name;
this.stack = error.stack;
}
}
class ShutdownError extends IndexedDBError {
constructor(error, method = "", identifier = "") {
super(error, method, identifier);
}
}
// We batch operations in order to reduce round-trip latency to the IndexedDB
// database thread. The trade-offs are that the more records in the batch, the
// more time we spend on this thread in structured serialization, and the
// greater the chance to jank PBackground and this thread when the responses
// come back. The initial choice of 250 was made targeting 2-3ms on a fast
// machine and 10-15ms on a slow machine.
// Every chunk waits for success before starting the next, and
// the final chunk's completion will fire transaction.oncomplete .
function bulkOperationHelper(
store,
{ reject, completion },
operation,
list,
listIndex = 0
) {
try {
const CHUNK_LENGTH = 250;
const max = Math.min(listIndex + CHUNK_LENGTH, list.length);
let request;
for (; listIndex < max; listIndex++) {
request = store[operation](list[listIndex]);
}
if (listIndex < list.length) {
// On error, `transaction.onerror` is called.
request.onsuccess = bulkOperationHelper.bind(
null,
store,
{ reject, completion },
operation,
list,
listIndex
);
} else if (completion) {
completion();
}
// otherwise, we're done, and the transaction will complete on its own.
} catch (e) {
// The executeIDB callsite has a try... catch, but it will not catch
// errors in subsequent bulkOperationHelper calls chained through
// request.onsuccess above. We do want to catch those, so we have to
// feed them through manually. We cannot use an async function with
// promises, because if we wait a microtask after onsuccess fires to
// put more requests on the transaction, the transaction will auto-commit
// before we can add more requests.
reject(e);
}
}
/**
* Helper to wrap some IDBObjectStore operations into a promise.
*
* @param {IDBDatabase} db
* @param {String|String[]} storeNames - either a string or an array of strings.
* @param {String} mode
* @param {function} callback
* @param {String} description of the operation for error handling purposes.
*/
function executeIDB(db, storeNames, mode, callback, desc) {
if (!Array.isArray(storeNames)) {
storeNames = [storeNames];
}
const transaction = db.transaction(storeNames, mode);
let promise = new Promise((resolve, reject) => {
let stores = storeNames.map(name => transaction.objectStore(name));
let result;
let rejectWrapper = e => {
reject(new IndexedDBError(e, desc || "execute()", storeNames.join(", ")));
try {
transaction.abort();
} catch (ex) {
console.error(ex);
}
};
// Add all the handlers before using the stores.
transaction.onerror = event =>
reject(new IndexedDBError(event.target.error, desc || "execute()"));
transaction.onabort = event =>
reject(
new IndexedDBError(
event.target.error || transaction.error || "IDBTransaction aborted",
desc || "execute()"
)
);
transaction.oncomplete = event => resolve(result);
// Simplify access to a single datastore:
if (stores.length == 1) {
stores = stores[0];
}
try {
// Although this looks sync, once the callback places requests
// on the datastore, it can independently keep the transaction alive and
// keep adding requests. Even once we exit this try.. catch, we may
// therefore experience errors which should abort the transaction.
// This is why we pass the rejection handler - then the consumer can
// continue to ensure that errors are handled appropriately.
// In theory, exceptions thrown from onsuccess handlers should also
// cause IndexedDB to abort the transaction, so this is a belt-and-braces
// approach.
result = callback(stores, rejectWrapper);
} catch (e) {
rejectWrapper(e);
}
});
return { promise, transaction };
}
/**
* Helper to wrap indexedDB.open() into a promise.
*/
async function openIDB(allowUpgrades = true) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = event => {
if (!allowUpgrades) {
reject(
new Error(
`IndexedDB: Error accessing ${DB_NAME} IDB at version ${DB_VERSION}`
)
);
return;
}
// When an upgrade is needed, a transaction is started.
const transaction = event.target.transaction;
transaction.onabort = event => {
const error =
event.target.error ||
transaction.error ||
new DOMException("The operation has been aborted", "AbortError");
reject(new IndexedDBError(error, "open()"));
};
const db = event.target.result;
db.onerror = event => reject(new IndexedDBError(event.target.error));
if (event.oldVersion < 1) {
// Records store
const recordsStore = db.createObjectStore("records", {
keyPath: ["_cid", "id"],
});
// An index to obtain all the records in a collection.
recordsStore.createIndex("cid", "_cid");
// Last modified field
recordsStore.createIndex("last_modified", ["_cid", "last_modified"]);
// Timestamps store
db.createObjectStore("timestamps", {
keyPath: "cid",
});
}
if (event.oldVersion < 2) {
// Collections store
db.createObjectStore("collections", {
keyPath: "cid",
});
}
if (event.oldVersion < 3) {
// Attachment store
db.createObjectStore("attachments", {
keyPath: ["cid", "attachmentId"],
});
}
};
request.onerror = event => reject(new IndexedDBError(event.target.error));
request.onsuccess = event => {
const db = event.target.result;
resolve(db);
};
});
}
function destroyIDB() {
const request = indexedDB.deleteDatabase(DB_NAME);
return new Promise((resolve, reject) => {
request.onerror = event => reject(new IndexedDBError(event.target.error));
request.onsuccess = () => resolve();
});
}
export var IDBHelpers = {
bulkOperationHelper,
executeIDB,
openIDB,
destroyIDB,
IndexedDBError,
ShutdownError,
};