smart_interval.js.es6 3.45 KB
Newer Older
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
/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */

(() => {
  class SmartInterval {
    /**
      * @param { function } callback Function to be called on each iteration (required)
      * @param { milliseconds } startingInterval `currentInterval` is set to this initially
      * @param { milliseconds } maxInterval `currentInterval` will be incremented to this
      * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
      * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
      */
    constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
      this.cfg = {
        callback,
        startingInterval,
        maxInterval,
        incrementByFactorOf,
        lazyStart,
      };

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

      this.initInterval();
    }
    /* public */

    start() {
      const cfg = this.cfg;
      const state = this.state;

      state.intervalId = window.setInterval(() => {
        cfg.callback();

        if (this.getCurrentInterval() === cfg.maxInterval) {
          return;
        }

        this.incrementInterval();
        this.resume();
      }, this.getCurrentInterval());
    }

    // cancel the existing timer, setting the currentInterval back to startingInterval
    cancel() {
      this.setCurrentInterval(this.cfg.startingInterval);
      this.stopTimer();
    }

    // start a timer, using the existing interval
    resume() {
      this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
      this.start();
    }

    destroy() {
      this.cancel();
      $(document).off('visibilitychange').off('page:before-unload');
    }

    /* private */

    initInterval() {
      const cfg = this.cfg;

      if (!cfg.lazyStart) {
        this.start();
      }

      this.initVisibilityChangeHandling();
      this.initPageUnloadHandling();
    }

    initVisibilityChangeHandling() {
      // cancel interval when tab no longer shown (prevents cached pages from polling)
      $(document)
        .off('visibilitychange').on('visibilitychange', (e) => {
          this.state.pageVisibility = e.target.visibilityState;
          this.handleVisibilityChange();
        });
    }

    initPageUnloadHandling() {
      // prevent interval continuing after page change, when kept in cache by Turbolinks
      $(document).on('page:before-unload', () => this.cancel());
    }

    handleVisibilityChange() {
      const state = this.state;

      const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;

      intervalAction.apply(this);
    }

    getCurrentInterval() {
      return this.state.currentInterval;
    }

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

    incrementInterval() {
      const cfg = this.cfg;
      const currentInterval = this.getCurrentInterval();
      let nextInterval = currentInterval * cfg.incrementByFactorOf;

      if (nextInterval > cfg.maxInterval) {
        nextInterval = cfg.maxInterval;
      }

      this.setCurrentInterval(nextInterval);
    }

    stopTimer() {
      const state = this.state;

      state.intervalId = window.clearInterval(state.intervalId);
    }
  }
  gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));