import $ from 'jquery'; /** * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable * and controllable by a public API. */ export default class SmartInterval { /** * @param { function } opts.callback Function that returns a promise, called on each iteration * unless still in progress (required) * @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, pagevisibile: true, }; this.initInterval(); } /* public */ start() { const { cfg, state } = this; if (cfg.immediateExecution && !this.isLoading) { cfg.immediateExecution = false; this.triggerCallback(); } state.intervalId = window.setInterval(() => { if (this.isLoading) { return; } this.triggerCallback(); 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(); } onVisibilityHidden() { if (this.cfg.hiddenInterval) { this.setCurrentInterval(this.cfg.hiddenInterval); this.resume(); } else { this.cancel(); } } // start a timer, using the existing interval resume() { this.stopTimer(); // stop existing timer, in case timer was not previously stopped this.start(); } onVisibilityVisible() { this.cancel(); this.start(); } destroy() { document.removeEventListener('visibilitychange', this.onVisibilityChange); window.removeEventListener('blur', this.onWindowVisibilityChange); window.removeEventListener('focus', this.onWindowVisibilityChange); this.cancel(); // eslint-disable-next-line @gitlab/no-global-event-off $(document) .off('visibilitychange') .off('beforeunload'); } /* private */ initInterval() { const { cfg } = this; if (!cfg.lazyStart) { this.start(); } this.initVisibilityChangeHandling(); this.initPageUnloadHandling(); } triggerCallback() { this.isLoading = true; this.cfg .callback() .then(() => { this.isLoading = false; }) .catch(err => { this.isLoading = false; throw err; }); } onWindowVisibilityChange(e) { this.state.pagevisibile = e.type === 'focus'; this.handleVisibilityChange(); } onVisibilityChange(e) { this.state.pagevisibile = e.target.visibilityState === 'visible'; this.handleVisibilityChange(); } initVisibilityChangeHandling() { // cancel interval when tab or window is no longer shown (prevents cached pages from polling) document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this)); window.addEventListener('blur', this.onWindowVisibilityChange.bind(this)); window.addEventListener('focus', this.onWindowVisibilityChange.bind(this)); } 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()); } handleVisibilityChange() { const intervalAction = this.isPageVisible() ? this.onVisibilityVisible : this.onVisibilityHidden; intervalAction.apply(this); } getCurrentInterval() { return this.state.currentInterval; } setCurrentInterval(newInterval) { this.state.currentInterval = newInterval; } incrementInterval() { const { cfg } = this; const currentInterval = this.getCurrentInterval(); if (cfg.hiddenInterval && !this.isPageVisible()) return; let nextInterval = currentInterval * cfg.incrementByFactorOf; if (nextInterval > cfg.maxInterval) { nextInterval = cfg.maxInterval; } this.setCurrentInterval(nextInterval); } isPageVisible() { return this.state.pagevisibile; } stopTimer() { const { state } = this; state.intervalId = window.clearInterval(state.intervalId); } }