summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/res/PaymentsStore.js
blob: 7e439076d84377c8389e57a0d75d407615c6638a (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
/* 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/. */

/**
 * The PaymentsStore class provides lightweight storage with an async publish/subscribe mechanism.
 * Synchronous state changes are batched to improve application performance and to reduce partial
 * state propagation.
 */

export default class PaymentsStore {
  /**
   * @param {object} [defaultState = {}] The initial state of the store.
   */
  constructor(defaultState = {}) {
    this._defaultState = Object.assign({}, defaultState);
    this._state = defaultState;
    this._nextNotifification = 0;
    this._subscribers = new Set();
  }

  /**
   * Get the current state as a shallow clone with a shallow freeze.
   * You shouldn't modify any part of the returned state object as that would bypass notifying
   * subscribers and could lead to subscribers assuming old state.
   *
   * @returns {Object} containing the current state
   */
  getState() {
    return Object.freeze(Object.assign({}, this._state));
  }

  /**
   * Used for testing to reset to the default state from the constructor.
   * @returns {Promise} returned by setState.
   */
  async reset() {
    return this.setState(this._defaultState);
  }

  /**
   * Augment the current state with the keys of `obj` and asynchronously notify
   * state subscribers. As a result, multiple synchronous state changes will lead
   * to a single subscriber notification which leads to better performance and
   * reduces partial state changes.
   *
   * @param {Object} obj The object to augment the state with. Keys in the object
   *                     will be shallow copied with Object.assign.
   *
   * @example If the state is currently {a:3} then setState({b:"abc"}) will result in a state of
   *          {a:3, b:"abc"}.
   */
  async setState(obj) {
    Object.assign(this._state, obj);
    let thisChangeNum = ++this._nextNotifification;

    // Let any synchronous setState calls that happen after the current setState call
    // complete first.
    // Their effects on the state will be batched up before the callback is actually called below.
    await Promise.resolve();

    // Don't notify for state changes that are no longer the most recent. We only want to call the
    // callback once with the latest state.
    if (thisChangeNum !== this._nextNotifification) {
      return;
    }

    for (let subscriber of this._subscribers) {
      try {
        subscriber.stateChangeCallback(this.getState());
      } catch (ex) {
        console.error(ex);
      }
    }
  }

  /**
   * Subscribe the object to state changes notifications via a `stateChangeCallback` method.
   *
   * @param {Object} component to receive state change callbacks via a `stateChangeCallback` method.
   *                           If the component is already subscribed, do nothing.
   */
  subscribe(component) {
    if (this._subscribers.has(component)) {
      return;
    }

    this._subscribers.add(component);
  }

  /**
   * @param {Object} component to stop receiving state change callbacks.
   */
  unsubscribe(component) {
    this._subscribers.delete(component);
  }
}