text: // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview A singleton object for managing Javascript code modules.
   *
   */
  
  goog.provide('goog.module.ModuleManager');
  goog.provide('goog.module.ModuleManager.CallbackType');
  goog.provide('goog.module.ModuleManager.FailureType');
  
  goog.require('goog.Disposable');
  goog.require('goog.array');
  goog.require('goog.asserts');
  goog.require('goog.async.Deferred');
  goog.require('goog.debug.Trace');
  /** @suppress {extraRequire} */
  goog.require('goog.dispose');
  goog.require('goog.log');
  /** @suppress {extraRequire} */
  goog.require('goog.module');
  goog.require('goog.module.ModuleInfo');
  goog.require('goog.module.ModuleLoadCallback');
  goog.require('goog.object');
  
  
  
  /**
   * The ModuleManager keeps track of all modules in the environment.
   * Since modules may not have their code loaded, we must keep track of them.
   * @constructor
   * @extends {goog.Disposable}
   * @struct
   */
  goog.module.ModuleManager = function() {
    goog.module.ModuleManager.base(this, 'constructor');
  
    /**
     * A mapping from module id to ModuleInfo object.
     * @private {Object<string, !goog.module.ModuleInfo>}
     */
    this.moduleInfoMap_ = {};
  
    // TODO (malteubl): Switch this to a reentrant design.
    /**
     * The ids of the currently loading modules. If batch mode is disabled, then
     * this array will never contain more than one element at a time.
     * @type {Array<string>}
     * @private
     */
    this.loadingModuleIds_ = [];
  
    /**
     * The requested ids of the currently loading modules. This does not include
     * module dependencies that may also be loading.
     * @type {Array<string>}
     * @private
     */
    this.requestedLoadingModuleIds_ = [];
  
    // TODO(user): Make these and other arrays that are used as sets be
    // actual sets.
    /**
     * All module ids that have ever been requested. In concurrent loading these
     * are the ones to subtract from future requests.
     * @type {!Array<string>}
     * @private
     */
    this.requestedModuleIds_ = [];
  
    /**
     * A queue of the ids of requested but not-yet-loaded modules. The zero
     * position is the front of the queue. This is a 2-D array to group modules
     * together with other modules that should be batch loaded with them, if
     * batch loading is enabled.
     * @type {Array<Array<string>>}
     * @private
     */
    this.requestedModuleIdsQueue_ = [];
  
    /**
     * The ids of the currently loading modules which have been initiated by user
     * actions.
     * @type {Array<string>}
     * @private
     */
    this.userInitiatedLoadingModuleIds_ = [];
  
    /**
     * A map of callback types to the functions to call for the specified
     * callback type.
     * @type {Object<goog.module.ModuleManager.CallbackType, Array<Function>>}
     * @private
     */
    this.callbackMap_ = {};
  
    /**
     * Module info for the base module (the one that contains the module
     * manager code), which we set as the loading module so one can
     * register initialization callbacks in the base module.
     *
     * The base module is considered loaded when #setAllModuleInfo is called or
     * #setModuleContext is called, whichever comes first.
     *
     * @type {goog.module.ModuleInfo}
     * @private
     */
    this.baseModuleInfo_ = new goog.module.ModuleInfo([], '');
  
    /**
     * The module that is currently loading, or null if not loading anything.
     * @type {goog.module.ModuleInfo}
     * @private
     */
    this.currentlyLoadingModule_ = this.baseModuleInfo_;
  
    /**
     * The id of the last requested initial module. When it loaded
     * the deferred in {@code this.initialModulesLoaded_} resolves.
     * @private {?string}
     */
    this.lastInitialModuleId_ = null;
  
    /**
     * Deferred for when all initial modules have loaded. We currently block
     * sending additional module requests until this deferred resolves. In a
     * future optimization it may be possible to use the initial modules as
     * seeds for the module loader "requested module ids" and start making new
     * requests even sooner.
     * @private {!goog.async.Deferred}
     */
    this.initialModulesLoaded_ = new goog.async.Deferred();
  
    /**
     * A logger.
     * @private {goog.log.Logger}
     */
    this.logger_ = goog.log.getLogger('goog.module.ModuleManager');
  
    /**
     * Whether the batch mode (i.e. the loading of multiple modules with just one
     * request) has been enabled.
     * @private {boolean}
     */
    this.batchModeEnabled_ = false;
  
    /**
     * Whether the module requests may be sent out of order.
     * @private {boolean}
     */
    this.concurrentLoadingEnabled_ = false;
  
    /**
     * A loader for the modules that implements loadModules(ids, moduleInfoMap,
     * opt_successFn, opt_errorFn, opt_timeoutFn, opt_forceReload) method.
     * @private {goog.module.AbstractModuleLoader}
     */
    this.loader_ = null;
  
    // TODO(user): Remove tracer.
    /**
     * Tracer that measures how long it takes to load a module.
     * @private {?number}
     */
    this.loadTracer_ = null;
  
    /**
     * The number of consecutive failures that have happened upon module load
     * requests.
     * @private {number}
     */
    this.consecutiveFailures_ = 0;
  
    /**
     * Determines if the module manager was just active before the processing of
     * the last data.
     * @private {boolean}
     */
    this.lastActive_ = false;
  
    /**
     * Determines if the module manager was just user active before the processing
     * of the last data. The module manager is user active if any of the
     * user-initiated modules are loading or queued up to load.
     * @private {boolean}
     */
    this.userLastActive_ = false;
  
    /**
     * The module context needed for module initialization.
     * @private {Object}
     */
    this.moduleContext_ = null;
  };
  goog.inherits(goog.module.ModuleManager, goog.Disposable);
  goog.addSingletonGetter(goog.module.ModuleManager);
  
  
  /**
  * The type of callbacks that can be registered with the module manager,.
  * @enum {string}
  */
  goog.module.ModuleManager.CallbackType = {
    /**
     * Fired when an error has occurred.
     */
    ERROR: 'error',
  
    /**
     * Fired when it becomes idle and has no more module loads to process.
     */
    IDLE: 'idle',
  
    /**
     * Fired when it becomes active and has module loads to process.
     */
    ACTIVE: 'active',
  
    /**
     * Fired when it becomes idle and has no more user-initiated module loads to
     * process.
     */
    USER_IDLE: 'userIdle',
  
    /**
     * Fired when it becomes active and has user-initiated module loads to
     * process.
     */
    USER_ACTIVE: 'userActive'
  };
  
  
  /**
   * A non-HTTP status code indicating a corruption in loaded module.
   * This should be used by a ModuleLoader as a replacement for the HTTP code
   * given to the error handler function to indicated that the module was
   * corrupted.
   * This will set the forceReload flag on the loadModules method when retrying
   * module loading.
   * @type {number}
   */
  goog.module.ModuleManager.CORRUPT_RESPONSE_STATUS_CODE = 8001;
  
  
  /**
   * Sets the batch mode as enabled or disabled for the module manager.
   * @param {boolean} enabled Whether the batch mode is to be enabled or not.
   */
  goog.module.ModuleManager.prototype.setBatchModeEnabled = function(enabled) {
    this.batchModeEnabled_ = enabled;
  };
  
  
  /**
   * Sets the concurrent loading mode as enabled or disabled for the module
   * manager. Requires a moduleloader implementation that supports concurrent
   * loads. The default {@see goog.module.ModuleLoader} does not.
   * @param {boolean} enabled
   */
  goog.module.ModuleManager.prototype.setConcurrentLoadingEnabled = function(
      enabled) {
    this.concurrentLoadingEnabled_ = enabled;
  };
  
  
  /**
   * Sets the module info for all modules. Should only be called once.
   *
   * @param {Object<Array<string>>} infoMap An object that contains a mapping
   *    from module id (String) to list of required module ids (Array).
   */
  goog.module.ModuleManager.prototype.setAllModuleInfo = function(infoMap) {
    for (var id in infoMap) {
      this.moduleInfoMap_[id] = new goog.module.ModuleInfo(infoMap[id], id);
    }
    if (!this.initialModulesLoaded_.hasFired()) {
      this.initialModulesLoaded_.callback();
    }
    this.maybeFinishBaseLoad_();
  };
  
  
  /**
   * Sets the module info for all modules. Should only be called once. Also
   * marks modules that are currently being loaded.
   *
   * @param {string=} opt_info A string representation of the module dependency
   *      graph, in the form: module1:dep1,dep2/module2:dep1,dep2 etc.
   *     Where depX is the base-36 encoded position of the dep in the module list.
   * @param {Array<string>=} opt_loadingModuleIds A list of moduleIds that
   *     are currently being loaded.
   */
  goog.module.ModuleManager.prototype.setAllModuleInfoString = function(
      opt_info, opt_loadingModuleIds) {
    if (!goog.isString(opt_info)) {
      // The call to this method is generated in two steps, the argument is added
      // after some of the compilation passes.  This means that the initial code
      // doesn't have any arguments and causes compiler errors.  We make it
      // optional to satisfy this constraint.
      return;
    }
  
    var modules = opt_info.split('/');
    var moduleIds = [];
  
    // Split the string into the infoMap of id->deps
    for (var i = 0; i < modules.length; i++) {
      var parts = modules[i].split(':');
      var id = parts[0];
      var deps;
      if (parts[1]) {
        deps = parts[1].split(',');
        for (var j = 0; j < deps.length; j++) {
          var index = parseInt(deps[j], 36);
          goog.asserts.assert(
              moduleIds[index], 'No module @ %s, dep of %s @ %s', index, id, i);
          deps[j] = moduleIds[index];
        }
      } else {
        deps = [];
      }
      moduleIds.push(id);
      this.moduleInfoMap_[id] = new goog.module.ModuleInfo(deps, id);
    }
    if (opt_loadingModuleIds && opt_loadingModuleIds.length) {
      goog.array.extend(this.loadingModuleIds_, opt_loadingModuleIds);
      // The last module in the list of initial modules. When it has loaded all
      // initial modules have loaded.
      this.lastInitialModuleId_ =
          /** @type {?string}  */ (goog.array.peek(opt_loadingModuleIds));
    } else {
      if (!this.initialModulesLoaded_.hasFired()) {
        this.initialModulesLoaded_.callback();
      }
    }
    this.maybeFinishBaseLoad_();
  };
  
  
  /**
   * Gets a module info object by id.
   * @param {string} id A module identifier.
   * @return {!goog.module.ModuleInfo} The module info.
   */
  goog.module.ModuleManager.prototype.getModuleInfo = function(id) {
    return this.moduleInfoMap_[id];
  };
  
  
  /**
   * Sets the module uris.
   *
   * @param {Object} moduleUriMap The map of id/uris pairs for each module.
   */
  goog.module.ModuleManager.prototype.setModuleUris = function(moduleUriMap) {
    for (var id in moduleUriMap) {
      this.moduleInfoMap_[id].setUris(moduleUriMap[id]);
    }
  };
  
  
  /**
   * Gets the application-specific module loader.
   * @return {goog.module.AbstractModuleLoader} An object that has a
   *     loadModules(ids, moduleInfoMap, opt_successFn, opt_errFn,
   *         opt_timeoutFn, opt_forceReload) method.
   */
  goog.module.ModuleManager.prototype.getLoader = function() {
    return this.loader_;
  };
  
  
  /**
   * Sets the application-specific module loader.
   * @param {goog.module.AbstractModuleLoader} loader An object that has a
   *     loadModules(ids, moduleInfoMap, opt_successFn, opt_errFn,
   *         opt_timeoutFn, opt_forceReload) method.
   */
  goog.module.ModuleManager.prototype.setLoader = function(loader) {
    this.loader_ = loader;
  };
  
  
  /**
   * Gets the module context to use to initialize the module.
   * @return {Object} The context.
   */
  goog.module.ModuleManager.prototype.getModuleContext = function() {
    return this.moduleContext_;
  };
  
  
  /**
   * Sets the module context to use to initialize the module.
   * @param {Object} context The context.
   */
  goog.module.ModuleManager.prototype.setModuleContext = function(context) {
    this.moduleContext_ = context;
    this.maybeFinishBaseLoad_();
  };
  
  
  /**
   * Determines if the ModuleManager is active
   * @return {boolean} TRUE iff the ModuleManager is active (i.e., not idle).
   */
  goog.module.ModuleManager.prototype.isActive = function() {
    return this.loadingModuleIds_.length > 0;
  };
  
  
  /**
   * Determines if the ModuleManager is user active
   * @return {boolean} TRUE iff the ModuleManager is user active (i.e., not idle).
   */
  goog.module.ModuleManager.prototype.isUserActive = function() {
    return this.userInitiatedLoadingModuleIds_.length > 0;
  };
  
  
  /**
   * Dispatches an ACTIVE or IDLE event if necessary.
   * @private
   */
  goog.module.ModuleManager.prototype.dispatchActiveIdleChangeIfNeeded_ =
      function() {
    var lastActive = this.lastActive_;
    var active = this.isActive();
    if (active != lastActive) {
      this.executeCallbacks_(
          active ? goog.module.ModuleManager.CallbackType.ACTIVE :
                   goog.module.ModuleManager.CallbackType.IDLE);
  
      // Flip the last active value.
      this.lastActive_ = active;
    }
  
    // Check if the module manager is user active i.e., there are user initiated
    // modules being loaded or queued up to be loaded.
    var userLastActive = this.userLastActive_;
    var userActive = this.isUserActive();
    if (userActive != userLastActive) {
      this.executeCallbacks_(
          userActive ? goog.module.ModuleManager.CallbackType.USER_ACTIVE :
                       goog.module.ModuleManager.CallbackType.USER_IDLE);
  
      // Flip the last user active value.
      this.userLastActive_ = userActive;
    }
  };
  
  
  /**
   * Preloads a module after a short delay.
   *
   * @param {string} id The id of the module to preload.
   * @param {number=} opt_timeout The number of ms to wait before adding the
   *     module id to the loading queue (defaults to 0 ms). Note that the module
   *     will be loaded asynchronously regardless of the value of this parameter.
   * @return {!goog.async.Deferred} A deferred object.
   */
  goog.module.ModuleManager.prototype.preloadModule = function(id, opt_timeout) {
    var d = new goog.async.Deferred();
    window.setTimeout(
        goog.bind(this.addLoadModule_, this, id, d), opt_timeout || 0);
    return d;
  };
  
  
  /**
   * Prefetches a JavaScript module and its dependencies, which means that the
   * module will be downloaded, but not evaluated. To complete the module load,
   * the caller should also call load or execOnLoad after prefetching the module.
   *
   * @param {string} id The id of the module to prefetch.
   */
  goog.module.ModuleManager.prototype.prefetchModule = function(id) {
    var moduleInfo = this.getModuleInfo(id);
    if (moduleInfo.isLoaded() || this.isModuleLoading(id)) {
      throw Error('Module load already requested: ' + id);
    } else if (this.batchModeEnabled_) {
      throw Error('Modules prefetching is not supported in batch mode');
    } else {
      var idWithDeps = this.getNotYetLoadedTransitiveDepIds_(id);
      for (var i = 0; i < idWithDeps.length; i++) {
        this.loader_.prefetchModule(
            idWithDeps[i], this.moduleInfoMap_[idWithDeps[i]]);
      }
    }
  };
  
  
  /**
   * Loads a single module for use with a given deferred.
   *
   * @param {string} id The id of the module to load.
   * @param {goog.async.Deferred} d A deferred object.
   * @private
   */
  goog.module.ModuleManager.prototype.addLoadModule_ = function(id, d) {
    var moduleInfo = this.getModuleInfo(id);
    if (moduleInfo.isLoaded()) {
      d.callback(this.moduleContext_);
      return;
    }
  
    this.registerModuleLoadCallbacks_(id, moduleInfo, false, d);
    if (!this.isModuleLoading(id)) {
      this.loadModulesOrEnqueue_([id]);
    }
  };
  
  
  /**
   * Loads a list of modules or, if some other module is currently being loaded,
   * appends the ids to the queue of requested module ids. Registers callbacks a
   * module that is currently loading and returns a fired deferred for a module
   * that is already loaded.
   *
   * @param {Array<string>} ids The id of the module to load.
   * @param {boolean=} opt_userInitiated If the load is a result of a user action.
   * @return {!Object<string, !goog.async.Deferred>} A mapping from id (String)
   *     to deferred objects that will callback or errback when the load for that
   *     id is finished.
   * @private
   */
  goog.module.ModuleManager.prototype.loadModulesOrEnqueueIfNotLoadedOrLoading_ =
      function(ids, opt_userInitiated) {
    var uniqueIds = [];
    goog.array.removeDuplicates(ids, uniqueIds);
    var idsToLoad = [];
    var deferredMap = {};
    for (var i = 0; i < uniqueIds.length; i++) {
      var id = uniqueIds[i];
      var moduleInfo = this.getModuleInfo(id);
      if (!moduleInfo) {
        throw new Error('Unknown module: ' + id);
      }
      var d = new goog.async.Deferred();
      deferredMap[id] = d;
      if (moduleInfo.isLoaded()) {
        d.callback(this.moduleContext_);
      } else {
        this.registerModuleLoadCallbacks_(id, moduleInfo, !!opt_userInitiated, d);
        if (!this.isModuleLoading(id)) {
          idsToLoad.push(id);
        }
      }
    }
  
    // If there are ids to load, load them, otherwise, they are all loading or
    // loaded.
    if (idsToLoad.length > 0) {
      this.loadModulesOrEnqueue_(idsToLoad);
    }
    return deferredMap;
  };
  
  
  /**
   * Registers the callbacks and handles logic if it is a user initiated module
   * load.
   *
   * @param {string} id The id of the module to possibly load.
   * @param {!goog.module.ModuleInfo} moduleInfo The module identifier for the
   *     given id.
   * @param {boolean} userInitiated If the load was user initiated.
   * @param {goog.async.Deferred} d A deferred object.
   * @private
   */
  goog.module.ModuleManager.prototype.registerModuleLoadCallbacks_ = function(
      id, moduleInfo, userInitiated, d) {
    moduleInfo.registerCallback(d.callback, d);
    moduleInfo.registerErrback(function(err) { d.errback(Error(err)); });
    // If it's already loading, we don't have to do anything besides handle
    // if it was user initiated
    if (this.isModuleLoading(id)) {
      if (userInitiated) {
        goog.log.info(
            this.logger_, 'User initiated module already loading: ' + id);
        this.addUserInitiatedLoadingModule_(id);
        this.dispatchActiveIdleChangeIfNeeded_();
      }
    } else {
      if (userInitiated) {
        goog.log.info(this.logger_, 'User initiated module load: ' + id);
        this.addUserInitiatedLoadingModule_(id);
      } else {
        goog.log.info(this.logger_, 'Initiating module load: ' + id);
      }
    }
  };
  
  
  /**
   * Initiates loading of a list of modules or, if a module is currently being
   * loaded, appends the modules to the queue of requested module ids.
   *
   * The caller should verify that the requested modules are not already loaded or
   * loading. {@link #loadModulesOrEnqueueIfNotLoadedOrLoading_} is a more lenient
   * alternative to this method.
   *
   * @param {Array<string>} ids The ids of the modules to load.
   * @private
   */
  goog.module.ModuleManager.prototype.loadModulesOrEnqueue_ = function(ids) {
    // With concurrent loading we always just send off the request.
    if (this.concurrentLoadingEnabled_) {
      // For now we wait for initial modules to have downloaded as this puts the
      // loader in a good state for calculating the needed deps of additional
      // loads.
      // TODO(user): Make this wait unnecessary.
      this.initialModulesLoaded_.addCallback(
          goog.bind(this.loadModules_, this, ids));
    } else {
      if (goog.array.isEmpty(this.loadingModuleIds_)) {
        this.loadModules_(ids);
      } else {
        this.requestedModuleIdsQueue_.push(ids);
        this.dispatchActiveIdleChangeIfNeeded_();
      }
    }
  };
  
  
  /**
   * Gets the amount of delay to wait before sending a request for more modules.
   * If a certain module request fails, we backoff a little bit and try again.
   * @return {number} Delay, in ms.
   * @private
   */
  goog.module.ModuleManager.prototype.getBackOff_ = function() {
    // 5 seconds after one error, 20 seconds after 2.
    return Math.pow(this.consecutiveFailures_, 2) * 5000;
  };
  
  
  /**
   * Loads a list of modules and any of their not-yet-loaded prerequisites.
   * If batch mode is enabled, the prerequisites will be loaded together with the
   * requested modules and all requested modules will be loaded at the same time.
   *
   * The caller should verify that the requested modules are not already loaded
   * and that no modules are currently loading before calling this method.
   *
   * @param {Array<string>} ids The ids of the modules to load.
   * @param {boolean=} opt_isRetry If the load is a retry of a previous load
   *     attempt.
   * @param {boolean=} opt_forceReload Whether to bypass cache while loading the
   *     module.
   * @private
   */
  goog.module.ModuleManager.prototype.loadModules_ = function(
      ids, opt_isRetry, opt_forceReload) {
    if (!opt_isRetry) {
      this.consecutiveFailures_ = 0;
    }
  
    // Not all modules may be loaded immediately if batch mode is not enabled.
    var idsToLoadImmediately = this.processModulesForLoad_(ids);
  
    goog.log.info(this.logger_, 'Loading module(s): ' + idsToLoadImmediately);
    this.loadingModuleIds_ = idsToLoadImmediately;
  
    if (this.batchModeEnabled_) {
      this.requestedLoadingModuleIds_ = ids;
    } else {
      // If batch mode is disabled, we treat each dependency load as a separate
      // load.
      this.requestedLoadingModuleIds_ = goog.array.clone(idsToLoadImmediately);
    }
  
    // Dispatch an active/idle change if needed.
    this.dispatchActiveIdleChangeIfNeeded_();
  
    if (goog.array.isEmpty(idsToLoadImmediately)) {
      // All requested modules and deps have been either loaded already or have
      // already been requested.
      return;
    }
  
    this.requestedModuleIds_.push.apply(
        this.requestedModuleIds_, idsToLoadImmediately);
  
    var loadFn = goog.bind(
        this.loader_.loadModules, this.loader_,
        goog.array.clone(idsToLoadImmediately), this.moduleInfoMap_, null,
        goog.bind(
            this.handleLoadError_, this, this.requestedLoadingModuleIds_,
            idsToLoadImmediately),
        goog.bind(this.handleLoadTimeout_, this), !!opt_forceReload);
  
    var delay = this.getBackOff_();
    if (delay) {
      window.setTimeout(loadFn, delay);
    } else {
      loadFn();
    }
  };
  
  
  /**
   * Processes a list of module ids for loading. Checks if any of the modules are
   * already loaded and then gets transitive deps. Queues any necessary modules
   * if batch mode is not enabled. Returns the list of ids that should be loaded.
   *
   * @param {Array<string>} ids The ids that need to be loaded.
   * @return {!Array<string>} The ids to load, including dependencies.
   * @throws {Error} If the module is already loaded.
   * @private
   */
  goog.module.ModuleManager.prototype.processModulesForLoad_ = function(ids) {
    for (var i = 0; i < ids.length; i++) {
      var moduleInfo = this.moduleInfoMap_[ids[i]];
      if (moduleInfo.isLoaded()) {
        throw Error('Module already loaded: ' + ids[i]);
      }
    }
  
    // Build a list of the ids of this module and any of its not-yet-loaded
    // prerequisite modules in dependency order.
    var idsWithDeps = [];
    for (var i = 0; i < ids.length; i++) {
      idsWithDeps =
          idsWithDeps.concat(this.getNotYetLoadedTransitiveDepIds_(ids[i]));
    }
    goog.array.removeDuplicates(idsWithDeps);
  
    if (!this.batchModeEnabled_ && idsWithDeps.length > 1) {
      var idToLoad = idsWithDeps.shift();
      goog.log.info(
          this.logger_, 'Must load ' + idToLoad + ' module before ' + ids);
  
      // Insert the requested module id and any other not-yet-loaded prereqs
      // that it has at the front of the queue.
      var queuedModules =
          goog.array.map(idsWithDeps, function(id) { return [id]; });
      this.requestedModuleIdsQueue_ =
          queuedModules.concat(this.requestedModuleIdsQueue_);
      return [idToLoad];
    } else {
      return idsWithDeps;
    }
  };
  
  
  /**
   * Builds a list of the ids of the not-yet-loaded modules that a particular
   * module transitively depends on, including itself.
   *
   * @param {string} id The id of a not-yet-loaded module.
   * @return {!Array<string>} An array of module ids in dependency order that's
   *     guaranteed to end with the provided module id.
   * @private
   */
  goog.module.ModuleManager.prototype.getNotYetLoadedTransitiveDepIds_ = function(
      id) {
    // NOTE(user): We want the earliest occurrance of a module, not the first
    // dependency we find. Therefore we strip duplicates at the end rather than
    // during.  See the tests for concrete examples.
    var ids = [];
    if (!goog.array.contains(this.requestedModuleIds_, id)) {
      ids.push(id);
    }
    var depIds = goog.array.clone(this.getModuleInfo(id).getDependencies());
    while (depIds.length) {
      var depId = depIds.pop();
      if (!this.getModuleInfo(depId).isLoaded() &&
          !goog.array.contains(this.requestedModuleIds_, depId)) {
        ids.unshift(depId);
        // We need to process direct dependencies first.
        Array.prototype.unshift.apply(
            depIds, this.getModuleInfo(depId).getDependencies());
      }
    }
    goog.array.removeDuplicates(ids);
    return ids;
  };
  
  
  /**
   * If we are still loading the base module, consider the load complete.
   * @private
   */
  goog.module.ModuleManager.prototype.maybeFinishBaseLoad_ = function() {
    if (this.currentlyLoadingModule_ == this.baseModuleInfo_) {
      this.currentlyLoadingModule_ = null;
      var error =
          this.baseModuleInfo_.onLoad(goog.bind(this.getModuleContext, this));
      if (error) {
        this.dispatchModuleLoadFailed_(
            goog.module.ModuleManager.FailureType.INIT_ERROR);
      }
  
      this.dispatchActiveIdleChangeIfNeeded_();
    }
  };
  
  
  /**
   * Records that a module was loaded. Also initiates loading the next module if
   * any module requests are queued. This method is called by code that is
   * generated and appended to each dynamic module's code at compilation time.
   *
   * @param {string} id A module id.
   */
  goog.module.ModuleManager.prototype.setLoaded = function(id) {
    if (this.isDisposed()) {
      goog.log.warning(
          this.logger_, 'Module loaded after module manager was disposed: ' + id);
      return;
    }
  
    goog.log.info(this.logger_, 'Module loaded: ' + id);
  
    var error =
        this.moduleInfoMap_[id].onLoad(goog.bind(this.getModuleContext, this));
    if (error) {
      this.dispatchModuleLoadFailed_(
          goog.module.ModuleManager.FailureType.INIT_ERROR);
    }
  
    // Remove the module id from the user initiated set if it existed there.
    goog.array.remove(this.userInitiatedLoadingModuleIds_, id);
  
    // Remove the module id from the loading modules if it exists there.
    goog.array.remove(this.loadingModuleIds_, id);
  
    if (goog.array.isEmpty(this.loadingModuleIds_)) {
      // No more modules are currently being loaded (e.g. arriving later in the
      // same HTTP response), so proceed to load the next module in the queue.
      this.loadNextModules_();
    }
  
    if (this.lastInitialModuleId_ && id == this.lastInitialModuleId_) {
      if (!this.initialModulesLoaded_.hasFired()) {
        this.initialModulesLoaded_.callback();
      }
    }
  
    // Dispatch an active/idle change if needed.
    this.dispatchActiveIdleChangeIfNeeded_();
  };
  
  
  /**
   * Gets whether a module is currently loading or in the queue, waiting to be
   * loaded.
   * @param {string} id A module id.
   * @return {boolean} TRUE iff the module is loading.
   */
  goog.module.ModuleManager.prototype.isModuleLoading = function(id) {
    if (goog.array.contains(this.loadingModuleIds_, id)) {
      return true;
    }
    for (var i = 0; i < this.requestedModuleIdsQueue_.length; i++) {
      if (goog.array.contains(this.requestedModuleIdsQueue_[i], id)) {
        return true;
      }
    }
    return false;
  };
  
  
  /**
   * Requests that a function be called once a particular module is loaded.
   * Client code can use this method to safely call into modules that may not yet
   * be loaded. For consistency, this method always calls the function
   * asynchronously -- even if the module is already loaded. Initiates loading of
   * the module if necessary, unless opt_noLoad is true.
   *
   * @param {string} moduleId A module id.
   * @param {Function} fn Function to execute when the module has loaded.
   * @param {Object=} opt_handler Optional handler under whose scope to execute
   *     the callback.
   * @param {boolean=} opt_noLoad TRUE iff not to initiate loading of the module.
   * @param {boolean=} opt_userInitiated TRUE iff the loading of the module was
   *     user initiated.
   * @param {boolean=} opt_preferSynchronous TRUE iff the function should be
   *     executed synchronously if the module has already been loaded.
   * @return {!goog.module.ModuleLoadCallback} A callback wrapper that exposes
   *     an abort and execute method.
   */
  goog.module.ModuleManager.prototype.execOnLoad = function(
      moduleId, fn, opt_handler, opt_noLoad, opt_userInitiated,
      opt_preferSynchronous) {
    var moduleInfo = this.moduleInfoMap_[moduleId];
    var callbackWrapper;
  
    if (moduleInfo.isLoaded()) {
      goog.log.info(this.logger_, moduleId + ' module already loaded');
      // Call async so that code paths don't change between loaded and unloaded
      // cases.
      callbackWrapper = new goog.module.ModuleLoadCallback(fn, opt_handler);
      if (opt_preferSynchronous) {
        callbackWrapper.execute(this.moduleContext_);
      } else {
        window.setTimeout(goog.bind(callbackWrapper.execute, callbackWrapper), 0);
      }
    } else if (this.isModuleLoading(moduleId)) {
      goog.log.info(this.logger_, moduleId + ' module already loading');
      callbackWrapper = moduleInfo.registerCallback(fn, opt_handler);
      if (opt_userInitiated) {
        goog.log.info(
            this.logger_, 'User initiated module already loading: ' + moduleId);
        this.addUserInitiatedLoadingModule_(moduleId);
        this.dispatchActiveIdleChangeIfNeeded_();
      }
    } else {
      goog.log.info(this.logger_, 'Registering callback for module: ' + moduleId);
      callbackWrapper = moduleInfo.registerCallback(fn, opt_handler);
      if (!opt_noLoad) {
        if (opt_userInitiated) {
          goog.log.info(this.logger_, 'User initiated module load: ' + moduleId);
          this.addUserInitiatedLoadingModule_(moduleId);
        }
        goog.log.info(this.logger_, 'Initiating module load: ' + moduleId);
        this.loadModulesOrEnqueue_([moduleId]);
      }
    }
    return callbackWrapper;
  };
  
  
  /**
   * Loads a module, returning a goog.async.Deferred for keeping track of the
   * result.
   *
   * @param {string} moduleId A module id.
   * @param {boolean=} opt_userInitiated If the load is a result of a user action.
   * @return {goog.async.Deferred} A deferred object.
   */
  goog.module.ModuleManager.prototype.load = function(
      moduleId, opt_userInitiated) {
    return this.loadModulesOrEnqueueIfNotLoadedOrLoading_(
        [moduleId], opt_userInitiated)[moduleId];
  };
  
  
  /**
   * Loads a list of modules, returning a goog.async.Deferred for keeping track of
   * the result.
   *
   * @param {Array<string>} moduleIds A list of module ids.
   * @param {boolean=} opt_userInitiated If the load is a result of a user action.
   * @return {!Object<string, !goog.async.Deferred>} A mapping from id (String)
   *     to deferred objects that will callback or errback when the load for that
   *     id is finished.
   */
  goog.module.ModuleManager.prototype.loadMultiple = function(
      moduleIds, opt_userInitiated) {
    return this.loadModulesOrEnqueueIfNotLoadedOrLoading_(
        moduleIds, opt_userInitiated);
  };
  
  
  /**
   * Ensures that the module with the given id is listed as a user-initiated
   * module that is being loaded. This method guarantees that a module will never
   * get listed more than once.
   * @param {string} id Identifier of the module.
   * @private
   */
  goog.module.ModuleManager.prototype.addUserInitiatedLoadingModule_ = function(
      id) {
    if (!goog.array.contains(this.userInitiatedLoadingModuleIds_, id)) {
      this.userInitiatedLoadingModuleIds_.push(id);
    }
  };
  
  
  /**
   * Method called just before a module code is loaded.
   * @param {string} id Identifier of the module.
   */
  goog.module.ModuleManager.prototype.beforeLoadModuleCode = function(id) {
    this.loadTracer_ =
        goog.debug.Trace.startTracer('Module Load: ' + id, 'Module Load');
    if (this.currentlyLoadingModule_) {
      goog.log.error(
          this.logger_, 'beforeLoadModuleCode called with module "' + id +
              '" while module "' + this.currentlyLoadingModule_.getId() +
              '" is loading');
    }
    this.currentlyLoadingModule_ = this.getModuleInfo(id);
  };
  
  
  /**
   * Method called just after module code is loaded
   * @param {string} id Identifier of the module.
   */
  goog.module.ModuleManager.prototype.afterLoadModuleCode = function(id) {
    if (!this.currentlyLoadingModule_ ||
        id != this.currentlyLoadingModule_.getId()) {
      goog.log.error(
          this.logger_, 'afterLoadModuleCode called with module "' + id +
              '" while loading module "' +
              (this.currentlyLoadingModule_ &&
               this.currentlyLoadingModule_.getId()) +
              '"');
    }
    this.currentlyLoadingModule_ = null;
    goog.debug.Trace.stopTracer(this.loadTracer_);
  };
  
  
  /**
   * Register an initialization callback for the currently loading module. This
   * should only be called by script that is executed during the evaluation of
   * a module's javascript. This is almost equivalent to calling the function
   * inline, but ensures that all the code from the currently loading module
   * has been loaded. This makes it cleaner and more robust than calling the
   * function inline.
   *
   * If this function is called from the base module (the one that contains
   * the module manager code), the callback is held until #setAllModuleInfo
   * is called, or until #setModuleContext is called, whichever happens first.
   *
   * @param {Function} fn A callback function that takes a single argument
   *    which is the module context.
   * @param {Object=} opt_handler Optional handler under whose scope to execute
   *     the callback.
   */
  goog.module.ModuleManager.prototype.registerInitializationCallback = function(
      fn, opt_handler) {
    if (!this.currentlyLoadingModule_) {
      goog.log.error(this.logger_, 'No module is currently loading');
    } else {
      this.currentlyLoadingModule_.registerEarlyCallback(fn, opt_handler);
    }
  };
  
  
  /**
   * Register a late initialization callback for the currently loading module.
   * Callbacks registered via this function are executed similar to
   * {@see registerInitializationCallback}, but they are fired after all
   * initialization callbacks are called.
   *
   * @param {Function} fn A callback function that takes a single argument
   *    which is the module context.
   * @param {Object=} opt_handler Optional handler under whose scope to execute
   *     the callback.
   */
  goog.module.ModuleManager.prototype.registerLateInitializationCallback =
      function(fn, opt_handler) {
    if (!this.currentlyLoadingModule_) {
      goog.log.error(this.logger_, 'No module is currently loading');
    } else {
      this.currentlyLoadingModule_.registerCallback(fn, opt_handler);
    }
  };
  
  
  /**
   * Sets the constructor to use for the module object for the currently
   * loading module. The constructor should derive from
   * {@see goog.module.BaseModule}.
   * @param {Function} fn The constructor function.
   */
  goog.module.ModuleManager.prototype.setModuleConstructor = function(fn) {
    if (!this.currentlyLoadingModule_) {
      goog.log.error(this.logger_, 'No module is currently loading');
      return;
    }
    this.currentlyLoadingModule_.setModuleConstructor(fn);
  };
  
  
  /**
   * The possible reasons for a module load failure callback being fired.
   * @enum {number}
   */
  goog.module.ModuleManager.FailureType = {
    /** 401 Status. */
    UNAUTHORIZED: 0,
  
    /** Error status (not 401) returned multiple times. */
    CONSECUTIVE_FAILURES: 1,
  
    /** Request timeout. */
    TIMEOUT: 2,
  
    /** 410 status, old code gone. */
    OLD_CODE_GONE: 3,
  
    /** The onLoad callbacks failed. */
    INIT_ERROR: 4
  };
  
  
  /**
   * Handles a module load failure.
   *
   * @param {!Array<string>} requestedLoadingModuleIds Modules ids that were
   *     requested in failed request. Does not included calculated dependencies.
   * @param {!Array<string>} requestedModuleIdsWithDeps All module ids requested
   *     in the failed request including all dependencies.
   * @param {?number} status The error status.
   * @private
   */
  goog.module.ModuleManager.prototype.handleLoadError_ = function(
      requestedLoadingModuleIds, requestedModuleIdsWithDeps, status) {
    this.consecutiveFailures_++;
    // Module manager was not designed to be reentrant. Reinstate the instance
    // var with actual value when request failed (Other requests may have
    // started already.)
    this.requestedLoadingModuleIds_ = requestedLoadingModuleIds;
    // Pretend we never requested the failed modules.
    goog.array.forEach(
        requestedModuleIdsWithDeps,
        goog.partial(goog.array.remove, this.requestedModuleIds_), this);
  
    if (status == 401) {
      // The user is not logged in. They've cleared their cookies or logged out
      // from another window.
      goog.log.info(this.logger_, 'Module loading unauthorized');
      this.dispatchModuleLoadFailed_(
          goog.module.ModuleManager.FailureType.UNAUTHORIZED);
      // Drop any additional module requests.
      this.requestedModuleIdsQueue_.length = 0;
    } else if (status == 410) {
      // The requested module js is old and not available.
      this.requeueBatchOrDispatchFailure_(
          goog.module.ModuleManager.FailureType.OLD_CODE_GONE);
      this.loadNextModules_();
    } else if (this.consecutiveFailures_ >= 3) {
      goog.log.info(
          this.logger_,
          'Aborting after failure to load: ' + this.loadingModuleIds_);
      this.requeueBatchOrDispatchFailure_(
          goog.module.ModuleManager.FailureType.CONSECUTIVE_FAILURES);
      this.loadNextModules_();
    } else {
      goog.log.info(
          this.logger_,
          'Retrying after failure to load: ' + this.loadingModuleIds_);
      var forceReload =
          status == goog.module.ModuleManager.CORRUPT_RESPONSE_STATUS_CODE;
      this.loadModules_(this.requestedLoadingModuleIds_, true, forceReload);
    }
  };
  
  
  /**
   * Handles a module load timeout.
   * @private
   */
  goog.module.ModuleManager.prototype.handleLoadTimeout_ = function() {
    goog.log.info(
        this.logger_, 'Aborting after timeout: ' + this.loadingModuleIds_);
    this.requeueBatchOrDispatchFailure_(
        goog.module.ModuleManager.FailureType.TIMEOUT);
    this.loadNextModules_();
  };
  
  
  /**
   * Requeues batch loads that had more than one requested module
   * (i.e. modules that were not included as dependencies) as separate loads or
   * if there was only one requested module, fails that module with the received
   * cause.
   * @param {goog.module.ModuleManager.FailureType} cause The reason for the
   *     failure.
   * @private
   */
  goog.module.ModuleManager.prototype.requeueBatchOrDispatchFailure_ = function(
      cause) {
    // The load failed, so if there are more than one requested modules, then we
    // need to retry each one as a separate load. Otherwise, if there is only one
    // requested module, remove it and its dependencies from the queue.
    if (this.requestedLoadingModuleIds_.length > 1) {
      var queuedModules = goog.array.map(
          this.requestedLoadingModuleIds_, function(id) { return [id]; });
      this.requestedModuleIdsQueue_ =
          queuedModules.concat(this.requestedModuleIdsQueue_);
    } else {
      this.dispatchModuleLoadFailed_(cause);
    }
  };
  
  
  /**
   * Handles when a module load failed.
   * @param {goog.module.ModuleManager.FailureType} cause The reason for the
   *     failure.
   * @private
   */
  goog.module.ModuleManager.prototype.dispatchModuleLoadFailed_ = function(
      cause) {
    var failedIds = this.requestedLoadingModuleIds_;
    this.loadingModuleIds_.length = 0;
    // If any pending modules depend on the id that failed,
    // they need to be removed from the queue.
    var idsToCancel = [];
    for (var i = 0; i < this.requestedModuleIdsQueue_.length; i++) {
      var dependentModules = goog.array.filter(
          this.requestedModuleIdsQueue_[i],
          /**
           * Returns true if the requestedId has dependencies on the modules that
           * just failed to load.
           * @param {string} requestedId The module to check for dependencies.
           * @return {boolean} True if the module depends on failed modules.
           */
          function(requestedId) {
            var requestedDeps =
                this.getNotYetLoadedTransitiveDepIds_(requestedId);
            return goog.array.some(failedIds, function(id) {
              return goog.array.contains(requestedDeps, id);
            });
          },
          this);
      goog.array.extend(idsToCancel, dependentModules);
    }
  
    // Also insert the ids that failed to load as ids to cancel.
    for (var i = 0; i < failedIds.length; i++) {
      goog.array.insert(idsToCancel, failedIds[i]);
    }
  
    // Remove ids to cancel from the queues.
    for (var i = 0; i < idsToCancel.length; i++) {
      for (var j = 0; j < this.requestedModuleIdsQueue_.length; j++) {
        goog.array.remove(this.requestedModuleIdsQueue_[j], idsToCancel[i]);
      }
      goog.array.remove(this.userInitiatedLoadingModuleIds_, idsToCancel[i]);
    }
  
    // Call the functions for error notification.
    var errorCallbacks =
        this.callbackMap_[goog.module.ModuleManager.CallbackType.ERROR];
    if (errorCallbacks) {
      for (var i = 0; i < errorCallbacks.length; i++) {
        var callback = errorCallbacks[i];
        for (var j = 0; j < idsToCancel.length; j++) {
          callback(
              goog.module.ModuleManager.CallbackType.ERROR, idsToCancel[j],
              cause);
        }
      }
    }
  
    // Call the errbacks on the module info.
    for (var i = 0; i < failedIds.length; i++) {
      if (this.moduleInfoMap_[failedIds[i]]) {
        this.moduleInfoMap_[failedIds[i]].onError(cause);
      }
    }
  
    // Clear the requested loading module ids.
    this.requestedLoadingModuleIds_.length = 0;
  
    this.dispatchActiveIdleChangeIfNeeded_();
  };
  
  
  /**
   * Loads the next modules on the queue.
   * @private
   */
  goog.module.ModuleManager.prototype.loadNextModules_ = function() {
    while (this.requestedModuleIdsQueue_.length) {
      // Remove modules that are already loaded.
      var nextIds = goog.array.filter(
          this.requestedModuleIdsQueue_.shift(),
          /** @param {string} id The module id. */
          function(id) { return !this.getModuleInfo(id).isLoaded(); }, this);
      if (nextIds.length > 0) {
        this.loadModules_(nextIds);
        return;
      }
    }
  
    // Dispatch an active/idle change if needed.
    this.dispatchActiveIdleChangeIfNeeded_();
  };
  
  
  /**
   * The function to call if the module manager is in error.
   * @param
   * {goog.module.ModuleManager.CallbackType|Array<goog.module.ModuleManager.CallbackType>}
   * types
   *  The callback type.
   * @param {Function} fn The function to register as a callback.
   */
  goog.module.ModuleManager.prototype.registerCallback = function(types, fn) {
    if (!goog.isArray(types)) {
      types = [types];
    }
  
    for (var i = 0; i < types.length; i++) {
      this.registerCallback_(types[i], fn);
    }
  };
  
  
  /**
   * Register a callback for the specified callback type.
   * @param {goog.module.ModuleManager.CallbackType} type The callback type.
   * @param {Function} fn The callback function.
   * @private
   */
  goog.module.ModuleManager.prototype.registerCallback_ = function(type, fn) {
    var callbackMap = this.callbackMap_;
    if (!callbackMap[type]) {
      callbackMap[type] = [];
    }
    callbackMap[type].push(fn);
  };
  
  
  /**
   * Call the callback functions of the specified type.
   * @param {goog.module.ModuleManager.CallbackType} type The callback type.
   * @private
   */
  goog.module.ModuleManager.prototype.executeCallbacks_ = function(type) {
    var callbacks = this.callbackMap_[type];
    for (var i = 0; callbacks && i < callbacks.length; i++) {
      callbacks[i](type);
    }
  };
  
  
  /** @override */
  goog.module.ModuleManager.prototype.disposeInternal = function() {
    goog.module.ModuleManager.base(this, 'disposeInternal');
  
    // Dispose of each ModuleInfo object.
    goog.disposeAll(
        goog.object.getValues(this.moduleInfoMap_), this.baseModuleInfo_);
    this.moduleInfoMap_ = null;
    this.loadingModuleIds_ = null;
    this.requestedLoadingModuleIds_ = null;
    this.userInitiatedLoadingModuleIds_ = null;
    this.requestedModuleIdsQueue_ = null;
    this.callbackMap_ = null;
  };
  