1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 getFrecentRecentCombinedUrls:
11 "resource://gre/modules/contentrelevancy/private/InputUtils.sys.mjs",
12 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
13 RelevancyStore: "resource://gre/modules/RustRelevancy.sys.mjs",
16 XPCOMUtils.defineLazyServiceGetter(
19 "@mozilla.org/updates/timer-manager;1",
20 "nsIUpdateTimerManager"
23 // Constants used by `nsIUpdateTimerManager` for a cross-session timer.
24 const TIMER_ID = "content-relevancy-timer";
25 const PREF_TIMER_LAST_UPDATE = `app.update.lastUpdateTime.${TIMER_ID}`;
26 const PREF_TIMER_INTERVAL = "toolkit.contentRelevancy.timerInterval";
27 // Set the timer interval to 1 day for validation.
28 const DEFAULT_TIMER_INTERVAL_SECONDS = 1 * 24 * 60 * 60;
30 // Default maximum input URLs to fetch from Places.
31 const DEFAULT_MAX_URLS = 100;
32 // Default minimal input URLs for clasification.
33 const DEFAULT_MIN_URLS = 0;
35 // File name of the relevancy database
36 const RELEVANCY_STORE_FILENAME = "content-relevancy.sqlite";
39 const NIMBUS_VARIABLE_ENABLED = "enabled";
40 const NIMBUS_VARIABLE_MAX_INPUT_URLS = "maxInputUrls";
41 const NIMBUS_VARIABLE_MIN_INPUT_URLS = "minInputUrls";
42 const NIMBUS_VARIABLE_TIMER_INTERVAL = "timerInterval";
44 ChromeUtils.defineLazyGetter(lazy, "log", () => {
45 return console.createInstance({
46 prefix: "ContentRelevancyManager",
47 maxLogLevel: Services.prefs.getBoolPref(
48 "toolkit.contentRelevancy.log",
56 class RelevancyManager {
58 return this.#initialized;
62 * Init the manager. An update timer is registered if the feature is enabled.
63 * The pref observer is always set so we can toggle the feature without restarting
66 * Note that this should be called once only. `#enable` and `#disable` can be
67 * used to toggle the feature once the manager is initialized.
70 if (this.initialized) {
74 lazy.log.info("Initializing the manager");
76 if (this.shouldEnable) {
80 this._nimbusUpdateCallback = this.#onNimbusUpdate.bind(this);
81 // This will handle both Nimbus updates and pref changes.
82 lazy.NimbusFeatures.contentRelevancy.onUpdate(this._nimbusUpdateCallback);
83 this.#initialized = true;
87 if (!this.initialized) {
91 lazy.log.info("Uninitializing the manager");
93 lazy.NimbusFeatures.contentRelevancy.offUpdate(this._nimbusUpdateCallback);
96 this.#initialized = false;
100 * Determine whether the feature should be enabled based on prefs and Nimbus.
104 lazy.NimbusFeatures.contentRelevancy.getVariable(
105 NIMBUS_VARIABLE_ENABLED
111 // Log the last timer tick for debugging.
112 const lastTick = Services.prefs.getIntPref(PREF_TIMER_LAST_UPDATE, 0);
115 `Last timer tick: ${lastTick}s (${
116 Math.round(Date.now() / 1000) - lastTick
120 lazy.log.debug("Last timer tick: none");
124 lazy.NimbusFeatures.contentRelevancy.getVariable(
125 NIMBUS_VARIABLE_TIMER_INTERVAL
127 Services.prefs.getIntPref(
129 DEFAULT_TIMER_INTERVAL_SECONDS
131 lazy.timerManager.registerTimer(
135 interval != 0 // Do not skip the first timer tick for a zero interval for testing
140 return PathUtils.join(
141 Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
142 RELEVANCY_STORE_FILENAME
148 // Init the relevancy store.
149 const path = this.#storePath;
150 lazy.log.info(`Initializing RelevancyStore: ${path}`);
153 this.#_store = await lazy.RelevancyStore.init(path);
155 lazy.log.error(`Error initializing RelevancyStore: ${error}`);
160 this.#startUpTimer();
164 * The reciprocal of `#enable()`, ensure this is safe to call when you add
165 * new disabling code here. It should be so even if `#enable()` hasn't been
170 lazy.timerManager.unregisterTimer(TIMER_ID);
173 async #toggleFeature() {
174 if (this.shouldEnable) {
175 await this.#enable();
185 lazy.log.info("Background job timer fired");
186 this.#doClassification();
190 return this.#isInProgress;
194 * Perform classification based on browsing history.
196 * It will fetch up to `DEFAULT_MAX_URLS` (or the corresponding Nimbus value)
197 * URLs from top frecent URLs and use most recent URLs as a fallback if the
198 * former is insufficient. The returned URLs might be fewer than requested.
200 * The classification will not be performed if the total number of input URLs
201 * is less than `DEFAULT_MIN_URLS` (or the corresponding Nimbus value).
203 async #doClassification() {
204 if (this.isInProgress) {
206 "Another classification is in progress, aborting interest classification"
211 // Set a flag indicating this classification. Ensure it's cleared upon early
212 // exit points & success.
213 this.#isInProgress = true;
216 lazy.log.info("Fetching input data for interest classification");
219 lazy.NimbusFeatures.contentRelevancy.getVariable(
220 NIMBUS_VARIABLE_MAX_INPUT_URLS
221 ) ?? DEFAULT_MAX_URLS;
223 lazy.NimbusFeatures.contentRelevancy.getVariable(
224 NIMBUS_VARIABLE_MIN_INPUT_URLS
225 ) ?? DEFAULT_MIN_URLS;
226 const urls = await lazy.getFrecentRecentCombinedUrls(maxUrls);
227 if (urls.length < minUrls) {
228 lazy.log.info("Aborting interest classification: insufficient input");
232 lazy.log.info("Starting interest classification");
233 await this.#doClassificationHelper(urls);
235 if (error instanceof StoreNotAvailableError) {
236 lazy.log.error("#store became null, aborting interest classification");
238 lazy.log.error("Classification error: " + (error.reason ?? error));
241 this.#isInProgress = false;
244 lazy.log.info("Finished interest classification");
248 * Classification helper. Use the getter `this.#store` rather than `#_store`
249 * to access the store so that when it becomes null, a `StoreNotAvailableError`
250 * will be raised. Likewise, other store related errors should be propagated
251 * to the caller if you want to perform custom error handling in this helper.
253 * @param {Array} urls
255 * @throws {StoreNotAvailableError}
256 * Thrown when the store became unavailable (i.e. set to null elsewhere).
257 * @throws {RelevancyAPIError}
258 * Thrown for other API errors on the store.
260 async #doClassificationHelper(urls) {
261 // The following logs are unnecessary, only used to suppress the linting error.
262 // TODO(nanj): delete me once the following TODO is done.
264 lazy.log.error("#store became null, aborting interest classification");
266 lazy.log.info("Classification input: " + urls);
268 // TODO(nanj): uncomment the following once `ingest()` is implemented.
269 // await this.#store.ingest(urls);
273 * Exposed for testing.
275 async _test_doClassification(urls) {
276 await this.#doClassificationHelper(urls);
280 * Internal getter for `#_store` used by for classification. It will throw
281 * a `StoreNotAvailableError` is the store is not ready.
284 if (!this._isStoreReady) {
285 throw new StoreNotAvailableError("Store is not available");
292 * Whether or not the store is ready (i.e. not null).
294 get _isStoreReady() {
295 return !!this.#_store;
299 * Nimbus update listener.
301 #onNimbusUpdate(_event, _reason) {
302 this.#toggleFeature();
305 // The `RustRelevancy` store.
308 // Whether or not the module is initialized.
309 #initialized = false;
311 // Whether or not there is an in-progress classification. Used to prevent
312 // duplicate classification tasks.
313 #isInProgress = false;
317 * Error raised when attempting to access a null store.
319 class StoreNotAvailableError extends Error {
320 constructor(message, ...params) {
321 super(message, ...params);
322 this.name = "StoreNotAvailableError";
326 export var ContentRelevancyManager = new RelevancyManager();