no bug - Correct some typos in the comments. a=typo-fix
[gecko.git] / services / settings / SyncHistory.sys.mjs
blobf609eb26f7f641cfeaa46604e9f3df76142c0fa1
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/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
9 });
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;
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   }
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   }
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   }
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   }
98   /**
99    * Wipe out the **whole** store.
100    */
101   async clear() {
102     const rkv = await this.#init();
103     await rkv.clear();
104   }
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   }