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/. */
4"use strict";
5
6const { ActivityStreamMessageChannel } = ChromeUtils.import(
7  "resource://activity-stream/lib/ActivityStreamMessageChannel.jsm"
8);
9const { ActivityStreamStorage } = ChromeUtils.import(
10  "resource://activity-stream/lib/ActivityStreamStorage.jsm"
11);
12const { Prefs } = ChromeUtils.import(
13  "resource://activity-stream/lib/ActivityStreamPrefs.jsm"
14);
15const { reducers } = ChromeUtils.import(
16  "resource://activity-stream/common/Reducers.jsm"
17);
18const { redux } = ChromeUtils.import(
19  "resource://activity-stream/vendor/Redux.jsm"
20);
21
22/**
23 * Store - This has a similar structure to a redux store, but includes some extra
24 *         functionality to allow for routing of actions between the Main processes
25 *         and child processes via a ActivityStreamMessageChannel.
26 *         It also accepts an array of "Feeds" on inititalization, which
27 *         can listen for any action that is dispatched through the store.
28 */
29this.Store = class Store {
30  /**
31   * constructor - The redux store and message manager are created here,
32   *               but no listeners are added until "init" is called.
33   */
34  constructor() {
35    this._middleware = this._middleware.bind(this);
36    // Bind each redux method so we can call it directly from the Store. E.g.,
37    // store.dispatch() will call store._store.dispatch();
38    for (const method of ["dispatch", "getState", "subscribe"]) {
39      this[method] = (...args) => this._store[method](...args);
40    }
41    this.feeds = new Map();
42    this._prefs = new Prefs();
43    this._messageChannel = new ActivityStreamMessageChannel({
44      dispatch: this.dispatch,
45    });
46    this._store = redux.createStore(
47      redux.combineReducers(reducers),
48      redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
49    );
50    this.storage = null;
51  }
52
53  /**
54   * _middleware - This is redux middleware consumed by redux.createStore.
55   *               it calls each feed's .onAction method, if one
56   *               is defined.
57   */
58  _middleware() {
59    return next => action => {
60      next(action);
61      for (const store of this.feeds.values()) {
62        if (store.onAction) {
63          store.onAction(action);
64        }
65      }
66    };
67  }
68
69  /**
70   * initFeed - Initializes a feed by calling its constructor function
71   *
72   * @param  {string} feedName The name of a feed, as defined in the object
73   *                           passed to Store.init
74   * @param {Action} initAction An optional action to initialize the feed
75   */
76  initFeed(feedName, initAction) {
77    const feed = this._feedFactories.get(feedName)();
78    feed.store = this;
79    this.feeds.set(feedName, feed);
80    if (initAction && feed.onAction) {
81      feed.onAction(initAction);
82    }
83  }
84
85  /**
86   * uninitFeed - Removes a feed and calls its uninit function if defined
87   *
88   * @param  {string} feedName The name of a feed, as defined in the object
89   *                           passed to Store.init
90   * @param {Action} uninitAction An optional action to uninitialize the feed
91   */
92  uninitFeed(feedName, uninitAction) {
93    const feed = this.feeds.get(feedName);
94    if (!feed) {
95      return;
96    }
97    if (uninitAction && feed.onAction) {
98      feed.onAction(uninitAction);
99    }
100    this.feeds.delete(feedName);
101  }
102
103  /**
104   * onPrefChanged - Listener for handling feed changes.
105   */
106  onPrefChanged(name, value) {
107    if (this._feedFactories.has(name)) {
108      if (value) {
109        this.initFeed(name, this._initAction);
110      } else {
111        this.uninitFeed(name, this._uninitAction);
112      }
113    }
114  }
115
116  /**
117   * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
118   *
119   * Note that it intentionally initializes the TelemetryFeed first so that the
120   * addon is able to report the init errors from other feeds.
121   *
122   * @param  {Map} feedFactories A Map of feeds with the name of the pref for
123   *                                the feed as the key and a function that
124   *                                constructs an instance of the feed.
125   * @param {Action} initAction An optional action that will be dispatched
126   *                            to feeds when they're created.
127   * @param {Action} uninitAction An optional action for when feeds uninit.
128   */
129  async init(feedFactories, initAction, uninitAction) {
130    this._feedFactories = feedFactories;
131    this._initAction = initAction;
132    this._uninitAction = uninitAction;
133
134    const telemetryKey = "feeds.telemetry";
135    if (feedFactories.has(telemetryKey) && this._prefs.get(telemetryKey)) {
136      this.initFeed(telemetryKey);
137    }
138
139    await this._initIndexedDB(telemetryKey);
140
141    for (const pref of feedFactories.keys()) {
142      if (pref !== telemetryKey && this._prefs.get(pref)) {
143        this.initFeed(pref);
144      }
145    }
146
147    this._prefs.observeBranch(this);
148    this._messageChannel.createChannel();
149
150    // Dispatch an initial action after all enabled feeds are ready
151    if (initAction) {
152      this.dispatch(initAction);
153    }
154
155    // Dispatch NEW_TAB_INIT/NEW_TAB_LOAD events after INIT event.
156    this._messageChannel.simulateMessagesForExistingTabs();
157  }
158
159  async _initIndexedDB(telemetryKey) {
160    this.dbStorage = new ActivityStreamStorage({
161      storeNames: ["sectionPrefs", "snippets"],
162    });
163    // Accessing the db causes the object stores to be created / migrated.
164    // This needs to happen before other instances try to access the db, which
165    // would update only a subset of the stores to the latest version.
166    try {
167      await this.dbStorage.db; // eslint-disable-line no-unused-expressions
168    } catch (e) {
169      this.dbStorage.telemetry = null;
170    }
171  }
172
173  /**
174   * uninit -  Uninitalizes each feed, clears them, and destroys the message
175   *           manager channel.
176   *
177   * @return {type}  description
178   */
179  uninit() {
180    if (this._uninitAction) {
181      this.dispatch(this._uninitAction);
182    }
183    this._prefs.ignoreBranch(this);
184    this.feeds.clear();
185    this._feedFactories = null;
186    this._messageChannel.destroyChannel();
187  }
188};
189
190const EXPORTED_SYMBOLS = ["Store"];
191