* @fileoverview Tasks and Idle state related utilities.
*/
export interface TaskTimer {
clear(id: number): void;
reset(then: Function, ms: number): number;
now(): number;
}
export class LiveTaskTimer implements TaskTimer {
clear(id: number): void {
clearTimeout(id);
}
reset(then: Function, ms: number): number {
return setTimeout(then, ms);
}
now(): number {
return Date.now();
}
}
export const TASK_SEPARATOR_MS = 50;
export const TASK_ACTIVITY_DELAY_MS = 300;
export class IdleTaskTracker {
private eventNames = [
'gesturechange',
'gesturestart',
'mousemove',
'mousedown',
'touchmove',
'touchstart',
'click',
'keydown',
'scroll',
];
private timerId = 0;
private activityListenersCount = 0;
lastActivityMs = 0;
private tasks = new Map<Function, number>();
constructor(
private taskTimer: TaskTimer = new LiveTaskTimer(),
private activityDelayMs = TASK_ACTIVITY_DELAY_MS,
private minimumTaskDelayMs = TASK_SEPARATOR_MS) {
this.lastActivityMs = taskTimer.now() - this.activityDelayMs;
}
schedule(task: Function, wakeUpDelayMs = this.minimumTaskDelayMs): void {
const taskStartMs = this.taskTimer.now() + wakeUpDelayMs;
if (!this.tasks.has(task)) {
this.startActivityListeners();
}
this.tasks.set(task, taskStartMs);
this.setNextWakeup(this.nextWakeUpMs());
}
shutdown(): void {
this.setNextWakeup(0);
this.activityListenersCount = 1;
this.stopActivityListeners();
this.tasks.clear();
}
startActivityListeners(): void {
if (++this.activityListenersCount !== 1) {
return;
}
this.eventNames.forEach((eventName) => {
window.addEventListener(eventName, this.recordLastActivity);
});
}
stopActivityListeners(): void {
if (--this.activityListenersCount !== 0) {
return;
}
this.eventNames.forEach((eventName) => {
window.removeEventListener(eventName, this.recordLastActivity);
});
}
private nextTask(): [Function|null, number] {
let earlierTaskMs = 0;
let earlierTask: Function|null = null;
this.tasks.forEach((taskMs: number, task: Function) => {
if (!earlierTask || taskMs < earlierTaskMs) {
earlierTaskMs = taskMs;
earlierTask = task;
}
});
return [earlierTask, earlierTaskMs];
}
private nextWakeUpMs(): number {
const [earlierTask, earlierTaskMs] = this.nextTask();
if (!earlierTask) {
return 0;
}
return earlierTaskMs;
}
private setNextWakeup(wakeUpMs: number): void {
if (this.timerId) {
this.taskTimer.clear(this.timerId);
this.timerId = 0;
}
if (wakeUpMs > 0) {
const now = this.taskTimer.now();
wakeUpMs = Math.max(wakeUpMs, now + this.minimumTaskDelayMs);
this.timerId = this.taskTimer.reset(this.onWakeUp, wakeUpMs - now);
}
}
private onWakeUp = () => {
const now = this.taskTimer.now();
if (this.lastActivityMs + this.activityDelayMs > now) {
this.setNextWakeup(this.lastActivityMs + this.activityDelayMs);
} else {
const [earlierTask] = this.nextTask();
if (earlierTask) {
this.tasks.delete(earlierTask);
earlierTask();
this.stopActivityListeners();
}
this.setNextWakeup(this.nextWakeUpMs());
}
};
private recordLastActivity = () => {
this.lastActivityMs = this.taskTimer.now();
};
}