import taskQueue from "../../utils/taskQueue";
import { cancelIdleCb, reqIdleCb } from "./IdleDeadline";
const DEFAULT_MIN_TASK_TIME = 0;
/**
* a queue system to utilize browser idle time to run tasks.
* @author: Aamir khan
*/
class IdliQueue {
constructor({
ensureTasksRun = false,
defaultMinTaskTime = DEFAULT_MIN_TASK_TIME,
} = {}) {
this.idliHandle = null;
this.Queue_ = [];
this.isProcessing_ = false;
this.state_ = null;
this.defaultMinTaskTime_ = defaultMinTaskTime;
this.ensureTasksRun_ = ensureTasksRun;
// Bind methods
this.runTasksImmediately = this.runTasksImmediately.bind(this);
this.runTasks_ = this.runTasks_.bind(this);
this.onVisibilityChange_ = this.onVisibilityChange_.bind(this);
if (this.ensureTasksRun_) {
// Note: Safari does not reliably fire the `pagehide` or `visibilitychange`
// for now we are only using chrome devices so we can use `visibilitychange`
global.addEventListener('visibilitychange', this.onVisibilityChange_, true);
}
}
/**
* @param {...*} args
*/
pushTask(...args) {
this.addTask_(Array.prototype.push, ...args);
}
/**
* @param {...*} args
*/
unshiftTask(...args) {
this.addTask_(Array.prototype.unshift, ...args);
}
/**
* Runs all scheduled tasks synchronously.
*/
runTasksImmediately() {
// By not passing a deadline, all tasks will be run sync.
this.runTasks_();
}
/**
* @return {boolean}
*/
hasPendingTasks() {
return this.Queue_.length > 0;
}
/**
* Clears all pending tasks for the queue and stops any scheduled tasks
* from running.
*/
clearPendingTasks() {
this.Queue_ = [];
this.cancelScheduledRun_();
}
/**
* Returns the state object for the currently running task. If no task is
* running, null is returned.
* @return {Object}
*/
getState() {
return this.state_;
}
/**
* Destroys the instance by unregistering all added event listeners and
* removing any overridden methods.
*/
destroy() {
this.Queue_ = [];
this.cancelScheduledRun_();
if (this.ensureTasksRun_) {
global.removeEventListener('visibilitychange', this.onVisibilityChange_, true);
}
}
/**
* @param {!Function} arrayMethod Either the Array.prototype{push|shift}.
* @param {!Function} task
* @param {{minTaskTime: number}=} param1
* @private
*/
addTask_(arrayMethod, task, { minTaskTime = this.defaultMinTaskTime_ } = {}) {
const state = {
time: Date.now(),
visibilityState: document.visibilityState,
};
arrayMethod.call(this.Queue_, { state, task, minTaskTime });
this.scheduleTasksToRun_();
}
/**
* Schedules the task queue to be processed. If the document is in the
* hidden state, they queue is scheduled as a microtask so it can be run
* in cases where a macrotask couldn't (like if the page is unloading). If
* the document is in the visible state, `requestIdleCallback` is used.
* @private
*/
scheduleTasksToRun_() {
if (this.ensureTasksRun_ && document.visibilityState === 'hidden') {
taskQueue(this.runTasks_);
} else {
if (!this.idliHandle) {
this.idliHandle = reqIdleCb(this.runTasks_);
}
}
}
/**
* Runs as many tasks in the queue as it can before reaching the
* deadline. If no deadline is passed, it will run all tasks.
* If an `IdleDeadline` object is passed (as is with `requestIdleCallback`)
* then the tasks are run until there's no time remaining, at which point
* we yield to input or other script and wait until the next idle time.
* @param {IdleDeadline=} deadline
* @private
*/
runTasks_(deadline = undefined) {
this.cancelScheduledRun_();
if (!this.isProcessing_) {
this.isProcessing_ = true;
// Process tasks until there's no time left or we need to yield to input.
while (this.hasPendingTasks() &&
!shouldWeYield(deadline, this.Queue_[0].minTaskTime)) {
const { task, state } = this.Queue_.shift();
this.state_ = state;
task(state);
this.state_ = null;
}
this.isProcessing_ = false;
if (this.hasPendingTasks()) {
// Schedule the rest of the tasks for the next idle time.
this.scheduleTasksToRun_();
}
}
}
/**
* Cancels any scheduled idle callback and removes the handler (if set).
* @private
*/
cancelScheduledRun_() {
cancelIdleCb(this.idliHandle);
this.idliHandle = null;
}
/**
* A callback for the `visibilitychange` event that runs all pending
* callbacks immediately if the document's visibility state is hidden.
* @private
*/
onVisibilityChange_() {
if (document.visibilityState === 'hidden') {
this.runTasksImmediately();
}
}
}
/**
* Returns true if the IdleDealine object exists and the remaining time is
* less or equal to than the minTaskTime. Otherwise returns false.
* @param {IdleDeadline|undefined} deadline
* @param {number} minTaskTime
* @return {boolean}
* @private
*/
const shouldWeYield = (deadline, minTaskTime) => {
return deadline && deadline.timeRemaining() <= minTaskTime
};
export default IdliQueue;