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/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
12 * A helper to keep track of synchronization statuses.
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
18 export class SyncHistory {
19 // Internal reference to underlying rkv store.
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.
27 constructor(source, { size } = { size: 100 }) {
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.
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
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}`);
48 const key = `v1-${this.source}\t${timestamp}`;
49 const value = { timestamp, status, infos };
50 await rkv.put(key, JSON.stringify(value));
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}`);
60 * Retrieve the stored history entries for a certain source, sorted by
61 * timestamp descending.
63 * @returns {Array<Object>} a list of objects
66 const rkv = await this.#init();
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(
78 const stored = JSON.parse(value);
79 entries.push({ ...stored, datetime: new Date(stored.timestamp) });
81 // Ignore malformed entries.
85 // Sort entries by `timestamp` descending.
86 entries.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1));
91 * Return the most recent entry.
94 // List is sorted from newer to older.
95 return (await this.list())[0];
99 * Wipe out the **whole** store.
102 const rkv = await this.#init();
107 * Initialize the rkv store in the user profile.
109 * @returns {Object} the underlying `KeyValueService` instance.
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");