diff options
Diffstat (limited to '')
-rw-r--r-- | services/settings/SyncHistory.sys.mjs | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/services/settings/SyncHistory.sys.mjs b/services/settings/SyncHistory.sys.mjs new file mode 100644 index 0000000000..f609eb26f7 --- /dev/null +++ b/services/settings/SyncHistory.sys.mjs @@ -0,0 +1,120 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + KeyValueService: "resource://gre/modules/kvstore.sys.mjs", +}); + +/** + * A helper to keep track of synchronization statuses. + * + * We rely on a different storage backend than for storing Remote Settings data, + * because the eventual goal is to be able to detect `IndexedDB` issues and act + * accordingly. + */ +export class SyncHistory { + // Internal reference to underlying rkv store. + #store; + + /** + * @param {String} source the synchronization source (eg. `"settings-sync"`) + * @param {Object} options + * @param {int} options.size Maximum number of entries per source. + */ + constructor(source, { size } = { size: 100 }) { + this.source = source; + this.size = size; + } + + /** + * Store the synchronization status. The ETag is converted and stored as + * a millisecond epoch timestamp. + * The entries with the oldest timestamps will be deleted to maintain the + * history size under the configured maximum. + * + * @param {String} etag the ETag value from the server (eg. `"1647961052593"`) + * @param {String} status the synchronization status (eg. `"success"`) + * @param {Object} infos optional additional information to keep track of + */ + async store(etag, status, infos = {}) { + const rkv = await this.#init(); + const timestamp = parseInt(etag.replace('"', ""), 10); + if (Number.isNaN(timestamp)) { + throw new Error(`Invalid ETag value ${etag}`); + } + const key = `v1-${this.source}\t${timestamp}`; + const value = { timestamp, status, infos }; + await rkv.put(key, JSON.stringify(value)); + // Trim old entries. + const allEntries = await this.list(); + for (let i = this.size; i < allEntries.length; i++) { + let { timestamp } = allEntries[i]; + await rkv.delete(`v1-${this.source}\t${timestamp}`); + } + } + + /** + * Retrieve the stored history entries for a certain source, sorted by + * timestamp descending. + * + * @returns {Array<Object>} a list of objects + */ + async list() { + const rkv = await this.#init(); + const entries = []; + // The "from" and "to" key parameters to nsIKeyValueStore.enumerate() + // are inclusive and exclusive, respectively, and keys are tuples + // of source and datetime joined by a tab (\t), which is character code 9; + // so enumerating ["source", "source\n"), where the line feed (\n) + // is character code 10, enumerates all pairs with the given source. + for (const { value } of await rkv.enumerate( + `v1-${this.source}`, + `v1-${this.source}\n` + )) { + try { + const stored = JSON.parse(value); + entries.push({ ...stored, datetime: new Date(stored.timestamp) }); + } catch (e) { + // Ignore malformed entries. + console.error(e); + } + } + // Sort entries by `timestamp` descending. + entries.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)); + return entries; + } + + /** + * Return the most recent entry. + */ + async last() { + // List is sorted from newer to older. + return (await this.list())[0]; + } + + /** + * Wipe out the **whole** store. + */ + async clear() { + const rkv = await this.#init(); + await rkv.clear(); + } + + /** + * Initialize the rkv store in the user profile. + * + * @returns {Object} the underlying `KeyValueService` instance. + */ + async #init() { + if (!this.#store) { + // Get and cache a handle to the kvstore. + const dir = PathUtils.join(PathUtils.profileDir, "settings"); + await IOUtils.makeDirectory(dir); + this.#store = await lazy.KeyValueService.getOrCreate(dir, "synchistory"); + } + return this.#store; + } +} |