Bug 1925425: Only consider -allow-content-analysis command line flag in nightly and...
[gecko.git] / toolkit / components / places / BookmarkList.sys.mjs
blob3465948b529dfe6eb2bf91cb036b3bae15d49d0a
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
9   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
10 });
12 const OBSERVER_DEBOUNCE_RATE_MS = 500;
13 const OBSERVER_DEBOUNCE_TIMEOUT_MS = 5000;
15 /**
16  * A collection of bookmarks that internally stays up-to-date in order to
17  * efficiently query whether certain URLs are bookmarked.
18  */
19 export class BookmarkList {
20   /**
21    * The set of hashed URLs that need to be fetched from the database.
22    *
23    * @type {Set<string>}
24    */
25   #urlsToFetch = new Set();
27   /**
28    * The function to call when changes are made.
29    *
30    * @type {function}
31    */
32   #observer;
34   /**
35    * Cached mapping of hashed URLs to how many bookmarks they are used in.
36    *
37    * @type {Map<string, number>}
38    */
39   #bookmarkCount = new Map();
41   /**
42    * Cached mapping of bookmark GUIDs to their respective URL hashes.
43    *
44    * @type {Map<string, string>}
45    */
46   #guidToUrl = new Map();
48   /**
49    * @type {DeferredTask}
50    */
51   #observerTask;
53   /**
54    * Construct a new BookmarkList.
55    *
56    * @param {string[]} urls
57    *   The initial set of URLs to track.
58    * @param {function} [observer]
59    *   The function to call when changes are made.
60    * @param {number} [debounceRate]
61    *   Time between observer executions, in milliseconds.
62    * @param {number} [debounceTimeout]
63    *   The maximum time to wait for an idle callback, in milliseconds.
64    */
65   constructor(urls, observer, debounceRate, debounceTimeout) {
66     this.setTrackedUrls(urls);
67     this.#observer = observer;
68     this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
69     this.addListeners(debounceRate, debounceTimeout);
70   }
72   /**
73    * Add places listeners to this bookmark list. The observer (if one was
74    * provided) will be called after processing any events.
75    *
76    * @param {number} [debounceRate]
77    *   Time between observer executions, in milliseconds.
78    * @param {number} [debounceTimeout]
79    *   The maximum time to wait for an idle callback, in milliseconds.
80    */
81   addListeners(
82     debounceRate = OBSERVER_DEBOUNCE_RATE_MS,
83     debounceTimeout = OBSERVER_DEBOUNCE_TIMEOUT_MS
84   ) {
85     lazy.PlacesUtils.observers.addListener(
86       ["bookmark-added", "bookmark-removed", "bookmark-url-changed"],
87       this.handlePlacesEvents
88     );
89     this.#observerTask = new lazy.DeferredTask(
90       () => this.#observer?.(),
91       debounceRate,
92       debounceTimeout
93     );
94   }
96   /**
97    * Update the set of URLs to track.
98    *
99    * @param {string[]} urls
100    */
101   async setTrackedUrls(urls) {
102     const updatedBookmarkCount = new Map();
103     for (const url of urls) {
104       // Use cached value if possible. Otherwise, it must be fetched from db.
105       const urlHash = lazy.PlacesUtils.history.hashURL(url);
106       const count = this.#bookmarkCount.get(urlHash);
107       if (count != undefined) {
108         updatedBookmarkCount.set(urlHash, count);
109       } else {
110         this.#urlsToFetch.add(urlHash);
111       }
112     }
113     this.#bookmarkCount = updatedBookmarkCount;
115     const updateGuidToUrl = new Map();
116     for (const [guid, urlHash] of this.#guidToUrl.entries()) {
117       if (updatedBookmarkCount.has(urlHash)) {
118         updateGuidToUrl.set(guid, urlHash);
119       }
120     }
121     this.#guidToUrl = updateGuidToUrl;
122   }
124   /**
125    * Check whether the given URL is bookmarked.
126    *
127    * @param {string} url
128    * @returns {boolean}
129    *   The result, or `undefined` if the URL is not tracked.
130    */
131   async isBookmark(url) {
132     if (this.#urlsToFetch.size) {
133       await this.#fetchTrackedUrls();
134     }
135     const urlHash = lazy.PlacesUtils.history.hashURL(url);
136     const count = this.#bookmarkCount.get(urlHash);
137     return count != undefined ? Boolean(count) : count;
138   }
140   /**
141    * Run the database query and populate the bookmarks cache with the URLs
142    * that are waiting to be fetched.
143    */
144   async #fetchTrackedUrls() {
145     const urls = [...this.#urlsToFetch];
146     this.#urlsToFetch = new Set();
147     for (const urlHash of urls) {
148       this.#bookmarkCount.set(urlHash, 0);
149     }
150     const db = await lazy.PlacesUtils.promiseDBConnection();
151     for (const chunk of lazy.PlacesUtils.chunkArray(urls, db.variableLimit)) {
152       // Note that this query does not *explicitly* filter out tags, but we
153       // should not expect to find any, unless the db is somehow malformed.
154       const sql = `SELECT b.guid, p.url_hash
155         FROM moz_bookmarks b
156         JOIN moz_places p
157         ON b.fk = p.id
158         WHERE p.url_hash IN (${Array(chunk.length).fill("?").join(",")})`;
159       const rows = await db.executeCached(sql, chunk);
160       for (const row of rows) {
161         this.#cacheBookmark(
162           row.getResultByName("guid"),
163           row.getResultByName("url_hash")
164         );
165       }
166     }
167   }
169   /**
170    * Handle bookmark events and update the cache accordingly.
171    *
172    * @param {PlacesEvent[]} events
173    */
174   async handlePlacesEvents(events) {
175     let cacheUpdated = false;
176     let needsFetch = false;
177     for (const { guid, type, url } of events) {
178       const urlHash = lazy.PlacesUtils.history.hashURL(url);
179       if (this.#urlsToFetch.has(urlHash)) {
180         needsFetch = true;
181         continue;
182       }
183       const isUrlTracked = this.#bookmarkCount.has(urlHash);
184       switch (type) {
185         case "bookmark-added":
186           if (isUrlTracked) {
187             this.#cacheBookmark(guid, urlHash);
188             cacheUpdated = true;
189           }
190           break;
191         case "bookmark-removed":
192           if (isUrlTracked) {
193             this.#removeCachedBookmark(guid, urlHash);
194             cacheUpdated = true;
195           }
196           break;
197         case "bookmark-url-changed": {
198           const oldUrlHash = this.#guidToUrl.get(guid);
199           if (oldUrlHash) {
200             this.#removeCachedBookmark(guid, oldUrlHash);
201             cacheUpdated = true;
202           }
203           if (isUrlTracked) {
204             this.#cacheBookmark(guid, urlHash);
205             cacheUpdated = true;
206           }
207           break;
208         }
209       }
210     }
211     if (needsFetch) {
212       await this.#fetchTrackedUrls();
213       cacheUpdated = true;
214     }
215     if (cacheUpdated) {
216       this.#observerTask.arm();
217     }
218   }
220   /**
221    * Remove places listeners from this bookmark list. URLs are no longer
222    * tracked.
223    *
224    * In order to resume tracking, you must call `setTrackedUrls()` followed by
225    * `addListeners()`.
226    */
227   removeListeners() {
228     lazy.PlacesUtils.observers.removeListener(
229       ["bookmark-added", "bookmark-removed", "bookmark-url-changed"],
230       this.handlePlacesEvents
231     );
232     if (!this.#observerTask.isFinalized) {
233       this.#observerTask.disarm();
234       this.#observerTask.finalize();
235     }
236     this.setTrackedUrls([]);
237   }
239   /**
240    * Store a bookmark in the cache.
241    *
242    * @param {string} guid
243    * @param {string} urlHash
244    */
245   #cacheBookmark(guid, urlHash) {
246     const count = this.#bookmarkCount.get(urlHash);
247     this.#bookmarkCount.set(urlHash, count + 1);
248     this.#guidToUrl.set(guid, urlHash);
249   }
251   /**
252    * Remove a bookmark from the cache.
253    *
254    * @param {string} guid
255    * @param {string} urlHash
256    */
257   #removeCachedBookmark(guid, urlHash) {
258     const count = this.#bookmarkCount.get(urlHash);
259     this.#bookmarkCount.set(urlHash, count - 1);
260     this.#guidToUrl.delete(guid);
261   }