173 lines
4.6 KiB
JavaScript
173 lines
4.6 KiB
JavaScript
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,
|
|
pageVisibility: 'visible',
|
|
};
|
|
|
|
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() {
|
|
this.cancel();
|
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
$(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;
|
|
});
|
|
}
|
|
|
|
initVisibilityChangeHandling() {
|
|
// cancel interval when tab no longer shown (prevents cached pages from polling)
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange.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(e) {
|
|
this.state.pageVisibility = e.target.visibilityState;
|
|
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.pageVisibility === 'visible'; }
|
|
|
|
stopTimer() {
|
|
const { state } = this;
|
|
|
|
state.intervalId = window.clearInterval(state.intervalId);
|
|
}
|
|
}
|
|
|