summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/RecommendationProvider.jsm
blob: 2438dfe542eb005f05bc3d9b3bbc45cc7f7315af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* 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/. */
"use strict";

// Use XPCOMUtils.defineLazyModuleGetters to make the test harness keeps working
// after bug 1608279.
//
// The test harness's workaround for "lazy getter on a plain object" is to
// set the `lazy` object's prototype to the global object, inside the lazy
// getter API.
//
// ChromeUtils.defineModuleGetter is converted into a static import declaration
// by babel-plugin-jsm-to-esmodules, and it doesn't work for the following
// 2 reasons:
//
//   * There's no other lazy getter API call in this file, and the workaround
//     above stops working
//   * babel-plugin-jsm-to-esmodules ignores the first parameter of the lazy
//     getter API, and the result is wrong
const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);
const lazy = {};
XPCOMUtils.defineLazyModuleGetters(lazy, {
  PersonalityProvider:
    "resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.jsm",
});

const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule(
  "resource://activity-stream/common/Actions.sys.mjs"
);
const PREF_PERSONALIZATION_MODEL_KEYS =
  "discoverystream.personalization.modelKeys";
const PREF_PERSONALIZATION = "discoverystream.personalization.enabled";

// The main purpose of this class is to handle interactions with the recommendation provider.
// A recommendation provider scores a list of stories, currently this is a personality provider.
// So all calls to the provider, anything involved with the setup of the provider,
// accessing prefs for the provider, or updaing devtools with provider state, is contained in here.
class RecommendationProvider {
  setProvider(scores) {
    // A provider is already set. This can happen when new stories come in
    // and we need to update their scores.
    // We can use the existing one, a fresh one is created after startup.
    // Using the existing one might be a bit out of date,
    // but it's fine for now. We can rely on restarts for updates.
    // See bug 1629931 for improvements to this.
    if (this.provider) {
      return;
    }
    // At this point we've determined we can successfully create a v2 personalization provider.
    this.provider = new lazy.PersonalityProvider(this.modelKeys);
    this.provider.setScores(scores);
  }

  /*
   * This calls any async initialization that's required,
   * and then signals to devtools when that's done.
   */
  async init() {
    if (this.provider && this.provider.init) {
      await this.provider.init();
      this.store.dispatch(
        ac.BroadcastToContent({
          type: at.DISCOVERY_STREAM_PERSONALIZATION_INIT,
        })
      );
    }
  }

  get modelKeys() {
    if (!this._modelKeys) {
      this._modelKeys =
        this.store.getState().Prefs.values[PREF_PERSONALIZATION_MODEL_KEYS];
    }

    return this._modelKeys;
  }

  getScores() {
    return this.provider.getScores();
  }

  async calculateItemRelevanceScore(item) {
    if (this.provider) {
      const scoreResult = await this.provider.calculateItemRelevanceScore(item);
      if (scoreResult === 0 || scoreResult) {
        item.score = scoreResult;
      }
    }
  }

  teardown() {
    if (this.provider && this.provider.teardown) {
      // This removes any in memory listeners if available.
      this.provider.teardown();
    }
  }

  resetState() {
    this._modelKeys = null;
    this.provider = null;
  }

  onAction(action) {
    switch (action.type) {
      case at.DISCOVERY_STREAM_CONFIG_CHANGE:
        this.teardown();
        this.resetState();
        break;
      case at.PREF_CHANGED:
        switch (action.data.name) {
          case PREF_PERSONALIZATION_MODEL_KEYS:
            this.store.dispatch(
              ac.BroadcastToContent({
                type: at.DISCOVERY_STREAM_CONFIG_RESET,
              })
            );
            break;
        }
        break;
      case at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE:
        let enabled = this.store.getState().Prefs.values[PREF_PERSONALIZATION];

        this.store.dispatch(ac.SetPref(PREF_PERSONALIZATION, !enabled));
        break;
    }
  }
}

const EXPORTED_SYMBOLS = ["RecommendationProvider"];