Bug 1495183 - Create a first muxer implementation. r=adw

Differential Revision: https://phabricator.services.mozilla.com/D13552

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Marco Bonardo 2018-12-02 09:58:15 +00:00
Родитель 2edebf41a2
Коммит 70a7e4ce9e
7 изменённых файлов: 262 добавлений и 10 удалений

Просмотреть файл

@ -0,0 +1,87 @@
/* 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 sort matches in a QueryContext.
*/
var EXPORTED_SYMBOLS = ["UrlbarMuxerUnifiedComplete"];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
Log: "resource://gre/modules/Log.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});
XPCOMUtils.defineLazyGetter(this, "logger", () =>
Log.repository.getLogger("Places.Urlbar.UrlbarMuxerUnifiedComplete"));
const MATCH_TYPE_TO_GROUP = new Map([
[ UrlbarUtils.MATCH_TYPE.TAB_SWITCH, UrlbarUtils.MATCH_GROUP.GENERAL ],
[ UrlbarUtils.MATCH_TYPE.SEARCH, UrlbarUtils.MATCH_GROUP.SUGGESTION ],
[ UrlbarUtils.MATCH_TYPE.URL, UrlbarUtils.MATCH_GROUP.GENERAL ],
[ UrlbarUtils.MATCH_TYPE.KEYWORD, UrlbarUtils.MATCH_GROUP.GENERAL ],
[ UrlbarUtils.MATCH_TYPE.OMNIBOX, UrlbarUtils.MATCH_GROUP.EXTENSION ],
[ UrlbarUtils.MATCH_TYPE.REMOTE_TAB, UrlbarUtils.MATCH_GROUP.GENERAL ],
]);
/**
* Class used to create a muxer.
* The muxer receives and sorts matches in a QueryContext.
*/
class MuxerUnifiedComplete {
constructor() {
// Nothing.
}
get name() {
return "MuxerUnifiedComplete";
}
/**
* Sorts matches in the given QueryContext.
* @param {object} context a QueryContext
*/
sort(context) {
if (!context.results.length) {
return;
}
// Check the first match, if it's a preselected search match, use search buckets.
let firstMatch = context.results[0];
let buckets = context.preselected &&
firstMatch.type == UrlbarUtils.MATCH_TYPE.SEARCH ?
UrlbarPrefs.get("matchBucketsSearch") :
UrlbarPrefs.get("matchBuckets");
logger.debug(`Buckets: ${buckets}`);
let sortedMatches = [];
for (let [group, count] of buckets) {
// Search all the available matches and fill this bucket.
for (let match of context.results) {
if (count == 0) {
// There's no more space in this bucket.
break;
}
// Handle the heuristic result.
if (group == UrlbarUtils.MATCH_GROUP.HEURISTIC &&
match == firstMatch && context.preselected) {
sortedMatches.push(match);
count--;
} else if (group == MATCH_TYPE_TO_GROUP.get(match.type)) {
sortedMatches.push(match);
count--;
} else {
let errorMsg = `Match type ${match.type} is not mapped to a match group.`;
logger.error(errorMsg);
Cu.reportError(errorMsg);
}
}
}
}
}
var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();

Просмотреть файл

