Bug 1944627 - update sidebar button checked state for non-revamped sidebar cases...
[gecko.git] / browser / components / urlbar / UrlbarProviderOpenTabs.sys.mjs
blob8e76ad53dbc9af7a748b6eecf6ccf7984c61b0b6
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 /**
6  * This module exports a provider, returning open tabs matches for the urlbar.
7  * It is also used to register and unregister open tabs.
8  */
10 import {
11   UrlbarProvider,
12   UrlbarUtils,
13 } from "resource:///modules/UrlbarUtils.sys.mjs";
15 const lazy = {};
17 ChromeUtils.defineESModuleGetters(lazy, {
18   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
19   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
20   UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
21 });
23 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
24   UrlbarUtils.getLogger({ prefix: "Provider.OpenTabs" })
27 const PRIVATE_USER_CONTEXT_ID = -1;
29 /**
30  * Maps the open tabs by userContextId.
31  * Each entry is a Map of url => count.
32  */
33 var gOpenTabUrls = new Map();
35 /**
36  * Class used to create the provider.
37  */
38 export class UrlbarProviderOpenTabs extends UrlbarProvider {
39   constructor() {
40     super();
41   }
43   /**
44    * Returns the name of this provider.
45    *
46    * @returns {string} the name of this provider.
47    */
48   get name() {
49     return "OpenTabs";
50   }
52   /**
53    * Returns the type of this provider.
54    *
55    * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
56    */
57   get type() {
58     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
59   }
61   /**
62    * Whether this provider should be invoked for the given context.
63    * If this method returns false, the providers manager won't start a query
64    * with this provider, to save on resources.
65    *
66    * @returns {boolean} Whether this provider should be invoked for the search.
67    */
68   isActive() {
69     // For now we don't actually use this provider to query open tabs, instead
70     // we join the temp table in UrlbarProviderPlaces.
71     return false;
72   }
74   /**
75    * Tracks whether the memory tables have been initialized yet. Until this
76    * happens tabs are only stored in openTabs and later copied over to the
77    * memory table.
78    */
79   static memoryTableInitialized = false;
81   /**
82    * Return unique urls that are open for given user context id.
83    *
84    * @param {integer|string} userContextId Containers user context id
85    * @param {boolean} [isInPrivateWindow] In private browsing window or not
86    * @returns {Array} urls
87    */
88   static getOpenTabUrlsForUserContextId(
89     userContextId,
90     isInPrivateWindow = false
91   ) {
92     // It's fairly common to retrieve the value from an HTML attribute, that
93     // means we're getting sometimes a string, sometimes an integer. As we're
94     // using this as key of a Map, we must treat it consistently.
95     userContextId = parseInt(userContextId);
96     userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
97       userContextId,
98       isInPrivateWindow
99     );
100     return Array.from(gOpenTabUrls.get(userContextId)?.keys() ?? []);
101   }
103   /**
104    * Return unique urls that are open, along with their user context id.
105    *
106    * @param {boolean} [isInPrivateWindow] Whether it's for a private browsing window
107    * @returns {Map} { url => Set({userContextIds}) }
108    */
109   static getOpenTabUrls(isInPrivateWindow = false) {
110     let uniqueUrls = new Map();
111     if (isInPrivateWindow) {
112       let urls = UrlbarProviderOpenTabs.getOpenTabUrlsForUserContextId(
113         PRIVATE_USER_CONTEXT_ID,
114         true
115       );
116       for (let url of urls) {
117         uniqueUrls.set(url, new Set([PRIVATE_USER_CONTEXT_ID]));
118       }
119     } else {
120       for (let [userContextId, urls] of gOpenTabUrls) {
121         if (userContextId == PRIVATE_USER_CONTEXT_ID) {
122           continue;
123         }
124         for (let url of urls.keys()) {
125           let userContextIds = uniqueUrls.get(url);
126           if (!userContextIds) {
127             userContextIds = new Set();
128             uniqueUrls.set(url, userContextIds);
129           }
130           userContextIds.add(userContextId);
131         }
132       }
133     }
134     return uniqueUrls;
135   }
137   /**
138    * Return urls registered in the memory table.
139    * This is mostly for testing purposes.
140    *
141    * @returns {Array} Array of {url, userContextId, count} objects.
142    */
143   static async getDatabaseRegisteredOpenTabsForTests() {
144     let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
145     let rows = await conn.execute(
146       "SELECT url, userContextId, open_count FROM moz_openpages_temp"
147     );
148     return rows.map(r => ({
149       url: r.getResultByIndex(0),
150       userContextId: r.getResultByIndex(1),
151       count: r.getResultByIndex(2),
152     }));
153   }
155   /**
156    * Return userContextId that is used in the moz_openpages_temp table and
157    * returned as part of the payload. It differs only for private windows.
158    *
159    * @param {integer} userContextId Containers user context id
160    * @param {boolean} isInPrivateWindow In private browsing window or not
161    * @returns {interger} userContextId
162    */
163   static getUserContextIdForOpenPagesTable(userContextId, isInPrivateWindow) {
164     return isInPrivateWindow ? PRIVATE_USER_CONTEXT_ID : userContextId;
165   }
167   /**
168    * Return whether the provided userContextId is for a non-private tab.
169    *
170    * @param {integer} userContextId the userContextId to evaluate
171    * @returns {boolean}
172    */
173   static isNonPrivateUserContextId(userContextId) {
174     return userContextId != PRIVATE_USER_CONTEXT_ID;
175   }
177   /**
178    * Return whether the provided userContextId is for a container.
179    *
180    * @param {integer} userContextId the userContextId to evaluate
181    * @returns {boolean}
182    */
183   static isContainerUserContextId(userContextId) {
184     return userContextId > 0;
185   }
187   /**
188    * Copy over cached open tabs to the memory table once the Urlbar
189    * connection has been initialized.
190    */
191   static promiseDBPopulated =
192     lazy.PlacesUtils.largeCacheDBConnDeferred.promise.then(async () => {
193       // Must be set before populating.
194       UrlbarProviderOpenTabs.memoryTableInitialized = true;
195       // Populate the table with the current cached tabs.
196       for (let [userContextId, entries] of gOpenTabUrls) {
197         for (let [url, count] of entries) {
198           await addToMemoryTable(url, userContextId, count).catch(
199             console.error
200           );
201         }
202       }
203     });
205   /**
206    * Registers a tab as open.
207    *
208    * @param {string} url Address of the tab
209    * @param {integer|string} userContextId Containers user context id
210    * @param {boolean} isInPrivateWindow In private browsing window or not
211    */
212   static async registerOpenTab(url, userContextId, isInPrivateWindow) {
213     // It's fairly common to retrieve the value from an HTML attribute, that
214     // means we're getting sometimes a string, sometimes an integer. As we're
215     // using this as key of a Map, we must treat it consistently.
216     userContextId = parseInt(userContextId);
217     if (!Number.isInteger(userContextId)) {
218       lazy.logger.error("Invalid userContextId while registering openTab: ", {
219         url,
220         userContextId,
221         isInPrivateWindow,
222       });
223       return;
224     }
225     lazy.logger.info("Registering openTab: ", {
226       url,
227       userContextId,
228       isInPrivateWindow,
229     });
230     userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
231       userContextId,
232       isInPrivateWindow
233     );
235     let entries = gOpenTabUrls.get(userContextId);
236     if (!entries) {
237       entries = new Map();
238       gOpenTabUrls.set(userContextId, entries);
239     }
240     entries.set(url, (entries.get(url) ?? 0) + 1);
241     await addToMemoryTable(url, userContextId).catch(console.error);
242   }
244   /**
245    * Unregisters a previously registered open tab.
246    *
247    * @param {string} url Address of the tab
248    * @param {integer|string} userContextId Containers user context id
249    * @param {boolean} isInPrivateWindow In private browsing window or not
250    */
251   static async unregisterOpenTab(url, userContextId, isInPrivateWindow) {
252     // It's fairly common to retrieve the value from an HTML attribute, that
253     // means we're getting sometimes a string, sometimes an integer. As we're
254     // using this as key of a Map, we must treat it consistently.
255     userContextId = parseInt(userContextId);
256     lazy.logger.info("Unregistering openTab: ", {
257       url,
258       userContextId,
259       isInPrivateWindow,
260     });
261     userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
262       userContextId,
263       isInPrivateWindow
264     );
266     let entries = gOpenTabUrls.get(userContextId);
267     if (entries) {
268       let oldCount = entries.get(url);
269       if (oldCount == 0) {
270         console.error("Tried to unregister a non registered open tab");
271         return;
272       }
273       if (oldCount == 1) {
274         entries.delete(url);
275         // Note: `entries` might be an empty Map now, though we don't remove it
276         // from `gOpenTabUrls` as it's likely to be reused later.
277       } else {
278         entries.set(url, oldCount - 1);
279       }
280       await removeFromMemoryTable(url, userContextId).catch(console.error);
281     }
282   }
284   /**
285    * Starts querying.
286    *
287    * @param {object} queryContext The query context object
288    * @param {Function} addCallback Callback invoked by the provider to add a new
289    *        match.
290    * @returns {Promise} resolved when the query stops.
291    */
292   async startQuery(queryContext, addCallback) {
293     // Note: this is not actually expected to be used as an internal provider,
294     // because normal history search will already coalesce with the open tabs
295     // temp table to return proper frecency.
296     // TODO:
297     //  * properly search and handle tokens, this is just a mock for now.
298     let instance = this.queryInstance;
299     let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
300     await UrlbarProviderOpenTabs.promiseDBPopulated;
301     await conn.executeCached(
302       `
303       SELECT url, userContextId
304       FROM moz_openpages_temp
305     `,
306       {},
307       (row, cancel) => {
308         if (instance != this.queryInstance) {
309           cancel();
310           return;
311         }
312         addCallback(
313           this,
314           new lazy.UrlbarResult(
315             UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
316             UrlbarUtils.RESULT_SOURCE.TABS,
317             {
318               url: row.getResultByName("url"),
319               userContextId: row.getResultByName("userContextId"),
320             }
321           )
322         );
323       }
324     );
325   }
329  * Adds an open page to the memory table.
331  * @param {string} url Address of the page
332  * @param {number} userContextId Containers user context id
333  * @param {number} [count] The number of times the page is open
334  * @returns {Promise} resolved after the addition.
335  */
336 async function addToMemoryTable(url, userContextId, count = 1) {
337   if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
338     return;
339   }
340   await lazy.UrlbarProvidersManager.runInCriticalSection(async () => {
341     let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
342     await conn.executeCached(
343       `
344       INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
345       VALUES ( :url,
346                 :userContextId,
347                 IFNULL( ( SELECT open_count + 1
348                           FROM moz_openpages_temp
349                           WHERE url = :url
350                           AND userContextId = :userContextId ),
351                         :count
352                       )
353               )
354     `,
355       { url, userContextId, count }
356     );
357   });
361  * Removes an open page from the memory table.
363  * @param {string} url Address of the page
364  * @param {number} userContextId Containers user context id
365  * @returns {Promise} resolved after the removal.
366  */
367 async function removeFromMemoryTable(url, userContextId) {
368   if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
369     return;
370   }
371   await lazy.UrlbarProvidersManager.runInCriticalSection(async () => {
372     let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
373     await conn.executeCached(
374       `
375       UPDATE moz_openpages_temp
376       SET open_count = open_count - 1
377       WHERE url = :url
378         AND userContextId = :userContextId
379     `,
380       { url, userContextId }
381     );
382   });