tor-browser

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

SyncHistory.sys.mjs (3859B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
      9 });
     10 
     11 /**
     12 * A helper to keep track of synchronization statuses.
     13 *
     14 * We rely on a different storage backend than for storing Remote Settings data,
     15 * because the eventual goal is to be able to detect `IndexedDB` issues and act
     16 * accordingly.
     17 */
     18 export class SyncHistory {
     19  // Internal reference to underlying rkv store.
     20  #store;
     21 
     22  /**
     23   * @param {string} source the synchronization source (eg. `"settings-sync"`)
     24   * @param {object} options
     25   * @param {int} options.size Maximum number of entries per source.
     26   */
     27  constructor(source, { size } = { size: 100 }) {
     28    this.source = source;
     29    this.size = size;
     30  }
     31 
     32  /**
     33   * Store the synchronization status. The ETag is converted and stored as
     34   * a millisecond epoch timestamp.
     35   * The entries with the oldest timestamps will be deleted to maintain the
     36   * history size under the configured maximum.
     37   *
     38   * @param {string} etag the ETag value from the server (eg. `"1647961052593"`)
     39   * @param {string} status the synchronization status (eg. `"success"`)
     40   * @param {object} infos optional additional information to keep track of
     41   */
     42  async store(etag, status, infos = {}) {
     43    const rkv = await this.#init();
     44    const timestamp = parseInt(etag.replace('"', ""), 10);
     45    if (Number.isNaN(timestamp)) {
     46      throw new Error(`Invalid ETag value ${etag}`);
     47    }
     48    const key = `v1-${this.source}\t${timestamp}`;
     49    const value = { timestamp, status, infos };
     50    await rkv.put(key, JSON.stringify(value));
     51    // Trim old entries.
     52    const allEntries = await this.list();
     53    for (let i = this.size; i < allEntries.length; i++) {
     54      let { timestamp } = allEntries[i];
     55      await rkv.delete(`v1-${this.source}\t${timestamp}`);
     56    }
     57  }
     58 
     59  /**
     60   * Retrieve the stored history entries for a certain source, sorted by
     61   * timestamp descending.
     62   *
     63   * @returns {Array<object>} a list of objects
     64   */
     65  async list() {
     66    const rkv = await this.#init();
     67    const entries = [];
     68    // The "from" and "to" key parameters to nsIKeyValueStore.enumerate()
     69    // are inclusive and exclusive, respectively, and keys are tuples
     70    // of source and datetime joined by a tab (\t), which is character code 9;
     71    // so enumerating ["source", "source\n"), where the line feed (\n)
     72    // is character code 10, enumerates all pairs with the given source.
     73    for (const { value } of await rkv.enumerate(
     74      `v1-${this.source}`,
     75      `v1-${this.source}\n`
     76    )) {
     77      try {
     78        const stored = JSON.parse(value);
     79        entries.push({ ...stored, datetime: new Date(stored.timestamp) });
     80      } catch (e) {
     81        // Ignore malformed entries.
     82        console.error(e);
     83      }
     84    }
     85    // Sort entries by `timestamp` descending.
     86    entries.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1));
     87    return entries;
     88  }
     89 
     90  /**
     91   * Return the most recent entry.
     92   */
     93  async last() {
     94    // List is sorted from newer to older.
     95    return (await this.list())[0];
     96  }
     97 
     98  /**
     99   * Wipe out the **whole** store.
    100   */
    101  async clear() {
    102    const rkv = await this.#init();
    103    await rkv.clear();
    104  }
    105 
    106  /**
    107   * Initialize the rkv store in the user profile.
    108   *
    109   * @returns {object} the underlying `KeyValueService` instance.
    110   */
    111  async #init() {
    112    if (!this.#store) {
    113      // Get and cache a handle to the kvstore.
    114      const dir = PathUtils.join(PathUtils.profileDir, "settings");
    115      await IOUtils.makeDirectory(dir);
    116      this.#store = await lazy.KeyValueService.getOrCreate(dir, "synchistory");
    117    }
    118    return this.#store;
    119  }
    120 }