summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/Store.sys.mjs
blob: ea66c36f50547c8b2e36d0c08516a18061f0acea (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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/* 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 { ActivityStreamMessageChannel } from "resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs";
import { ActivityStreamStorage } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs";
import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs";
import { reducers } from "resource://activity-stream/common/Reducers.sys.mjs";
import { redux } from "resource://activity-stream/vendor/Redux.sys.mjs";

/**
 * Store - This has a similar structure to a redux store, but includes some extra
 *         functionality to allow for routing of actions between the Main processes
 *         and child processes via a ActivityStreamMessageChannel.
 *         It also accepts an array of "Feeds" on inititalization, which
 *         can listen for any action that is dispatched through the store.
 */
export class Store {
  /**
   * constructor - The redux store and message manager are created here,
   *               but no listeners are added until "init" is called.
   */
  constructor() {
    this._middleware = this._middleware.bind(this);
    // Bind each redux method so we can call it directly from the Store. E.g.,
    // store.dispatch() will call store._store.dispatch();
    for (const method of ["dispatch", "getState", "subscribe"]) {
      this[method] = (...args) => this._store[method](...args);
    }
    this.feeds = new Map();
    this._prefs = new Prefs();
    this._messageChannel = new ActivityStreamMessageChannel({
      dispatch: this.dispatch,
    });
    this._store = redux.createStore(
      redux.combineReducers(reducers),
      redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
    );
    this.storage = null;
  }

  /**
   * _middleware - This is redux middleware consumed by redux.createStore.
   *               it calls each feed's .onAction method, if one
   *               is defined.
   */
  _middleware() {
    return next => action => {
      next(action);
      for (const store of this.feeds.values()) {
        if (store.onAction) {
          store.onAction(action);
        }
      }
    };
  }

  /**
   * initFeed - Initializes a feed by calling its constructor function
   *
   * @param  {string} feedName The name of a feed, as defined in the object
   *                           passed to Store.init
   * @param {Action} initAction An optional action to initialize the feed
   */
  initFeed(feedName, initAction) {
    const feed = this._feedFactories.get(feedName)();
    feed.store = this;
    this.feeds.set(feedName, feed);
    if (initAction && feed.onAction) {
      feed.onAction(initAction);
    }
  }

  /**
   * uninitFeed - Removes a feed and calls its uninit function if defined
   *
   * @param  {string} feedName The name of a feed, as defined in the object
   *                           passed to Store.init
   * @param {Action} uninitAction An optional action to uninitialize the feed
   */
  uninitFeed(feedName, uninitAction) {
    const feed = this.feeds.get(feedName);
    if (!feed) {
      return;
    }
    if (uninitAction && feed.onAction) {
      feed.onAction(uninitAction);
    }
    this.feeds.delete(feedName);
  }

  /**
   * onPrefChanged - Listener for handling feed changes.
   */
  onPrefChanged(name, value) {
    if (this._feedFactories.has(name)) {
      if (value) {
        this.initFeed(name, this._initAction);
      } else {
        this.uninitFeed(name, this._uninitAction);
      }
    }
  }

  /**
   * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
   *
   * Note that it intentionally initializes the TelemetryFeed first so that the
   * addon is able to report the init errors from other feeds.
   *
   * @param  {Map} feedFactories A Map of feeds with the name of the pref for
   *                                the feed as the key and a function that
   *                                constructs an instance of the feed.
   * @param {Action} initAction An optional action that will be dispatched
   *                            to feeds when they're created.
   * @param {Action} uninitAction An optional action for when feeds uninit.
   */
  async init(feedFactories, initAction, uninitAction) {
    this._feedFactories = feedFactories;
    this._initAction = initAction;
    this._uninitAction = uninitAction;

    const telemetryKey = "feeds.telemetry";
    if (feedFactories.has(telemetryKey) && this._prefs.get(telemetryKey)) {
      this.initFeed(telemetryKey);
    }

    await this._initIndexedDB(telemetryKey);

    for (const pref of feedFactories.keys()) {
      if (pref !== telemetryKey && this._prefs.get(pref)) {
        this.initFeed(pref);
      }
    }

    this._prefs.observeBranch(this);

    // Dispatch an initial action after all enabled feeds are ready
    if (initAction) {
      this.dispatch(initAction);
    }

    // Dispatch NEW_TAB_INIT/NEW_TAB_LOAD events after INIT event.
    this._messageChannel.simulateMessagesForExistingTabs();
  }

  async _initIndexedDB() {
    // "snippets" is the name of one storage space, but these days it is used
    // not for snippet-related data (snippets were removed in bug 1715158),
    // but storage for impression or session data for all ASRouter messages.
    //
    // We keep the name "snippets" to avoid having to do an IndexedDB database
    // migration.
    this.dbStorage = new ActivityStreamStorage({
      storeNames: ["sectionPrefs", "snippets"],
    });
    // Accessing the db causes the object stores to be created / migrated.
    // This needs to happen before other instances try to access the db, which
    // would update only a subset of the stores to the latest version.
    try {
      await this.dbStorage.db; // eslint-disable-line no-unused-expressions
    } catch (e) {
      this.dbStorage.telemetry = null;
    }
  }

  /**
   * uninit -  Uninitalizes each feed, clears them, and destroys the message
   *           manager channel.
   *
   * @return {type}  description
   */
  uninit() {
    if (this._uninitAction) {
      this.dispatch(this._uninitAction);
    }
    this._prefs.ignoreBranch(this);
    this.feeds.clear();
    this._feedFactories = null;
  }

  /**
   * getMessageChannel - Used by the AboutNewTabParent actor to get the message channel.
   */
  getMessageChannel() {
    return this._messageChannel;
  }
}