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/. */
6 * This module exports a provider, returning open tabs matches for the urlbar.
7 * It is also used to register and unregister open tabs.
13 } from "resource:///modules/UrlbarUtils.sys.mjs";
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",
23 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
24 UrlbarUtils.getLogger({ prefix: "Provider.OpenTabs" })
27 const PRIVATE_USER_CONTEXT_ID = -1;
30 * Maps the open tabs by userContextId.
31 * Each entry is a Map of url => count.
33 var gOpenTabUrls = new Map();
36 * Class used to create the provider.
38 export class UrlbarProviderOpenTabs extends UrlbarProvider {
44 * Returns the name of this provider.
46 * @returns {string} the name of this provider.
53 * Returns the type of this provider.
55 * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
58 return UrlbarUtils.PROVIDER_TYPE.PROFILE;
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.
66 * @returns {boolean} Whether this provider should be invoked for the search.
69 // For now we don't actually use this provider to query open tabs, instead
70 // we join the temp table in UrlbarProviderPlaces.
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
79 static memoryTableInitialized = false;
82 * Return unique urls that are open for given user context id.
84 * @param {integer|string} userContextId Containers user context id
85 * @param {boolean} [isInPrivateWindow] In private browsing window or not
86 * @returns {Array} urls
88 static getOpenTabUrlsForUserContextId(
90 isInPrivateWindow = false
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(
100 return Array.from(gOpenTabUrls.get(userContextId)?.keys() ?? []);
104 * Return unique urls that are open, along with their user context id.
106 * @param {boolean} [isInPrivateWindow] Whether it's for a private browsing window
107 * @returns {Map} { url => Set({userContextIds}) }
109 static getOpenTabUrls(isInPrivateWindow = false) {
110 let uniqueUrls = new Map();
111 if (isInPrivateWindow) {
112 let urls = UrlbarProviderOpenTabs.getOpenTabUrlsForUserContextId(
113 PRIVATE_USER_CONTEXT_ID,
116 for (let url of urls) {
117 uniqueUrls.set(url, new Set([PRIVATE_USER_CONTEXT_ID]));
120 for (let [userContextId, urls] of gOpenTabUrls) {
121 if (userContextId == PRIVATE_USER_CONTEXT_ID) {
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);
130 userContextIds.add(userContextId);
138 * Return urls registered in the memory table.
139 * This is mostly for testing purposes.
141 * @returns {Array} Array of {url, userContextId, count} objects.
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"
148 return rows.map(r => ({
149 url: r.getResultByIndex(0),
150 userContextId: r.getResultByIndex(1),
151 count: r.getResultByIndex(2),
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.
159 * @param {integer} userContextId Containers user context id
160 * @param {boolean} isInPrivateWindow In private browsing window or not
161 * @returns {interger} userContextId
163 static getUserContextIdForOpenPagesTable(userContextId, isInPrivateWindow) {
164 return isInPrivateWindow ? PRIVATE_USER_CONTEXT_ID : userContextId;
168 * Return whether the provided userContextId is for a non-private tab.
170 * @param {integer} userContextId the userContextId to evaluate
173 static isNonPrivateUserContextId(userContextId) {
174 return userContextId != PRIVATE_USER_CONTEXT_ID;
178 * Return whether the provided userContextId is for a container.
180 * @param {integer} userContextId the userContextId to evaluate
183 static isContainerUserContextId(userContextId) {
184 return userContextId > 0;
188 * Copy over cached open tabs to the memory table once the Urlbar
189 * connection has been initialized.
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(
206 * Registers a tab as open.
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
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: ", {
225 lazy.logger.info("Registering openTab: ", {
230 userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
235 let entries = gOpenTabUrls.get(userContextId);
238 gOpenTabUrls.set(userContextId, entries);
240 entries.set(url, (entries.get(url) ?? 0) + 1);
241 await addToMemoryTable(url, userContextId).catch(console.error);
245 * Unregisters a previously registered open tab.
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
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: ", {
261 userContextId = UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
266 let entries = gOpenTabUrls.get(userContextId);
268 let oldCount = entries.get(url);
270 console.error("Tried to unregister a non registered open tab");
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.
278 entries.set(url, oldCount - 1);
280 await removeFromMemoryTable(url, userContextId).catch(console.error);
287 * @param {object} queryContext The query context object
288 * @param {Function} addCallback Callback invoked by the provider to add a new
290 * @returns {Promise} resolved when the query stops.
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.
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(
303 SELECT url, userContextId
304 FROM moz_openpages_temp
308 if (instance != this.queryInstance) {
314 new lazy.UrlbarResult(
315 UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
316 UrlbarUtils.RESULT_SOURCE.TABS,
318 url: row.getResultByName("url"),
319 userContextId: row.getResultByName("userContextId"),
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.
336 async function addToMemoryTable(url, userContextId, count = 1) {
337 if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
340 await lazy.UrlbarProvidersManager.runInCriticalSection(async () => {
341 let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
342 await conn.executeCached(
344 INSERT OR REPLACE INTO moz_openpages_temp (url, userContextId, open_count)
347 IFNULL( ( SELECT open_count + 1
348 FROM moz_openpages_temp
350 AND userContextId = :userContextId ),
355 { url, userContextId, count }
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.
367 async function removeFromMemoryTable(url, userContextId) {
368 if (!UrlbarProviderOpenTabs.memoryTableInitialized) {
371 await lazy.UrlbarProvidersManager.runInCriticalSection(async () => {
372 let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
373 await conn.executeCached(
375 UPDATE moz_openpages_temp
376 SET open_count = open_count - 1
378 AND userContextId = :userContextId
380 { url, userContextId }