@ -29,6 +29,11 @@ var localProviderModules = {
UrlbarProviderUnifiedComplete: "resource:///modules/UrlbarProviderUnifiedComplete.jsm",
};
// List of available local muxers, each is implemented in its own jsm module.
var localMuxerModules = {
UrlbarMuxerUnifiedComplete: "resource:///modules/UrlbarMuxerUnifiedComplete.jsm",
};
// To improve dataflow and reduce UI work, when a match is added by a
// non-immediate provider, we notify it to the controller after a delay, so
// that we can chunk matches coming in that timeframe into a single call.
@ -59,6 +64,13 @@ class ProvidersManager {
// running a query that shouldn't be interrupted, and if so it should
// bump this through disableInterrupt and enableInterrupt.
this.interruptLevel = 0;
// This maps muxer names to muxers.
this.muxers = new Map();
for (let [symbol, module] of Object.entries(localMuxerModules)) {
let {[symbol]: muxer} = ChromeUtils.import(module, {});
this.registerMuxer(muxer);
}
}
/**
@ -66,10 +78,15 @@ class ProvidersManager {
* @param {object} provider
*/
registerProvider(provider) {
logger.info(`Registering provider ${provider.name}`);
if (!provider || !provider.name ||
(typeof provider.startQuery != "function") ||
(typeof provider.cancelQuery != "function")) {
throw new Error(`Trying to register an invalid provider`);
}
if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
throw new Error(`Unknown provider type ${provider.type}`);
}
logger.info(`Registering provider ${provider.name}`);
this.providers.get(provider.type).set(provider.name, provider);
}
@ -82,6 +99,28 @@ class ProvidersManager {
this.providers.get(provider.type).delete(provider.name);
}
/**
* Registers a muxer object with the manager.
* @param {object} muxer a UrlbarMuxer object
*/
registerMuxer(muxer) {
if (!muxer || !muxer.name || (typeof muxer.sort != "function")) {
throw new Error(`Trying to register an invalid muxer`);
}
logger.info(`Registering muxer ${muxer.name}`);
this.muxers.set(muxer.name, muxer);
}
/**
* Unregisters a previously registered muxer object.
* @param {object} muxer a UrlbarMuxer object or name.
*/
unregisterMuxer(muxer) {
let muxerName = typeof muxer == "string" ? muxer : muxer.name;
logger.info(`Unregistering muxer ${muxerName}`);
this.muxers.delete(muxerName);
}
/**
* Starts querying.
* @param {object} queryContext The query context object
@ -89,7 +128,13 @@ class ProvidersManager {
*/
async startQuery(queryContext, controller) {
logger.info(`Query start ${queryContext.searchString}`);
let query = new Query(queryContext, controller, this.providers);
let muxerName = queryContext.muxer || "MuxerUnifiedComplete";
logger.info(`Using muxer ${muxerName}`);
let muxer = this.muxers.get(muxerName);
if (!muxer) {
throw new Error(`Muxer with name ${muxerName} not found`);
}
let query = new Query(queryContext, controller, muxer, this.providers);
this.queries.set(queryContext, query);
await query.start();
}
@ -145,12 +190,15 @@ class Query {
* The query context
* @param {object} controller
* The controller to be notified
* @param {object} muxer
* The muxer to sort matches
* @param {object} providers
* Map of all the providers by type and name
*/
constructor(queryContext, controller, providers) {
constructor(queryContext, controller, muxer, providers) {
this.context = queryContext;
this.context.results = [];
this.muxer = muxer;
this.controller = controller;
this.providers = providers;
this.started = false;
@ -267,8 +315,7 @@ class Query {
this._chunkTimer.cancel().catch(Cu.reportError);
delete this._chunkTimer;
}
// TODO:
// * pass results to a muxer before sending them back to the controller.
this.muxer.sort(this.context);
this.controller.receiveResults(this.context);
};

Просмотреть файл

@ -9,6 +9,7 @@ EXTRA_JS_MODULES += [
'UrlbarController.jsm',
'UrlbarInput.jsm',
'UrlbarMatch.jsm',
'UrlbarMuxerUnifiedComplete.jsm',
'UrlbarPrefs.jsm',
'UrlbarProviderOpenTabs.jsm',
'UrlbarProvidersManager.jsm',

Просмотреть файл

@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_muxer() {
Assert.throws(() => UrlbarProvidersManager.registerMuxer(),
/invalid muxer/,
"Should throw with no arguments");
Assert.throws(() => UrlbarProvidersManager.registerMuxer({}),
/invalid muxer/,
"Should throw with empty object");
Assert.throws(() => UrlbarProvidersManager.registerMuxer({
name: "",
}),
/invalid muxer/,
"Should throw with empty name");
Assert.throws(() => UrlbarProvidersManager.registerMuxer({
name: "test",
sort: "no",
}),
/invalid muxer/,
"Should throw with invalid sort");
let matches = [
new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
UrlbarUtils.MATCH_SOURCE.TABS,
{ url: "http://mozilla.org/tab/" }),
new UrlbarMatch(UrlbarUtils.MATCH_TYPE.URL,
UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
{ url: "http://mozilla.org/bookmark/" }),
new UrlbarMatch(UrlbarUtils.MATCH_TYPE.URL,
UrlbarUtils.MATCH_SOURCE.HISTORY,
{ url: "http://mozilla.org/history/" }),
];
registerBasicTestProvider(matches);
let context = createContext();
let controller = new UrlbarController({
browserWindow: {
location: {
href: AppConstants.BROWSER_CHROME_URL,
},
},
});
let muxer = {
get name() {
return "TestMuxer";
},
sort(queryContext) {
queryContext.results.sort((a, b) => {
if (b.source == UrlbarUtils.MATCH_SOURCE.TABS) {
return -1;
}
if (b.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS) {
return 1;
}
return a.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS ? -1 : 1;
});
},
};
UrlbarProvidersManager.registerMuxer(muxer);
context.muxer = "TestMuxer";
info("Check results, the order should be: bookmark, history, tab");
await UrlbarProvidersManager.startQuery(context, controller);
Assert.deepEqual(context.results, [
matches[1],
matches[2],
matches[0],
]);
// Sanity check, should not throw.
UrlbarProvidersManager.unregisterMuxer(muxer);
UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op.
});

Просмотреть файл

@ -4,6 +4,31 @@
"use strict";
add_task(async function test_providers() {
Assert.throws(() => UrlbarProvidersManager.registerProvider(),
/invalid provider/,
"Should throw with no arguments");
Assert.throws(() => UrlbarProvidersManager.registerProvider({}),
/invalid provider/,
"Should throw with empty object");
Assert.throws(() => UrlbarProvidersManager.registerProvider({
name: "",
}),
/invalid provider/,
"Should throw with empty name");
Assert.throws(() => UrlbarProvidersManager.registerProvider({
name: "test",
startQuery: "no",
}),
/invalid provider/,
"Should throw with invalid startQuery");
Assert.throws(() => UrlbarProvidersManager.registerProvider({
name: "test",
startQuery: () => {},
cancelQuery: "no",
}),
/invalid provider/,
"Should throw with invalid cancelQuery");
let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
UrlbarUtils.MATCH_SOURCE.TABS,
{ url: "http://mozilla.org/foo/" });

Просмотреть файл

@ -2,6 +2,7 @@
head = head.js
firefox-appdir = browser
[test_muxer.js]
[test_providerOpenTabs.js]
[test_providersManager.js]
[test_providersManager_filtering.js]

Просмотреть файл

@ -65,6 +65,10 @@ It is augmented as it progresses through the system, with various information:
isPrivate; // {boolean} Whether the search started in a private context.
userContextId; // {integer} The user context ID (containers feature).
// Optional properties.
muxer; // Name of a registered muxer. Muxers can be registered through the
// UrlbarProvidersManager
// Properties added by the Model.
tokens; // {array} tokens extracted from the searchString, each token is an
// object in the form {type, value}.
@ -88,9 +92,10 @@ startup and can register/unregister providers on the fly.
It can manage multiple concurrent queries, and tracks them internally as
separate *Query* objects.
The *Controller* starts and stops queries through the *ProvidersManager*. It's
possible to wait for the promise returned by *startQuery* to know when no more
matches will be returned, it is not mandatory though. Queries can be canceled.
The *Controller* starts and stops queries through the *UrlbarProvidersManager*.
It's possible to wait for the promise returned by *startQuery* to know when no
more matches will be returned, it is not mandatory though.
Queries can be canceled.
.. note::
@ -114,6 +119,8 @@ used by the user to restrict the search to specific type of matches (See the
UrlbarProvidersManager {
registerProvider(providerObj);
unregisterProvider(providerObj);
registerMuxer(muxerObj);
unregisterMuxer(muxerObjOrName);
async startQuery(queryContext);
cancelQuery(queryContext);
// Can be used by providers to run uninterruptible queries.
@ -160,14 +167,22 @@ UrlbarMuxer
-----------
The *Muxer* is responsible for sorting matches based on their importance and
additional rules that depend on the QueryContext.
additional rules that depend on the QueryContext. The muxer to use is indicated
by the QueryContext.muxer property.
.. caution
The Muxer is a replaceable component, as such what is described here is a
reference for the default View, but may not be valid for other implementations.
*Content to be written*
.. highlight:: JavaScript
.. code:
UrlbarMuxer {
name; // {string} A simple name to track the provider.
// Invoked by the ProvidersManager to sort matches.
sort(queryContext);
}
The Controller