chromium/ios/web/annotations/resources/text_tasks.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Tasks and Idle state related utilities.
 */

// Interface for all real time operations. Can be mocked easily.
interface TaskTimer {
  clear(id: number): void;
  reset(then: Function, ms: number): number;
  now(): number;
}

// Real time TaskTimer.
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();
  }
}

// Minimum delay between two tasks.
const TASK_SEPARATOR_MS = 50;

// Minimum delay after user activity.
const TASK_ACTIVITY_DELAY_MS = 300;

// Task manager wrapper `TaskTimer`. It also monitors user's activity and pushes
// back the timer if needed.
// TODO(crbug.com/40936184): replace by requestIdleCallback when available or
// move to general ts utilities.
class IdleTaskTracker {
  // The events to monitor for user activity.
  private eventNames = [
    'gesturechange', 'gesturestart', 'mousemove', 'mousedown', 'touchmove',
    'touchstart', 'click', 'keydown', 'scroll'
  ];

  // Id for the current timer for the next task.
  private timerId = 0;

  // Keeps track of calls to `startActivityListeners` and
  // `stopActivityListeners` to properly install and remove event listeners.
  private activityListenersCount = 0;

  // The last time a user activity was recorded.
  public lastActivityMs = 0;

  // Map of all tasks, keyed by the task callback `Function`. `value` is the
  // current wake up time requested.
  private tasks = new Map<Function, number>();

  // Requires a `taskTimer` so it can be easily testable. `activityDelayMs` is
  // the delay between a user activity and when the next task should be
  // triggered (if any). `minimumTaskDelayMs` is an extra delay between two
  // tasks to avoid blocking the main thread for too long.
  constructor(
      private taskTimer: TaskTimer = new LiveTaskTimer(),
      private activityDelayMs = TASK_ACTIVITY_DELAY_MS,
      private minimumTaskDelayMs = TASK_SEPARATOR_MS) {
    this.lastActivityMs = taskTimer.now() - this.activityDelayMs;
  }

  // Queues the given `task` for execution when the page is idled, starting
  // `wakeUpDelayMs` from now. If the task exists, its new execution time is
  // updated.
  schedule(task: Function, wakeUpDelayMs = this.minimumTaskDelayMs): void {
    const taskStartMs = this.taskTimer.now() + wakeUpDelayMs;
    if (!this.tasks.has(task)) {
      // For each task, start activity listeners only once.
      this.startActivityListeners();
    }
    // Add to tasks and reschedule next wakeup time from new set of tasks.
    this.tasks.set(task, taskStartMs);
    this.setNextWakeup(this.nextWakeUpMs());
  }

  // Stops and clears this scheduler.
  shutdown(): void {
    this.setNextWakeup(0);
    this.activityListenersCount = 1;
    this.stopActivityListeners();
    this.tasks.clear();
  }

  // Starts all activity listeners. Will do it only once when
  // `activityListenersCount` goes from 0 to 1.
  startActivityListeners(): void {
    if (++this.activityListenersCount !== 1) {
      return;
    }
    this.eventNames.forEach((eventName) => {
      window.addEventListener(eventName, this.recordLastActivity);
    });
  }

  // Stops all activity listeners. Will do it only once when
  // `activityListenersCount` goes from 1 to 0.
  stopActivityListeners(): void {
    if (--this.activityListenersCount !== 0) {
      return;
    }
    this.eventNames.forEach((eventName) => {
      window.removeEventListener(eventName, this.recordLastActivity);
    });
  }

  // Returns the next task and its wakeup time.
  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];
  }

  // Returns next wakeup time based on current set of tasks.
  private nextWakeUpMs(): number {
    let [earlierTask, earlierTaskMs] = this.nextTask();
    if (!earlierTask)
      return 0;
    return earlierTaskMs;
  }

  // Resets timer for the new `wakeUpMs` wakeup time. A `wakeUp` time of 0 stops
  // the timer. Minimum delay will be `minimumTaskDelayMs` from now, to let user
  // some time between tasks.
  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);
    }
  }

  // Called when the timer triggers. It will check that enough idle time
  // has passed, if not schedule more time, then execute one task. Finally
  // will schedule next wakeup time based on tasks left.
  private onWakeUp = () => {
    const now = this.taskTimer.now();
    if (this.lastActivityMs + this.activityDelayMs > now) {
      this.setNextWakeup(this.lastActivityMs + this.activityDelayMs);
    } else {
      let [earlierTask] = this.nextTask();
      if (earlierTask) {
        this.tasks.delete(earlierTask);
        earlierTask();
        this.stopActivityListeners();
      }
      this.setNextWakeup(this.nextWakeUpMs());
    }
  };

  // Records last activity time event listener.
  private recordLastActivity = () => {
    this.lastActivityMs = this.taskTimer.now();
  };
}

export {
  TASK_SEPARATOR_MS,
  TASK_ACTIVITY_DELAY_MS,
  TaskTimer,
  LiveTaskTimer,
  IdleTaskTracker,
}