debian-mirror-gitlab/app/assets/javascripts/smart_interval.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

219 lines
6.7 KiB
JavaScript
Raw Normal View History

2018-05-09 12:01:36 +05:30
import $ from 'jquery';
2017-09-10 17:25:29 +05:30
/**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
2021-09-30 23:02:18 +05:30
*
* This component has two intervals:
*
* - current interval - when the page is visible - defined by `startingInterval`, `maxInterval`, and `incrementByFactorOf`
* - Example:
* - `startingInterval: 10000`, `maxInterval: 240000`, `incrementByFactorOf: 2`
* - results in `10s, 20s, 40s, 80s, ..., 240s`, it stops increasing at `240s` and keeps this interval indefinitely.
* - hidden interval - when the page is not visible
*
* Visibility transitions:
*
* - `visible -> not visible`
* - `document.addEventListener('visibilitychange', () => ...)`
*
* > This event fires with a visibilityState of hidden when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile, switches from the browser to a different app.
*
* Source [Document: visibilitychange event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event)
*
* - `window.addEventListener('blur', () => ...)` - every time user clicks somewhere else then in the browser page
* - `not visible -> visible`
* - `document.addEventListener('visibilitychange', () => ...)` same as the transition `visible -> not visible`
* - `window.addEventListener('focus', () => ...)`
*
* The combination of these two listeners can result in an unexpected resumption of polling:
*
* - switch to a different window (causes `blur`)
* - switch to a different desktop (causes `visibilitychange` (not visible))
* - switch back to the original desktop (causes `visibilitychange` (visible))
* - *now the polling happens even in window that user doesn't work in*
2017-09-10 17:25:29 +05:30
*/
2018-03-17 18:26:18 +05:30
export default class SmartInterval {
2017-09-10 17:25:29 +05:30
/**
2018-03-17 18:26:18 +05:30
* @param { function } opts.callback Function that returns a promise, called on each iteration
* unless still in progress (required)
2017-09-10 17:25:29 +05:30
* @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,
2020-04-22 19:07:51 +05:30
pagevisibile: true,
2017-09-10 17:25:29 +05:30
};
this.initInterval();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
/* public */
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
start() {
2018-11-08 19:23:39 +05:30
const { cfg, state } = this;
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
if (cfg.immediateExecution && !this.isLoading) {
2017-09-10 17:25:29 +05:30
cfg.immediateExecution = false;
2018-03-17 18:26:18 +05:30
this.triggerCallback();
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
state.intervalId = window.setInterval(() => {
2018-03-17 18:26:18 +05:30
if (this.isLoading) {
return;
}
this.triggerCallback();
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
this.incrementInterval();
this.resume();
}, this.getCurrentInterval());
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// cancel the existing timer, setting the currentInterval back to startingInterval
cancel() {
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
2017-08-17 22:00:37 +05:30
this.cancel();
}
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// start a timer, using the existing interval
resume() {
2018-03-17 18:26:18 +05:30
this.stopTimer(); // stop existing timer, in case timer was not previously stopped
2017-09-10 17:25:29 +05:30
this.start();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
onVisibilityVisible() {
this.cancel();
this.start();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
destroy() {
2020-04-22 19:07:51 +05:30
document.removeEventListener('visibilitychange', this.onVisibilityChange);
window.removeEventListener('blur', this.onWindowVisibilityChange);
window.removeEventListener('focus', this.onWindowVisibilityChange);
2017-09-10 17:25:29 +05:30
this.cancel();
2021-02-22 17:27:13 +05:30
// eslint-disable-next-line @gitlab/no-global-event-off
2021-03-08 18:12:59 +05:30
$(document).off('visibilitychange').off('beforeunload');
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
/* private */
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
initInterval() {
2018-11-08 19:23:39 +05:30
const { cfg } = this;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (!cfg.lazyStart) {
this.start();
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
this.initVisibilityChangeHandling();
this.initPageUnloadHandling();
}
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
triggerCallback() {
this.isLoading = true;
2018-12-13 13:39:08 +05:30
this.cfg
.callback()
2018-03-17 18:26:18 +05:30
.then(() => {
this.isLoading = false;
})
2021-03-08 18:12:59 +05:30
.catch((err) => {
2018-03-17 18:26:18 +05:30
this.isLoading = false;
throw err;
});
}
2020-04-22 19:07:51 +05:30
onWindowVisibilityChange(e) {
this.state.pagevisibile = e.type === 'focus';
this.handleVisibilityChange();
}
onVisibilityChange(e) {
this.state.pagevisibile = e.target.visibilityState === 'visible';
this.handleVisibilityChange();
}
2017-09-10 17:25:29 +05:30
initVisibilityChangeHandling() {
2020-04-22 19:07:51 +05:30
// 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));
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
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());
}
2017-08-17 22:00:37 +05:30
2020-04-22 19:07:51 +05:30
handleVisibilityChange() {
2018-12-13 13:39:08 +05:30
const intervalAction = this.isPageVisible()
? this.onVisibilityVisible
: this.onVisibilityHidden;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
intervalAction.apply(this);
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
getCurrentInterval() {
return this.state.currentInterval;
}
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
2018-11-08 19:23:39 +05:30
const { cfg } = this;
2017-09-10 17:25:29 +05:30
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
this.setCurrentInterval(nextInterval);
}
2017-08-17 22:00:37 +05:30
2018-12-13 13:39:08 +05:30
isPageVisible() {
2020-04-22 19:07:51 +05:30
return this.state.pagevisibile;
2018-12-13 13:39:08 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
stopTimer() {
2018-11-08 19:23:39 +05:30
const { state } = this;
2017-09-10 17:25:29 +05:30
state.intervalId = window.clearInterval(state.intervalId);
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}