The view's synchronous per-result data -- a dynamic result's view template and a result's menu commands -- was fetched from the result's provider on demand, which the message path can't do when the provider lives in another process. It worked around this by pre-fetching the data with each QueryResults notification and stashing it on a non-enumerable `viewData` expando that the view read back. Compute it eagerly instead, when the result is finalized in UrlbarProvidersManager, and store it on UrlbarResult as `viewTemplate` and `commands`. Both are serialized by toWire()/fromWire(), so the view reads them synchronously on both transports without a round-trip or the expando (and its deepEqual/serialization hazard). getViewUpdate stays dynamic and round-trips. Static per-result data a provider parked in getViewUpdate moves to the baked template too: RealtimeSuggestProvider set the result's navigation url/query on the row's dataset from getViewUpdate, so on the message path a click resolved no URL until that async update landed. Baking the dataset into getViewTemplate makes it present when the row is built; only genuinely dynamic per-item data (text, images, aria group) stays in getViewUpdate. Because the commands are baked, a provider that changes them while the result stays in the view must refresh the baked value, not just drop the view's command cache -- reaching the "show less frequently" cap removes that command, and the stale baked commands would otherwise keep it. Consolidate the show-less-frequently handling each suggestion provider inlined into a shared SuggestProvider.handleShowLessFrequently that re-bakes the result's commands, so the refresh lives in one place rather than being repeated (and missable) per provider. Differential Revision: https://phabricator.services.mozilla.com/D310082
470 lines
16 KiB
JavaScript
470 lines
16 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/. */
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs",
|
|
UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
|
|
UrlbarShared: "chrome://browser/content/urlbar/UrlbarShared.mjs",
|
|
});
|
|
|
|
/**
|
|
* Base class for Suggest features. It can be extended to implement a feature
|
|
* that should be enabled only when Suggest is enabled. Most features should
|
|
* extend one of the `SuggestFeature` subclasses, however.
|
|
*
|
|
* Subclasses should be registered with `QuickSuggest` by adding them to the
|
|
* `FEATURES` const in `QuickSuggest.sys.mjs`.
|
|
*/
|
|
export class SuggestFeature {
|
|
// Methods designed for overriding below
|
|
|
|
/**
|
|
* @returns {Array}
|
|
* If the feature is conditioned on any prefs or Nimbus variables, the
|
|
* subclass should override this getter and return their names in this
|
|
* array. When one of these prefs or variables changes, Suggest will call
|
|
* `feature.update()`, which checks `feature.shouldEnable`. If the value of
|
|
* `shouldEnable` is different from the feature's current enabled status,
|
|
* then `feature.update()` calls `feature.enable()`.
|
|
*
|
|
* Pref and variable names should be recognized by `UrlbarPrefs`. i.e., pref
|
|
* names should be relative to the `browser.urlbar.` branch. For Nimbus
|
|
* variables with fallback prefs, include only the variable name.
|
|
*/
|
|
get enablingPreferences() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @returns {Array}
|
|
* If there are any feature-specific prefs that are exposed to the user and
|
|
* allow the feature to be toggled on or off, the subclass should override
|
|
* this getter and return their names. They should also be included in
|
|
* `enablingPreferences`. The names should be recognized by `UrlbarPrefs`,
|
|
* i.e., they should be relative to the `browser.urlbar.` branch.
|
|
*
|
|
* If the feature is a `SuggestProvider`, typically this should include the
|
|
* pref that's named `suggest.mySuggestionType` and set to `false` when the
|
|
* user dismisses the entire suggestion type, i.e., the relevant
|
|
* These prefs should be controlled by the user, so they should never
|
|
* include the feature's `featureGate` pref.
|
|
*
|
|
* These prefs should control this feature specifically, so they should
|
|
* never include `suggest.quicksuggest.all` or
|
|
* `suggest.quicksuggest.sponsored`. If the feature has no such prefs,
|
|
* this getter should return an empty array.
|
|
*/
|
|
get primaryUserControlledPreferences() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
* Whether the feature should be enabled, assuming Suggest is enabled. This
|
|
* base implementation returns true if every pref in `enablingPreferences`
|
|
* is truthy. The subclass should override it if it needs different logic.
|
|
*
|
|
* This getter will be called only when Suggest is enabled.
|
|
*/
|
|
get shouldEnable() {
|
|
return this.enablingPreferences.every(p => lazy.UrlbarPrefs.get(p));
|
|
}
|
|
|
|
/**
|
|
* This method should initialize or uninitialize any state related to the
|
|
* feature. It will only be called when the enabled status changes, i.e., when
|
|
* it goes from false to true or true to false.
|
|
*
|
|
* @param {boolean} enabled
|
|
* Whether the feature should be enabled or not.
|
|
*/
|
|
enable(enabled) {}
|
|
|
|
// Methods not designed for overriding below
|
|
|
|
/**
|
|
* @returns {Console}
|
|
* The feature's logger.
|
|
*/
|
|
get logger() {
|
|
if (!this._logger) {
|
|
this._logger = lazy.UrlbarShared.getLogger({
|
|
prefix: `QuickSuggest.${this.name}`,
|
|
});
|
|
}
|
|
return this._logger;
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
* Whether the feature is enabled. The enabled status is automatically
|
|
* managed by `QuickSuggest` and subclasses should not override this.
|
|
*/
|
|
get isEnabled() {
|
|
return this.#isEnabled;
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
* The feature's name.
|
|
*/
|
|
get name() {
|
|
return this.constructor.name;
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the feature according to `shouldEnable` and whether
|
|
* Suggest is enabled. If the feature's enabled status changes, `enable()` is
|
|
* called with the new status; otherwise `enable()` is not called.
|
|
*/
|
|
update() {
|
|
let enable =
|
|
lazy.UrlbarPrefs.get("quickSuggestEnabled") && this.shouldEnable;
|
|
if (enable != this.isEnabled) {
|
|
this.logger.info("Feature enabled status changed", {
|
|
nowEnabled: enable,
|
|
});
|
|
this.#isEnabled = enable;
|
|
this.enable(enable);
|
|
}
|
|
}
|
|
|
|
#isEnabled = false;
|
|
}
|
|
|
|
/**
|
|
* Base class for Suggest features that manage a suggestion type [1].
|
|
*
|
|
* The same suggestion type can be served by multiple backends, and a single
|
|
* `SuggestProvider` subclass can manage the type regardless of backend by
|
|
* overriding the appropriate methods and getters.
|
|
*
|
|
* Subclasses should be registered with `QuickSuggest` by adding them to the
|
|
* `FEATURES` const in `QuickSuggest.sys.mjs`.
|
|
*
|
|
* [1] Typically a feature should manage only one type. In rare cases, it might
|
|
* make sense to manage multiple types, for example when a single Merino
|
|
* provider serves more than one type of suggestion.
|
|
*/
|
|
export class SuggestProvider extends SuggestFeature {
|
|
// Methods designed for overriding below
|
|
|
|
/**
|
|
* @returns {string}
|
|
* If the feature's suggestions are served by Merino, the subclass should
|
|
* override this getter and return the name of the Merino provider that
|
|
* serves them.
|
|
*/
|
|
get merinoProvider() {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
* If the feature's suggestions are served by the Rust component, the
|
|
* subclass should override this getter and return their type name as
|
|
* defined by the `Suggestion` enum in the component. e.g., "Amp",
|
|
* "Wikipedia", "Mdn", etc.
|
|
*/
|
|
get rustSuggestionType() {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* @returns {Array}
|
|
* If the feature manages dynamic Rust suggestions, its `rustSuggestionType`
|
|
* getter should return "Dynamic", and it should override
|
|
* `dynamicRustSuggestionTypes` to return an array of the dynamic type
|
|
* names as defined by `suggestion_type` in the remote settings records.
|
|
*/
|
|
get dynamicRustSuggestionTypes() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @returns {object|null}
|
|
* If the feature manages suggestions served by the Rust component that
|
|
* require provider constraints, the subclass should override this getter
|
|
* and return a plain JS object that can be passed to
|
|
* `SuggestionProviderConstraints()`. This getter will only be called if the
|
|
* feature is enabled.
|
|
*/
|
|
get rustProviderConstraints() {
|
|
if (this.dynamicRustSuggestionTypes?.length) {
|
|
return {
|
|
dynamicSuggestionTypes: this.dynamicRustSuggestionTypes,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @returns {string}
|
|
* If the feature's suggestions are served by the ML backend, the subclass
|
|
* should override this getter and return the ML intent name as returned by
|
|
* `MLSuggest`. e.g., "yelp_intent"
|
|
*/
|
|
get mlIntent() {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
* If the feature's suggestions are served by the ML backend, the subclass
|
|
* should override this getter and return true if the ML suggestions should
|
|
* be enabled and false otherwise.
|
|
*/
|
|
get isMlIntentEnabled() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Subclasses should typically override this method. It should return the
|
|
* telemetry type for the given suggestion. A telemetry type uniquely
|
|
* identifies a type of Suggest suggestion independent of the backend that
|
|
* returned it. It's used to build the result type values that are recorded in
|
|
* urlbar telemetry. The telemetry type does not include the suggestion's
|
|
* source/backend. For example, "adm_sponsored" is the AMP suggestion
|
|
* telemetry type, not "rust_adm_sponsored".
|
|
*
|
|
* @param {object} suggestion
|
|
* A suggestion returned by one of the Suggest backends.
|
|
* @returns {string}
|
|
* The suggestion's telemetry type.
|
|
*/
|
|
getSuggestionTelemetryType(suggestion) {
|
|
return this.merinoProvider;
|
|
}
|
|
|
|
/**
|
|
* Gets the list of commands that should be shown in the result menu for a
|
|
* given result from the provider. All commands returned by this method should
|
|
* be handled by implementing `onEngagement()` with the possible exception of
|
|
* commands automatically handled by the urlbar, like "help".
|
|
*
|
|
* @param {UrlbarResult} _result
|
|
* The result to get menu commands for.
|
|
* @returns {?UrlbarResultCommand[]}
|
|
*/
|
|
getResultCommands(_result) {
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Whether the "show less frequently" result-menu command should still be
|
|
* offered. Subclasses that support the command override this.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get canShowLessFrequently() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Bumps the count backing `canShowLessFrequently`. Subclasses that support the
|
|
* "show less frequently" command override this.
|
|
*/
|
|
incrementShowLessFrequentlyCount() {}
|
|
|
|
/**
|
|
* The subclass should override this method if it manages any sponsored
|
|
* suggestion types. It should return true if the given suggestion should be
|
|
* considered sponsored.
|
|
*
|
|
* @param {object} suggestion
|
|
* A suggestion returned by one of the Suggest backends.
|
|
* @returns {boolean}
|
|
* Whether the suggestion should be considered sponsored.
|
|
*/
|
|
isSuggestionSponsored(suggestion) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* The subclass may override this method as necessary. It will be called once
|
|
* per query with all of the feature's suggestions that matched the query. It
|
|
* should filter out suggestions that should not be shown. This is useful in
|
|
* cases where a backend may return many of the feature's suggestions but only
|
|
* some of them should be shown, and the criteria for determining which to
|
|
* show are external to the backend.
|
|
*
|
|
* `makeResult()` can also be used to filter suggestions by returning null for
|
|
* suggestions that should be discarded. Use `filterSuggestions()` when you
|
|
* need to know all matching suggestions in order to decide which to show.
|
|
*
|
|
* @param {Array} suggestions
|
|
* All the feature's suggestions that matched a query.
|
|
* @returns {Promise<Array>}
|
|
* The subset of `suggestions` that should be shown (typically all).
|
|
*/
|
|
async filterSuggestions(suggestions) {
|
|
return suggestions;
|
|
}
|
|
|
|
/**
|
|
* The subclass should override this method. It should return either a new
|
|
* `UrlbarResult` for the given suggestion or null if the suggestion should
|
|
* not be shown.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The query context.
|
|
* @param {object} suggestion
|
|
* A suggestion returned by one of the Suggest backends.
|
|
* @param {string} searchString
|
|
* The search string that was used to fetch the suggestion. It may be
|
|
* different from `queryContext.searchString` due to trimming, lower-casing,
|
|
* etc. This is included as a param in case it's useful.
|
|
* @returns {Promise<UrlbarResult|null>}
|
|
* A new result for the suggestion or null if a result should not be shown.
|
|
*/
|
|
async makeResult(queryContext, suggestion, searchString) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The subclass may override this method as necessary. It's analogous to
|
|
* `UrlbarProvider.onImpression()` and will be called when one or more of the
|
|
* feature's results were visible at the end of a urlbar session.
|
|
*
|
|
* @param {string} state
|
|
* The user-interaction state. See `UrlbarProvider.onImpression()`.
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The urlbar session's query context.
|
|
* @param {UrlbarParentController} controller
|
|
* The controller.
|
|
* @param {Array} featureResults
|
|
* The feature's results that were visible at the end of the session. This
|
|
* will always be non-empty and will only contain results from the feature.
|
|
* @param {object|null} details
|
|
* Details about the engagement. See `UrlbarProvider.onImpression()`.
|
|
*/
|
|
onImpression(state, queryContext, controller, featureResults, details) {}
|
|
|
|
/**
|
|
* The subclass may override this method as necessary. It's analogous to
|
|
* `UrlbarProvider.onEngagement()` and will be called when the user engages
|
|
* with a result from the feature.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The urlbar session's query context.
|
|
* @param {UrlbarParentController} controller
|
|
* The controller.
|
|
* @param {object|null} details
|
|
* See `UrlbarProvider.onEngagement()`.
|
|
* @param {string} searchString
|
|
* The actual search string used to fetch Suggest results. It might be
|
|
* slightly different from `queryContext.searchString`. e.g., it might be
|
|
* trimmed differently.
|
|
*/
|
|
onEngagement(queryContext, controller, details, searchString) {}
|
|
|
|
/**
|
|
* Some features may create result URLs that are potentially unique per query.
|
|
* Typically this is done by modifying an original suggestion URL at query
|
|
* time, for example by adding timestamps or query-specific search params. In
|
|
* that case, a single original suggestion URL will map to many result URLs.
|
|
* If this is true for the subclass, it should override this method and return
|
|
* whether the given URL and result URL both map back to the same original
|
|
* suggestion URL.
|
|
*
|
|
* @param {string} url
|
|
* The URL to check, typically from the user's history.
|
|
* @param {UrlbarResult} result
|
|
* The Suggest result.
|
|
* @returns {boolean}
|
|
* Whether `url` is equivalent to the result's URL.
|
|
*/
|
|
isUrlEquivalentToResultUrl(url, result) {
|
|
return url == result.payload.url;
|
|
}
|
|
|
|
// Methods not designed for overriding below
|
|
|
|
/**
|
|
* Handles the shared "show less frequently" result-menu command: acknowledges
|
|
* the feedback, increments the provider's count, and once the cap is reached
|
|
* refreshes the result's baked menu commands so the command is dropped from
|
|
* the still-open view. Subclasses call this from `onEngagement()`.
|
|
*
|
|
* @param {UrlbarParentController} controller
|
|
* The engagement's controller.
|
|
* @param {UrlbarResult} result
|
|
* The result the command acted on.
|
|
*/
|
|
handleShowLessFrequently(controller, result) {
|
|
controller.view.acknowledgeFeedback(result);
|
|
this.incrementShowLessFrequentlyCount();
|
|
if (!this.canShowLessFrequently) {
|
|
result.commands = this.getResultCommands(result);
|
|
controller.view.invalidateResultMenuCommands();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the feature. If the feature manages any Rust suggestion
|
|
* types that become enabled as a result, they will be ingested.
|
|
*/
|
|
update() {
|
|
super.update();
|
|
lazy.QuickSuggest.rustBackend?.ingestEnabledSuggestions(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for Suggest features that serve suggestions. None of the methods
|
|
* will be called when the backend is disabled.
|
|
*
|
|
* Subclasses should be registered with `QuickSuggest` by adding them to the
|
|
* `FEATURES` const in `QuickSuggest.sys.mjs`.
|
|
*/
|
|
export class SuggestBackend extends SuggestFeature {
|
|
// Methods designed for overriding below
|
|
|
|
/**
|
|
* The subclass should override this method. It should fetch and return
|
|
* matching suggestions.
|
|
*
|
|
* @param {string} searchString
|
|
* The search string.
|
|
* @param {object} [options]
|
|
* Options object.
|
|
* @param {UrlbarQueryContext} [options.queryContext]
|
|
* The query context.
|
|
* @param {?Array} [options.types]
|
|
* This is only intended to be used in special circumstances and normally
|
|
* should not be specified. Array of suggestion types to query. By default
|
|
* all enabled suggestion types are queried.
|
|
* @returns {Promise<Array>}
|
|
* Array of matching suggestions. An empty array should be returned if no
|
|
* suggestions matched or suggestions can't be fetched for any reason.
|
|
* @abstract
|
|
*/
|
|
async query(searchString, { queryContext, types = null } = {}) {
|
|
throw new Error("Trying to access the base class, must be overridden");
|
|
}
|
|
|
|
/**
|
|
* The subclass should override this method if anything needs to be stopped or
|
|
* cleaned up when a query is canceled.
|
|
*/
|
|
cancelQuery() {}
|
|
|
|
/**
|
|
* The subclass should override this method as necessary. It's called on the
|
|
* backend in response to `UrlbarProviderQuickSuggest.onSearchSessionEnd()`.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The query context.
|
|
* @param {UrlbarParentController} controller
|
|
* The controller.
|
|
* @param {object} details
|
|
* Details object.
|
|
*/
|
|
onSearchSessionEnd(queryContext, controller, details) {}
|
|
}
|