summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/WeatherFeed.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/lib/WeatherFeed.sys.mjs')
-rw-r--r--browser/components/newtab/lib/WeatherFeed.sys.mjs208
1 files changed, 208 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/WeatherFeed.sys.mjs b/browser/components/newtab/lib/WeatherFeed.sys.mjs
new file mode 100644
index 0000000000..16aa8196af
--- /dev/null
+++ b/browser/components/newtab/lib/WeatherFeed.sys.mjs
@@ -0,0 +1,208 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ PersistentCache: "resource://activity-stream/lib/PersistentCache.sys.mjs",
+});
+
+import {
+ actionTypes as at,
+ actionCreators as ac,
+} from "resource://activity-stream/common/Actions.mjs";
+
+const CACHE_KEY = "weather_feed";
+const WEATHER_UPDATE_TIME = 10 * 60 * 1000; // 10 minutes
+const MERINO_PROVIDER = "accuweather";
+
+const PREF_WEATHER_QUERY = "weather.query";
+const PREF_SHOW_WEATHER = "showWeather";
+const PREF_SYSTEM_SHOW_WEATHER = "system.showWeather";
+
+/**
+ * A feature that periodically fetches weather suggestions from Merino for HNT.
+ */
+export class WeatherFeed {
+ constructor() {
+ this.loaded = false;
+ this.merino = null;
+ this.suggestions = [];
+ this.lastUpdated = null;
+ this.fetchTimer = null;
+ this.fetchIntervalMs = 30 * 60 * 1000; // 30 minutes
+ this.timeoutMS = 5000;
+ this.lastFetchTimeMs = 0;
+ this.fetchDelayAfterComingOnlineMs = 3000; // 3s
+ this.cache = this.PersistentCache(CACHE_KEY, true);
+ }
+
+ async resetCache() {
+ if (this.cache) {
+ await this.cache.set("weather", {});
+ }
+ }
+
+ async resetWeather() {
+ await this.resetCache();
+ this.suggestions = [];
+ this.lastUpdated = null;
+ }
+
+ isEnabled() {
+ return (
+ this.store.getState().Prefs.values[PREF_SHOW_WEATHER] &&
+ this.store.getState().Prefs.values[PREF_SYSTEM_SHOW_WEATHER]
+ );
+ }
+
+ async init() {
+ await this.loadWeather(true /* isStartup */);
+ }
+
+ stopFetching() {
+ if (!this.merino) {
+ return;
+ }
+
+ lazy.clearTimeout(this.fetchTimer);
+ this.merino = null;
+ this.suggestions = null;
+ this.fetchTimer = 0;
+ }
+
+ /**
+ * This thin wrapper around the fetch call makes it easier for us to write
+ * automated tests that simulate responses.
+ */
+ async fetchHelper() {
+ this.restartFetchTimer();
+ const weatherQuery = this.store.getState().Prefs.values[PREF_WEATHER_QUERY];
+ let suggestions = [];
+ try {
+ suggestions = await this.merino.fetch({
+ query: weatherQuery || "",
+ providers: [MERINO_PROVIDER],
+ timeoutMs: 5000,
+ });
+ } catch (error) {
+ // We don't need to do anything with this right now.
+ }
+
+ // results from the API or empty array if null
+ this.suggestions = suggestions ?? [];
+ }
+
+ async fetch(isStartup) {
+ // Keep a handle on the `MerinoClient` instance that exists at the start of
+ // this fetch. If fetching stops or this `Weather` instance is uninitialized
+ // during the fetch, `#merino` will be nulled, and the fetch should stop. We
+ // can compare `merino` to `this.merino` to tell when this occurs.
+ this.merino = await this.MerinoClient("HNT_WEATHER_FEED");
+ await this.fetchHelper();
+
+ if (this.suggestions.length) {
+ this.lastUpdated = this.Date().now();
+ await this.cache.set("weather", {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ });
+ }
+
+ this.update(isStartup);
+ }
+
+ async loadWeather(isStartup = false) {
+ const cachedData = (await this.cache.get()) || {};
+ const { weather } = cachedData;
+
+ // If we have nothing in cache, or cache has expired, we can make a fresh fetch.
+ if (
+ !weather?.lastUpdated ||
+ !(this.Date().now() - weather.lastUpdated < WEATHER_UPDATE_TIME)
+ ) {
+ await this.fetch(isStartup);
+ } else if (!this.lastUpdated) {
+ this.suggestions = weather.suggestions;
+ this.lastUpdated = weather.lastUpdated;
+ this.update(isStartup);
+ }
+ }
+
+ update(isStartup) {
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.WEATHER_UPDATE,
+ data: {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ },
+ meta: {
+ isStartup,
+ },
+ })
+ );
+ }
+
+ restartFetchTimer(ms = this.fetchIntervalMs) {
+ lazy.clearTimeout(this.fetchTimer);
+ this.fetchTimer = lazy.setTimeout(() => {
+ this.fetch();
+ }, ms);
+ }
+
+ async onPrefChangedAction(action) {
+ switch (action.data.name) {
+ case PREF_WEATHER_QUERY:
+ await this.loadWeather();
+ break;
+ case PREF_SHOW_WEATHER:
+ case PREF_SYSTEM_SHOW_WEATHER:
+ if (this.isEnabled() && action.data.value) {
+ await this.loadWeather();
+ } else {
+ await this.resetWeather();
+ }
+ break;
+ }
+ }
+
+ async onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ if (this.isEnabled()) {
+ await this.init();
+ }
+ break;
+ case at.UNINIT:
+ await this.resetWeather();
+ break;
+ case at.DISCOVERY_STREAM_DEV_SYSTEM_TICK:
+ case at.SYSTEM_TICK:
+ if (this.isEnabled()) {
+ await this.loadWeather();
+ }
+ break;
+ case at.PREF_CHANGED:
+ await this.onPrefChangedAction(action);
+ break;
+ }
+ }
+}
+
+/**
+ * Creating a thin wrapper around MerinoClient, PersistentCache, and Date.
+ * This makes it easier for us to write automated tests that simulate responses.
+ */
+WeatherFeed.prototype.MerinoClient = (...args) => {
+ return new lazy.MerinoClient(...args);
+};
+WeatherFeed.prototype.PersistentCache = (...args) => {
+ return new lazy.PersistentCache(...args);
+};
+WeatherFeed.prototype.Date = () => {
+ return Date;
+};