loader.js

// Copyright 2006 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 This class supports the dynamic loading of compiled
 * javascript modules at runtime, as descibed in the designdoc.
 *
 *   <http://go/js_modules_design>
 *
 */

goog.provide('goog.module.Loader');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.object');



/**
 * The dynamic loading functionality is defined as a class. The class
 * will be used as singleton. There is, however, a two step
 * initialization procedure because parameters need to be passed to
 * the goog.module.Loader instance.
 *
 * @constructor
 * @final
 */
goog.module.Loader = function() {
  /**
   * Map of module name/array of {symbol name, callback} pairs that are pending
   * to be loaded.
   * @type {Object}
   * @private
   */
  this.pending_ = {};

  /**
   * Provides associative access to each module and the symbols of each module
   * that have aready been loaded (one lookup for the module, another lookup
   * on the module for the symbol).
   * @type {Object}
   * @private
   */
  this.modules_ = {};

  /**
   * Map of module name to module url. Used to avoid fetching the same URL
   * twice by keeping track of in-flight URLs.
   * Note: this allows two modules to be bundled into the same file.
   * @type {Object}
   * @private
   */
  this.pendingModuleUrls_ = {};

  /**
   * The base url to load modules from. This property will be set in init().
   * @type {?string}
   * @private
   */
  this.urlBase_ = null;

  /**
   * Array of modules that have been requested before init() was called.
   * If require() is called before init() was called, the required
   * modules can obviously not yet be loaded, because their URL is
   * unknown. The modules that are requested before init() are
   * therefore stored in this array, and they are loaded at init()
   * time.
   * @type {Array.<string>}
   * @private
   */
  this.pendingBeforeInit_ = [];
};
goog.addSingletonGetter(goog.module.Loader);


/**
 * Creates a full URL to the compiled module code given a base URL and a
 * module name. By default it's urlBase + '_' + module + '.js'.
 * @param {string} urlBase URL to the module files.
 * @param {string} module Module name.
 * @return {string} The full url to the module binary.
 * @private
 */
goog.module.Loader.prototype.getModuleUrl_ = function(urlBase, module) {
  return urlBase + '_' + module + '.js';
};


/**
 * The globally exported name of the load callback. Matches the
 * definition in the js_modular_binary() BUILD rule.
 * @type {string}
 */
goog.module.Loader.LOAD_CALLBACK = '__gjsload__';


/**
 * Loads the module by evaluating the javascript text in the current
 * scope. Uncompiled, base identifiers are visible in the global scope;
 * when compiled they are visible in the closure of the anonymous
 * namespace. Notice that this cannot be replaced by the global eval,
 * because the global eval isn't in the scope of the anonymous
 * namespace function that the jscompiled code lives in.
 *
 * @param {string} t_ The javascript text to evaluate. IMPORTANT: The
 *   name of the identifier is chosen so that it isn't compiled and
 *   hence cannot shadow compiled identifiers in the surrounding scope.
 * @private
 */
goog.module.Loader.loaderEval_ = function(t_) {
  eval(t_);
};


/**
 * Initializes the Loader to be fully functional. Also executes load
 * requests that were received before initialization. Must be called
 * exactly once, with the URL of the base library. Module URLs are
 * derived from the URL of the base library by inserting the module
 * name, preceded by a period, before the .js prefix of the base URL.
 *
 * @param {string} baseUrl The URL of the base library.
 * @param {Function=} opt_urlFunction Function that creates the URL for the
 *     module file. It will be passed the base URL for module files and the
 *     module name and should return the fully-formed URL to the module file to
 *     load.
 */
goog.module.Loader.prototype.init = function(baseUrl, opt_urlFunction) {
  // For the use by the module wrappers, loaderEval_ is exported to
  // the page. Note that, despite the name, this is not part of the
  // API, so it is here and not in api_app.js. Cf. BUILD. Note this is
  // done before the first load requests are sent.
  goog.exportSymbol(goog.module.Loader.LOAD_CALLBACK,
      goog.module.Loader.loaderEval_);

  this.urlBase_ = baseUrl.replace(/\.js$/, '');
  if (opt_urlFunction) {
    this.getModuleUrl_ = opt_urlFunction;
  }

  goog.array.forEach(this.pendingBeforeInit_, function(module) {
    this.load_(module);
  }, this);
  goog.array.clear(this.pendingBeforeInit_);
};


