tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

IDBHelpers.sys.mjs (7260B)


      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/. */
      4 
      5 const DB_NAME = "remote-settings";
      6 const DB_VERSION = 3;
      7 
      8 /**
      9 * Wrap IndexedDB errors to catch them more easily.
     10 */
     11 class IndexedDBError extends Error {
     12  constructor(error, method = "", identifier = "") {
     13    if (typeof error == "string") {
     14      error = new Error(error);
     15    }
     16    super(`IndexedDB: ${identifier} ${method} ${error && error.message}`);
     17    this.name = error.name;
     18    this.stack = error.stack;
     19  }
     20 }
     21 
     22 class ShutdownError extends IndexedDBError {
     23  constructor(error, method = "", identifier = "") {
     24    super(error, method, identifier);
     25  }
     26 }
     27 
     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(
     37  store,
     38  { reject, completion },
     39  operation,
     40  list,
     41  listIndex = 0
     42 ) {
     43  try {
     44    const CHUNK_LENGTH = 250;
     45    const max = Math.min(listIndex + CHUNK_LENGTH, list.length);
     46    let request;
     47    for (; listIndex < max; listIndex++) {
     48      request = store[operation](list[listIndex]);
     49    }
     50    if (listIndex < list.length) {
     51      // On error, `transaction.onerror` is called.
     52      request.onsuccess = bulkOperationHelper.bind(
     53        null,
     54        store,
     55        { reject, completion },
     56        operation,
     57        list,
     58        listIndex
     59      );
     60    } else if (completion) {
     61      completion();
     62    }
     63    // otherwise, we're done, and the transaction will complete on its own.
     64  } catch (e) {
     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.
     72    reject(e);
     73  }
     74 }
     75 
     76 /**
     77 * Helper to wrap some IDBObjectStore operations into a promise.
     78 *
     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.
     84 */
     85 function executeIDB(db, storeNames, mode, callback, desc) {
     86  if (!Array.isArray(storeNames)) {
     87    storeNames = [storeNames];
     88  }
     89  const transaction = db.transaction(storeNames, mode);
     90  let promise = new Promise((resolve, reject) => {
     91    let stores = storeNames.map(name => transaction.objectStore(name));
     92    let result;
     93    let rejectWrapper = e => {
     94      reject(new IndexedDBError(e, desc || "execute()", storeNames.join(", ")));
     95      try {
     96        transaction.abort();
     97      } catch (ex) {
     98        console.error(ex);
     99      }
    100    };
    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 =>
    105      reject(
    106        new IndexedDBError(
    107          event.target.error || transaction.error || "IDBTransaction aborted",
    108          desc || "execute()"
    109        )
    110      );
    111    transaction.oncomplete = () => resolve(result);
    112    // Simplify access to a single datastore:
    113    if (stores.length == 1) {
    114      stores = stores[0];
    115    }
    116    try {
    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
    125      // approach.
    126      result = callback(stores, rejectWrapper);
    127    } catch (e) {
    128      rejectWrapper(e);
    129    }
    130  });
    131  return { promise, transaction };
    132 }
    133 
    134 /**
    135 * Helper to wrap indexedDB.open() into a promise.
    136 */
    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) {
    142        reject(
    143          new Error(
    144            `IndexedDB: Error accessing ${DB_NAME} IDB at version ${DB_VERSION}`
    145          )
    146        );
    147        return;
    148      }
    149      // When an upgrade is needed, a transaction is started.
    150      const transaction = event.target.transaction;
    151      transaction.onabort = event => {
    152        const error =
    153          event.target.error ||
    154          transaction.error ||
    155          new DOMException("The operation has been aborted", "AbortError");
    156        reject(new IndexedDBError(error, "open()"));
    157      };
    158 
    159      const db = event.target.result;
    160      db.onerror = event => reject(new IndexedDBError(event.target.error));
    161 
    162      if (event.oldVersion < 1) {
    163        // Records store
    164        const recordsStore = db.createObjectStore("records", {
    165          keyPath: ["_cid", "id"],
    166        });
    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"]);
    171        // Timestamps store
    172        db.createObjectStore("timestamps", {
    173          keyPath: "cid",
    174        });
    175      }
    176      if (event.oldVersion < 2) {
    177        // Collections store
    178        db.createObjectStore("collections", {
    179          keyPath: "cid",
    180        });
    181      }
    182      if (event.oldVersion < 3) {
    183        // Clear existing stores for a fresh start
    184        transaction.objectStore("records").clear();
    185        transaction.objectStore("timestamps").clear();
    186        transaction.objectStore("collections").clear();
    187        // Attachment store
    188        db.createObjectStore("attachments", {
    189          keyPath: ["cid", "attachmentId"],
    190        });
    191      }
    192    };
    193    request.onerror = event => reject(new IndexedDBError(event.target.error));
    194    request.onsuccess = event => {
    195      const db = event.target.result;
    196      resolve(db);
    197    };
    198  });
    199 }
    200 
    201 function destroyIDB() {
    202  const request = indexedDB.deleteDatabase(DB_NAME);
    203  return new Promise((resolve, reject) => {
    204    request.onerror = event => reject(new IndexedDBError(event.target.error));
    205    request.onsuccess = () => resolve();
    206  });
    207 }
    208 
    209 export var IDBHelpers = {
    210  bulkOperationHelper,
    211  executeIDB,
    212  openIDB,
    213  destroyIDB,
    214  IndexedDBError,
    215  ShutdownError,
    216 };