summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/private/Weather.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/private/Weather.sys.mjs')
-rw-r--r--browser/components/urlbar/private/Weather.sys.mjs171
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;
+}