diff options
Diffstat (limited to 'browser/components/urlbar/private/Weather.sys.mjs')
-rw-r--r-- | browser/components/urlbar/private/Weather.sys.mjs | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/browser/components/urlbar/private/Weather.sys.mjs b/browser/components/urlbar/private/Weather.sys.mjs new file mode 100644 index 0000000000..611994c8f8 --- /dev/null +++ b/browser/components/urlbar/private/Weather.sys.mjs @@ -0,0 +1,171 @@ +/* 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/. */ + +import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + clearInterval: "resource://gre/modules/Timer.sys.mjs", + MerinoClient: "resource:///modules/MerinoClient.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + setInterval: "resource://gre/modules/Timer.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +const FETCH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes +const MERINO_PROVIDER = "accuweather"; +const MERINO_TIMEOUT_MS = 5000; // 5s + +const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_WEATHER_MS"; +const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE_WEATHER"; + +/** + * A feature that periodically fetches weather suggestions from Merino. + */ +export class Weather extends BaseFeature { + get shouldEnable() { + return ( + lazy.UrlbarPrefs.get("weatherFeatureGate") && + lazy.UrlbarPrefs.get("suggest.weather") && + lazy.UrlbarPrefs.get("merinoEnabled") + ); + } + + get enablingPreferences() { + return ["suggest.weather"]; + } + + /** + * @returns {object} + * The last weather suggestion fetched from Merino or null if none. + */ + get suggestion() { + return this.#suggestion; + } + + enable(enabled) { + if (enabled) { + this.#init(); + } else { + this.#uninit(); + } + } + + /** + * Returns a promise that resolves when all pending fetches finish, if there + * are pending fetches. If there aren't, the promise resolves when all pending + * fetches starting with the next fetch finish. + * + * @returns {Promise} + */ + waitForFetches() { + if (!this.#waitForFetchesDeferred) { + this.#waitForFetchesDeferred = lazy.PromiseUtils.defer(); + } + return this.#waitForFetchesDeferred.promise; + } + + #init() { + this.#merino = new lazy.MerinoClient(this.constructor.name); + + this.#fetchInterval = lazy.setInterval( + () => this.#fetch(), + this.#fetchIntervalMs + ); + + // `#fetch()` is async but there's no need to await it here. + this.#fetch(); + } + + #uninit() { + this.#merino = null; + this.#suggestion = null; + lazy.clearInterval(this.#fetchInterval); + this.#fetchInterval = 0; + } + + async #fetch() { + this.logger.info("Fetching suggestion"); + + // This `Weather` instance may be uninitialized while awaiting the fetch or + // even uninitialized and re-initialized a number of times. Discard the + // fetched suggestion if the `#merino` after the fetch isn't the same as the + // one before. + let merino = this.#merino; + + let suggestions; + this.#pendingFetchCount++; + try { + suggestions = await merino.fetch({ + query: "", + providers: [MERINO_PROVIDER], + timeoutMs: this.#timeoutMs, + extraLatencyHistogram: HISTOGRAM_LATENCY, + extraResponseHistogram: HISTOGRAM_RESPONSE, + }); + } finally { + this.#pendingFetchCount--; + } + + // Reset the Merino client's session so different fetches use different + // sessions. A single session is intended to represent a single user + // engagement in the urlbar, which this is not. Practically this isn't + // necessary since the client automatically resets the session on a timer + // whose period is much shorter than our fetch period, but there's no reason + // to keep it ticking in the meantime. + merino.resetSession(); + + if (merino != this.#merino) { + this.logger.info("Fetch canceled, discarding fetched suggestion, if any"); + } else { + let suggestion = suggestions?.[0]; + if (!suggestion) { + this.logger.info("No suggestion received"); + this.#suggestion = null; + } else { + this.logger.info("Got suggestion"); + this.logger.debug(JSON.stringify({ suggestion })); + this.#suggestion = { ...suggestion, source: "merino" }; + } + } + + if (!this.#pendingFetchCount) { + this.#waitForFetchesDeferred?.resolve(); + this.#waitForFetchesDeferred = null; + } + } + + get _test_merino() { + return this.#merino; + } + + get _test_fetchInterval() { + return this.#fetchInterval; + } + + get _test_pendingFetchCount() { + return this.#pendingFetchCount; + } + + async _test_fetch() { + await this.#fetch(); + } + + _test_setFetchIntervalMs(ms) { + this.#fetchIntervalMs = ms < 0 ? FETCH_INTERVAL_MS : ms; + } + + _test_setTimeoutMs(ms) { + this.#timeoutMs = ms < 0 ? MERINO_TIMEOUT_MS : ms; + } + + #merino = null; + #suggestion = null; + #fetchInterval = 0; + #fetchIntervalMs = FETCH_INTERVAL_MS; + #timeoutMs = MERINO_TIMEOUT_MS; + #waitForFetchesDeferred = null; + #pendingFetchCount = 0; +} |