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 }