mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
1f324dedfe
Depends on D179808 Differential Revision: https://phabricator.services.mozilla.com/D179809
213 lines
6.9 KiB
JavaScript
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,
|
|
};
|