Source: modules/base/task_scheduler.js

/**
 * TaskScheduler helps manage tasks and ensures they are executed in
 * sequential order. When a task of a certain type is enqueued, all pending
 * tasks of the same type in the queue are removed. This avoids redundant
 * queries and improves user perceived performance.
 *
 * @module modules/base/task_scheduler
 */
define(function(require) {
  'use strict';

  var Defer = require('modules/defer');
  var Module = require('modules/base/module');
  var Observable = require('modules/mvvm/observable');

  /**
   * @class TaskScheduler
   * @returns {TaskScheduler}
   */
  var TaskScheduler = Module.create(function TaskScheduler() {
    this.super(Observable).call(this);
    this.isLocked = false;
    this._tasks = [];
  }).extend(Observable);

  Observable.defineObservableProperty(TaskScheduler.prototype, 'isLocked', {
    value: false
  });

  const TASK_TYPE = {
    GENERAL: 0
  };

  /**
   * We will keep some constants here as a static variable
   *
   * @access public
   * @memberOf TaskScheduler
   */
  Object.defineProperty(TaskScheduler, 'TASK_TYPE', {
    enumerable: true,
    configurable: false,
    get: function() {
      return TASK_TYPE;
    }
  });

  /**
   * Change the internal state to make sure we are locked.
   *
   * @access private
   * @memberOf TaskScheduler
   */
  TaskScheduler.prototype._lock = function() {
    this.isLocked = true;
  };

  /**
   * Change the internal state to unlocked and execute next task.
   *
   * @access private
   * @memberOf TaskScheduler
   */
  TaskScheduler.prototype._unlock = function() {
    this.isLocked = false;
    this._executeNextTask();
  };

  /**
   * We will remove any redundant same-type tasks and keep the remaining ones
   * including the others which are not cancelable (general use task).
   *
   * @access private
   * @memberOf TaskScheduler
   * @param {String} type
   * @return {Array} array of tasks
   */
  TaskScheduler.prototype._removeRedundantTasks = function(type) {
    return this._tasks.filter((task) => {
      if (!task.cancelable) {
        return true;
      } else {
        return task.type !== type;
      }
    });
  };

  /**
   * Execute the next task when unlocked.
   *
   * @access private
   * @returns {Promise}
   * @memberOf TaskScheduler
   */
  TaskScheduler.prototype._executeNextTask = function() {
    if (this.isLocked) {
      return;
    }
    var nextTask = this._tasks.shift();
    if (nextTask) {
      this._lock();
      return nextTask.func().then((result) => {
        nextTask.defer.resolve(result);
        this._unlock();
      }, () => {
        nextTask.defer.reject();
        this._unlock();
      });
    }
  };

  /**
   * This is the only entry point for caller, and we will enqueue all tasks
   * one by one and execute them in order.
   *
   * @access public
   * @memberOf TaskScheduler
   * @param {Object} task
   * @param {String} task.type
   * @param {Function} task.func - make sure the func would return a promise
   * @param {Boolean} task.cancelable
   * @return {Promise}
   */
  TaskScheduler.prototype.enqueue = function(task) {
    var defer = Defer();

    // if there is no func, let's directly resolve this task
    if (!task.func) {
      return Promise.resolve();
    }

    if (!task.type) {
      // copy the task and make sure we won't change the previous one
      task = Object.assign({}, task);
      task.type = TaskScheduler.TASK_TYPE.GENERAL;
    }

    task.defer = defer;
    this._tasks = this._removeRedundantTasks(task.type);
    this._tasks.push(task);
    this._executeNextTask();

    return defer.promise;
  };

  return TaskScheduler;
});