/**
 * Requests the loading of a symbol from a module. When the module is
 * loaded, the requested symbol will be passed as argument to the
 * function callback.
 *
 * @param {string} module The name of the module. Usually, the value
 *     is defined as a constant whose name starts with MOD_.
 * @param {number|string} symbol The ID of the symbol. Usually, the value is
 *     defined as a constant whose name starts with SYM_.
 * @param {Function} callback This function will be called with the
 *     resolved symbol as the argument once the module is loaded.
 */
goog.module.Loader.prototype.require = function(module, symbol, callback) {
  var pending = this.pending_;
  var modules = this.modules_;
  if (modules[module]) {
    // already loaded
    callback(modules[module][symbol]);
  } else if (pending[module]) {
    // loading is pending from another require of the same module
    pending[module].push([symbol, callback]);
  } else {
    // not loaded, and not requested
    pending[module] = [[symbol, callback]];  // Yes, really [[ ]].
    // Defer loading to initialization if Loader is not yet
    // initialized, otherwise load the module.
    if (goog.isString(this.urlBase_)) {
      this.load_(module);
    } else {
      this.pendingBeforeInit_.push(module);
    }
  }
};


/**
 * Registers a symbol in a loaded module. When called without symbol,
 * registers the module to be fully loaded and executes all callbacks
 * from pending require() callbacks for this module.
 *
 * @param {string} module The name of the module. Cf. parameter module
 *     of method require().
 * @param {number|string=} opt_symbol The symbol being defined, or nothing when
 *     all symbols of the module are defined. Cf. parameter symbol of method
 *     require().
 * @param {Object=} opt_object The object bound to the symbol, or nothing when
 *     all symbols of the module are defined.
 */
goog.module.Loader.prototype.provide = function(
    module, opt_symbol, opt_object) {
  var modules = this.modules_;
  var pending = this.pending_;
  if (!modules[module]) {
    modules[module] = {};
  }
  if (opt_object) {
    // When an object is provided, just register it.
    modules[module][opt_symbol] = opt_object;
  } else if (pending[module]) {
    // When no object is provided, and there are pending require()
    // callbacks for this module, execute them.
    for (var i = 0; i < pending[module].length; ++i) {
      var symbol = pending[module][i][0];
      var callback = pending[module][i][1];
      callback(modules[module][symbol]);
    }
    delete pending[module];
    delete this.pendingModuleUrls_[module];
  }
};


/**
 * Starts to load a module. Assumes that init() was called.
 *
 * @param {string} module The name of the module.
 * @private
 */
goog.module.Loader.prototype.load_ = function(module) {
  // NOTE(user): If the module request happens inside a click handler
  // (presumably inside any user event handler, but the onload event
  // handler is fine), IE will load the script but not execute
  // it. Thus we break out of the current flow of control before we do
  // the load. For the record, for IE it would have been enough to
  // just defer the assignment to src. Safari doesn't execute the
  // script if the assignment to src happens *after* the script
  // element is inserted into the DOM.
  goog.Timer.callOnce(function() {
    // The module might have been registered in the interim (if fetched as part
    // of another module fetch because they share the same url)
    if (this.modules_[module]) {
      return;
    }

    goog.asserts.assertString(this.urlBase_);
    var url = this.getModuleUrl_(this.urlBase_, module);

    // Check if specified URL is already in flight
    var urlInFlight = goog.object.containsValue(this.pendingModuleUrls_, url);
    this.pendingModuleUrls_[module] = url;
    if (urlInFlight) {
      return;
    }

    var s = goog.dom.createDom('script',
        {'type': 'text/javascript', 'src': url});
    document.body.appendChild(s);
  }, 0, this);
};