gecko-dev/browser/components/urlbar/UrlbarProvidersManager.jsm

238 строки
7.4 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This module exports a component used to register search providers and manage
* the connection between such providers and a UrlbarController.
*/
var EXPORTED_SYMBOLS = ["UrlbarProvidersManager"];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
Log: "resource://gre/modules/Log.jsm",
PlacesUtils: "resource://modules/PlacesUtils.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});
XPCOMUtils.defineLazyGetter(this, "logger", () =>
Log.repository.getLogger("Places.Urlbar.ProvidersManager"));
// List of available local providers, each is implemented in its own jsm module
// and will track different queries internally by queryContext.
var localProviderModules = {
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
};
/**
* Class used to create a manager.
* The manager is responsible to keep a list of providers, instantiate query
* objects and pass those to the providers.
*/
class ProvidersManager {
constructor() {
// Tracks the available providers.
// This is a double map, first it maps by PROVIDER_TYPE, then
// registerProvider maps by provider.name: { type: { name: provider }}
this.providers = new Map();
for (let type of Object.values(UrlbarUtils.PROVIDER_TYPE)) {
this.providers.set(type, new Map());
}
for (let [symbol, module] of Object.entries(localProviderModules)) {
let {[symbol]: provider} = ChromeUtils.import(module, {});
this.registerProvider(provider);
}
// Tracks ongoing Query instances by queryContext.
this.queries = new Map();
// Interrupt() allows to stop any running SQL query, some provider may be
// running a query that shouldn't be interrupted, and if so it should
// bump this through disableInterrupt and enableInterrupt.
this.interruptLevel = 0;
}
/**
* Registers a provider object with the manager.
* @param {object} provider
*/
registerProvider(provider) {
logger.info(`Registering provider ${provider.name}`);
if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
throw new Error(`Unknown provider type ${provider.type}`);
}
this.providers.get(provider.type).set(provider.name, provider);
}
/**
* Unregisters a previously registered provider object.
* @param {object} provider
*/
unregisterProvider(provider) {
logger.info(`Unregistering provider ${provider.name}`);
this.providers.get(provider.type).delete(provider.name);
}
/**
* Starts querying.
* @param {object} queryContext The query context object
* @param {object} controller a UrlbarController instance
*/
async startQuery(queryContext, controller) {
logger.info(`Query start ${queryContext.searchString}`);
let query = Object.seal(new Query(queryContext, controller, this.providers));
this.queries.set(queryContext, query);
await query.start();
}
/**
* Cancels a running query.
* @param {object} queryContext
*/
cancelQuery(queryContext) {
logger.info(`Query cancel ${queryContext.searchString}`);
let query = this.queries.get(queryContext);
if (!query) {
throw new Error("Couldn't find a matching query for the given context");
}
query.cancel();
if (!this.interruptLevel) {
try {
let db = PlacesUtils.promiseLargeCacheDBConnection();
db.interrupt();
} catch (ex) {}
}
this.queries.delete(queryContext);
}
/**
* A provider can use this util when it needs to run a SQL query that can't
* be interrupted. Otherwise, when a query is canceled any running SQL query
* is interrupted abruptly.
* @param {function} taskFn a Task to execute in the critical section.
*/
async runInCriticalSection(taskFn) {
this.interruptLevel++;
try {
await taskFn();
} finally {
this.interruptLevel--;
}
}
}
var UrlbarProvidersManager = new ProvidersManager();
/**
* Tracks a query status.
* Multiple queries can potentially be executed at the same time by different
* controllers. Each query has to track its own status and delays separately,
* to avoid conflicting with other ones.
*/
class Query {
/**
* Initializes the query object.
* @param {object} queryContext
* The query context
* @param {object} controller
* The controller to be notified
* @param {object} providers
* Map of all the providers by type and name
*/
constructor(queryContext, controller, providers) {
this.context = queryContext;
this.context.results = [];
this.controller = controller;
this.providers = providers;
// Track the delay timer.
this.sleepResolve = Promise.resolve();
this.sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.started = false;
this.canceled = false;
this.complete = false;
}
/**
* Starts querying.
*/
async start() {
if (this.started) {
throw new Error("This Query has been started already");
}
this.started = true;
UrlbarTokenizer.tokenize(this.context);
let promises = [];
for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
if (this.canceled) {
break;
}
promises.push(provider.startQuery(this.context, this.add));
}
await new Promise(resolve => {
let time = UrlbarPrefs.get("delay");
this.sleepResolve = resolve;
this.sleepTimer.initWithCallback(resolve, time, Ci.nsITimer.TYPE_ONE_SHOT);
});
for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
UrlbarUtils.PROVIDER_TYPE.PROFILE,
UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
for (let provider of this.providers.get(providerType).values()) {
if (this.canceled) {
break;
}
promises.push(provider.startQuery(this.context, this.add.bind(this)));
}
}
await Promise.all(promises.map(p => p.catch(Cu.reportError)));
// Nothing should be failing above, since we catch all the promises, thus
// this is not in a finally for now.
this.complete = true;
}
/**
* Cancels this query.
* @note Invoking cancel multiple times is a no-op.
*/
cancel() {
if (this.canceled) {
return;
}
this.canceled = true;
this.sleepTimer.cancel();
for (let providers of this.providers.values()) {
for (let provider of providers.values()) {
provider.cancelQuery(this.context);
}
}
this.sleepResolve();
}
/**
* Adds a match returned from a provider to the results set.
* @param {object} provider
* @param {object} match
*/
add(provider, match) {
// Stop returning results as soon as we've been canceled.
if (this.canceled) {
return;
}
// TODO:
// * coalesce results in timed chunks: we don't want to notify every single
// result as soon as it arrives, we'll rather collect results for a few
// ms, then send them
// * pass results to a muxer before sending them back to the controller.
this.context.results.push(match);
this.controller.receiveResults(this.context);
}
}