BigW Consortium Gitlab

smart_interval.js 4.57 KB
Newer Older
1 2 3 4 5
/**
 * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
 * and controllable by a public API.
 */

6
export default class SmartInterval {
7
  /**
Simon Knox committed
8 9
   * @param { function } opts.callback Function that returns a promise, called on each iteration
   *                     unless still in progress (required)
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
   * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
   * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
   * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
   *                         when the page is hidden
   * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
   * @param { boolean } opts.lazyStart Configure if timer is initialized on
   *                    instantiation or lazily
   * @param { boolean } opts.immediateExecution Configure if callback should
   *                    be executed before the first interval.
   */
  constructor(opts = {}) {
    this.cfg = {
      callback: opts.callback,
      startingInterval: opts.startingInterval,
      maxInterval: opts.maxInterval,
      hiddenInterval: opts.hiddenInterval,
      incrementByFactorOf: opts.incrementByFactorOf,
      lazyStart: opts.lazyStart,
      immediateExecution: opts.immediateExecution,
    };

    this.state = {
      intervalId: null,
      currentInterval: this.cfg.startingInterval,
      pageVisibility: 'visible',
    };

    this.initInterval();
  }
39

40
  /* public */
41

42 43 44
  start() {
    const cfg = this.cfg;
    const state = this.state;
45

46
    if (cfg.immediateExecution && !this.isLoading) {
47
      cfg.immediateExecution = false;
48
      this.triggerCallback();
49 50
    }

51
    state.intervalId = window.setInterval(() => {
52 53 54 55
      if (this.isLoading) {
        return;
      }
      this.triggerCallback();
56

57 58
      if (this.getCurrentInterval() === cfg.maxInterval) {
        return;
59 60
      }

61 62 63 64
      this.incrementInterval();
      this.resume();
    }, this.getCurrentInterval());
  }
65

66 67 68 69 70
  // cancel the existing timer, setting the currentInterval back to startingInterval
  cancel() {
    this.setCurrentInterval(this.cfg.startingInterval);
    this.stopTimer();
  }
71

72 73 74 75 76
  onVisibilityHidden() {
    if (this.cfg.hiddenInterval) {
      this.setCurrentInterval(this.cfg.hiddenInterval);
      this.resume();
    } else {
77 78
      this.cancel();
    }
79
  }
80

81 82
  // start a timer, using the existing interval
  resume() {
83
    this.stopTimer(); // stop existing timer, in case timer was not previously stopped
84 85
    this.start();
  }
86

87 88 89 90
  onVisibilityVisible() {
    this.cancel();
    this.start();
  }
91

92 93 94 95 96
  destroy() {
    this.cancel();
    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
    $(document).off('visibilitychange').off('beforeunload');
  }
97

98
  /* private */
99

100 101
  initInterval() {
    const cfg = this.cfg;
102

103 104
    if (!cfg.lazyStart) {
      this.start();
105 106
    }

107 108 109
    this.initVisibilityChangeHandling();
    this.initPageUnloadHandling();
  }
110

111 112 113 114 115 116
  triggerCallback() {
    this.isLoading = true;
    this.cfg.callback()
      .then(() => {
        this.isLoading = false;
      })
117
      .catch((err) => {
118
        this.isLoading = false;
Simon Knox committed
119
        throw err;
120 121 122
      });
  }

123 124 125 126
  initVisibilityChangeHandling() {
    // cancel interval when tab no longer shown (prevents cached pages from polling)
    document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
  }
127

128 129 130 131 132
  initPageUnloadHandling() {
    // TODO: Consider refactoring in light of turbolinks removal.
    // prevent interval continuing after page change, when kept in cache by Turbolinks
    $(document).on('beforeunload', () => this.cancel());
  }
133

134 135 136 137 138
  handleVisibilityChange(e) {
    this.state.pageVisibility = e.target.visibilityState;
    const intervalAction = this.isPageVisible() ?
      this.onVisibilityVisible :
      this.onVisibilityHidden;
139

140 141
    intervalAction.apply(this);
  }
142

143 144 145 146 147 148 149 150 151 152 153 154 155
  getCurrentInterval() {
    return this.state.currentInterval;
  }

  setCurrentInterval(newInterval) {
    this.state.currentInterval = newInterval;
  }

  incrementInterval() {
    const cfg = this.cfg;
    const currentInterval = this.getCurrentInterval();
    if (cfg.hiddenInterval && !this.isPageVisible()) return;
    let nextInterval = currentInterval * cfg.incrementByFactorOf;
156

157 158
    if (nextInterval > cfg.maxInterval) {
      nextInterval = cfg.maxInterval;
159 160
    }

161 162
    this.setCurrentInterval(nextInterval);
  }
163

164
  isPageVisible() { return this.state.pageVisibility === 'visible'; }
165

166 167 168 169
  stopTimer() {
    const state = this.state;

    state.intervalId = window.clearInterval(state.intervalId);
170
  }
171 172
}