| // 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 The module loader for loading modules across the network. |
| * |
| * Browsers do not guarantee that scripts appended to the document |
| * are executed in the order they are added. For production mode, we use |
| * XHRs to load scripts, because they do not have this problem and they |
| * have superior mechanisms for handling failure. However, XHR-evaled |
| * scripts are harder to debug. |
| * |
| * In debugging mode, we use normal script tags. In order to make this work, |
| * we load the scripts in serial: we do not execute script B to the document |
| * until we are certain that script A is finished loading. |
| * |
| */ |
| |
| goog.provide('goog.module.ModuleLoader'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.array'); |
| goog.require('goog.events'); |
| goog.require('goog.events.Event'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.log'); |
| goog.require('goog.module.AbstractModuleLoader'); |
| goog.require('goog.net.BulkLoader'); |
| goog.require('goog.net.EventType'); |
| goog.require('goog.net.jsloader'); |
| goog.require('goog.userAgent'); |
| goog.require('goog.userAgent.product'); |
| |
| |
| |
| /** |
| * A class that loads Javascript modules. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| * @implements {goog.module.AbstractModuleLoader} |
| */ |
| goog.module.ModuleLoader = function() { |
| goog.module.ModuleLoader.base(this, 'constructor'); |
| |
| /** |
| * Event handler for managing handling events. |
| * @type {goog.events.EventHandler<!goog.module.ModuleLoader>} |
| * @private |
| */ |
| this.eventHandler_ = new goog.events.EventHandler(this); |
| |
| /** |
| * A map from module IDs to goog.module.ModuleLoader.LoadStatus. |
| * @type {!Object<Array<string>, goog.module.ModuleLoader.LoadStatus>} |
| * @private |
| */ |
| this.loadingModulesStatus_ = {}; |
| }; |
| goog.inherits(goog.module.ModuleLoader, goog.events.EventTarget); |
| |
| |
| /** |
| * A logger. |
| * @type {goog.log.Logger} |
| * @protected |
| */ |
| goog.module.ModuleLoader.prototype.logger = goog.log.getLogger( |
| 'goog.module.ModuleLoader'); |
| |
| |
| /** |
| * Whether debug mode is enabled. |
| * @type {boolean} |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.debugMode_ = false; |
| |
| |
| /** |
| * Whether source url injection is enabled. |
| * @type {boolean} |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.sourceUrlInjection_ = false; |
| |
| |
| /** |
| * @return {boolean} Whether sourceURL affects stack traces. |
| * Chrome is currently the only browser that does this, but |
| * we believe other browsers are working on this. |
| * @see http://bugzilla.mozilla.org/show_bug.cgi?id=583083 |
| */ |
| goog.module.ModuleLoader.supportsSourceUrlStackTraces = function() { |
| return goog.userAgent.product.CHROME; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether sourceURL affects the debugger. |
| */ |
| goog.module.ModuleLoader.supportsSourceUrlDebugger = function() { |
| return goog.userAgent.product.CHROME || goog.userAgent.GECKO; |
| }; |
| |
| |
| /** |
| * Gets the debug mode for the loader. |
| * @return {boolean} Whether the debug mode is enabled. |
| */ |
| goog.module.ModuleLoader.prototype.getDebugMode = function() { |
| return this.debugMode_; |
| }; |
| |
| |
| /** |
| * Sets the debug mode for the loader. |
| * @param {boolean} debugMode Whether the debug mode is enabled. |
| */ |
| goog.module.ModuleLoader.prototype.setDebugMode = function(debugMode) { |
| this.debugMode_ = debugMode; |
| }; |
| |
| |
| /** |
| * When enabled, we will add a sourceURL comment to the end of all scripts |
| * to mark their origin. |
| * |
| * On WebKit, stack traces will refect the sourceURL comment, so this is |
| * useful for debugging webkit stack traces in production. |
| * |
| * Notice that in debug mode, we will use source url injection + eval rather |
| * then appending script nodes to the DOM, because the scripts will load far |
| * faster. (Appending script nodes is very slow, because we can't parallelize |
| * the downloading and evaling of the script). |
| * |
| * The cost of appending sourceURL information is negligible when compared to |
| * the cost of evaling the script. Almost all clients will want this on. |
| * |
| * TODO(nicksantos): Turn this on by default. We may want to turn this off |
| * for clients that inject their own sourceURL. |
| * |
| * @param {boolean} enabled Whether source url injection is enabled. |
| */ |
| goog.module.ModuleLoader.prototype.setSourceUrlInjection = function(enabled) { |
| this.sourceUrlInjection_ = enabled; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether we're using source url injection. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.usingSourceUrlInjection_ = function() { |
| return this.sourceUrlInjection_ || |
| (this.getDebugMode() && |
| goog.module.ModuleLoader.supportsSourceUrlStackTraces()); |
| }; |
| |
| |
| /** @override */ |
| goog.module.ModuleLoader.prototype.loadModules = function( |
| ids, moduleInfoMap, opt_successFn, opt_errorFn, opt_timeoutFn, |
| opt_forceReload) { |
| var loadStatus = this.loadingModulesStatus_[ids] || |
| new goog.module.ModuleLoader.LoadStatus(); |
| loadStatus.loadRequested = true; |
| loadStatus.successFn = opt_successFn || null; |
| loadStatus.errorFn = opt_errorFn || null; |
| |
| if (!this.loadingModulesStatus_[ids]) { |
| // Modules were not prefetched. |
| this.loadingModulesStatus_[ids] = loadStatus; |
| this.downloadModules_(ids, moduleInfoMap); |
| // TODO(user): Need to handle timeouts in the module loading code. |
| } else if (goog.isDefAndNotNull(loadStatus.responseTexts)) { |
| // Modules prefetch is complete. |
| this.evaluateCode_(ids); |
| } |
| // Otherwise modules prefetch is in progress, and these modules will be |
| // executed after the prefetch is complete. |
| }; |
| |
| |
| /** |
| * Evaluate the JS code. |
| * @param {Array<string>} moduleIds The module ids. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.evaluateCode_ = function(moduleIds) { |
| this.dispatchEvent(new goog.module.ModuleLoader.Event( |
| goog.module.ModuleLoader.EventType.REQUEST_SUCCESS, moduleIds)); |
| |
| goog.log.info(this.logger, 'evaluateCode ids:' + moduleIds); |
| var success = true; |
| var loadStatus = this.loadingModulesStatus_[moduleIds]; |
| var uris = loadStatus.requestUris; |
| var texts = loadStatus.responseTexts; |
| try { |
| if (this.usingSourceUrlInjection_()) { |
| for (var i = 0; i < uris.length; i++) { |
| var uri = uris[i]; |
| goog.globalEval(texts[i] + ' //@ sourceURL=' + uri); |
| } |
| } else { |
| goog.globalEval(texts.join('\n')); |
| } |
| } catch (e) { |
| success = false; |
| // TODO(user): Consider throwing an exception here. |
| goog.log.warning(this.logger, 'Loaded incomplete code for module(s): ' + |
| moduleIds, e); |
| } |
| |
| this.dispatchEvent( |
| new goog.module.ModuleLoader.Event( |
| goog.module.ModuleLoader.EventType.EVALUATE_CODE, moduleIds)); |
| |
| if (!success) { |
| this.handleErrorHelper_(moduleIds, loadStatus.errorFn, null /* status */); |
| } else if (loadStatus.successFn) { |
| loadStatus.successFn(); |
| } |
| delete this.loadingModulesStatus_[moduleIds]; |
| }; |
| |
| |
| /** |
| * Handles a successful response to a request for prefetch or load one or more |
| * modules. |
| * |
| * @param {goog.net.BulkLoader} bulkLoader The bulk loader. |
| * @param {Array<string>} moduleIds The ids of the modules requested. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.handleSuccess_ = function( |
| bulkLoader, moduleIds) { |
| goog.log.info(this.logger, 'Code loaded for module(s): ' + moduleIds); |
| |
| var loadStatus = this.loadingModulesStatus_[moduleIds]; |
| loadStatus.responseTexts = bulkLoader.getResponseTexts(); |
| |
| if (loadStatus.loadRequested) { |
| this.evaluateCode_(moduleIds); |
| } |
| |
| // NOTE: A bulk loader instance is used for loading a set of module ids. |
| // Once these modules have been loaded successfully or in error the bulk |
| // loader should be disposed as it is not needed anymore. A new bulk loader |
| // is instantiated for any new modules to be loaded. The dispose is called |
| // on a timer so that the bulkloader has a chance to release its |
| // objects. |
| goog.Timer.callOnce(bulkLoader.dispose, 5, bulkLoader); |
| }; |
| |
| |
| /** @override */ |
| goog.module.ModuleLoader.prototype.prefetchModule = function( |
| id, moduleInfo) { |
| // Do not prefetch in debug mode. |
| if (this.getDebugMode()) { |
| return; |
| } |
| var loadStatus = this.loadingModulesStatus_[[id]]; |
| if (loadStatus) { |
| return; |
| } |
| |
| var moduleInfoMap = {}; |
| moduleInfoMap[id] = moduleInfo; |
| this.loadingModulesStatus_[[id]] = new goog.module.ModuleLoader.LoadStatus(); |
| this.downloadModules_([id], moduleInfoMap); |
| }; |
| |
| |
| /** |
| * Downloads a list of JavaScript modules. |
| * |
| * @param {Array<string>} ids The module ids in dependency order. |
| * @param {Object} moduleInfoMap A mapping from module id to ModuleInfo object. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.downloadModules_ = function( |
| ids, moduleInfoMap) { |
| var uris = []; |
| for (var i = 0; i < ids.length; i++) { |
| goog.array.extend(uris, moduleInfoMap[ids[i]].getUris()); |
| } |
| goog.log.info(this.logger, 'downloadModules ids:' + ids + ' uris:' + uris); |
| |
| if (this.getDebugMode() && |
| !this.usingSourceUrlInjection_()) { |
| // In debug mode use <script> tags rather than XHRs to load the files. |
| // This makes it possible to debug and inspect stack traces more easily. |
| // It's also possible to use it to load JavaScript files that are hosted on |
| // another domain. |
| // The scripts need to load serially, so this is much slower than parallel |
| // script loads with source url injection. |
| goog.net.jsloader.loadMany(uris); |
| } else { |
| var loadStatus = this.loadingModulesStatus_[ids]; |
| loadStatus.requestUris = uris; |
| |
| var bulkLoader = new goog.net.BulkLoader(uris); |
| |
| var eventHandler = this.eventHandler_; |
| eventHandler.listen( |
| bulkLoader, |
| goog.net.EventType.SUCCESS, |
| goog.bind(this.handleSuccess_, this, bulkLoader, ids)); |
| eventHandler.listen( |
| bulkLoader, |
| goog.net.EventType.ERROR, |
| goog.bind(this.handleError_, this, bulkLoader, ids)); |
| bulkLoader.load(); |
| } |
| }; |
| |
| |
| /** |
| * Handles an error during a request for one or more modules. |
| * @param {goog.net.BulkLoader} bulkLoader The bulk loader. |
| * @param {Array<string>} moduleIds The ids of the modules requested. |
| * @param {number} status The response status. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.handleError_ = function( |
| bulkLoader, moduleIds, status) { |
| var loadStatus = this.loadingModulesStatus_[moduleIds]; |
| // The bulk loader doesn't cancel other requests when a request fails. We will |
| // delete the loadStatus in the first failure, so it will be undefined in |
| // subsequent errors. |
| if (loadStatus) { |
| delete this.loadingModulesStatus_[moduleIds]; |
| this.handleErrorHelper_(moduleIds, loadStatus.errorFn, status); |
| } |
| |
| // NOTE: A bulk loader instance is used for loading a set of module ids. Once |
| // these modules have been loaded successfully or in error the bulk loader |
| // should be disposed as it is not needed anymore. A new bulk loader is |
| // instantiated for any new modules to be loaded. The dispose is called |
| // on another thread so that the bulkloader has a chance to release its |
| // objects. |
| goog.Timer.callOnce(bulkLoader.dispose, 5, bulkLoader); |
| }; |
| |
| |
| /** |
| * Handles an error during a request for one or more modules. |
| * @param {Array<string>} moduleIds The ids of the modules requested. |
| * @param {?function(?number)} errorFn The function to call on failure. |
| * @param {?number} status The response status. |
| * @private |
| */ |
| goog.module.ModuleLoader.prototype.handleErrorHelper_ = function( |
| moduleIds, errorFn, status) { |
| this.dispatchEvent( |
| new goog.module.ModuleLoader.Event( |
| goog.module.ModuleLoader.EventType.REQUEST_ERROR, moduleIds)); |
| |
| goog.log.warning(this.logger, 'Request failed for module(s): ' + moduleIds); |
| |
| if (errorFn) { |
| errorFn(status); |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.module.ModuleLoader.prototype.disposeInternal = function() { |
| goog.module.ModuleLoader.superClass_.disposeInternal.call(this); |
| |
| this.eventHandler_.dispose(); |
| this.eventHandler_ = null; |
| }; |
| |
| |
| /** |
| * @enum {string} |
| */ |
| goog.module.ModuleLoader.EventType = { |
| /** Called after the code for a module is evaluated. */ |
| EVALUATE_CODE: goog.events.getUniqueId('evaluateCode'), |
| |
| /** Called when the BulkLoader finishes successfully. */ |
| REQUEST_SUCCESS: goog.events.getUniqueId('requestSuccess'), |
| |
| /** Called when the BulkLoader fails, or code loading fails. */ |
| REQUEST_ERROR: goog.events.getUniqueId('requestError') |
| }; |
| |
| |
| |
| /** |
| * @param {goog.module.ModuleLoader.EventType} type The type. |
| * @param {Array<string>} moduleIds The ids of the modules being evaluated. |
| * @constructor |
| * @extends {goog.events.Event} |
| * @final |
| */ |
| goog.module.ModuleLoader.Event = function(type, moduleIds) { |
| goog.module.ModuleLoader.Event.base(this, 'constructor', type); |
| |
| /** |
| * @type {Array<string>} |
| */ |
| this.moduleIds = moduleIds; |
| }; |
| goog.inherits(goog.module.ModuleLoader.Event, goog.events.Event); |
| |
| |
| |
| /** |
| * A class that keeps the state of the module during the loading process. It is |
| * used to save loading information between modules download and evaluation. |
| * @constructor |
| * @final |
| */ |
| goog.module.ModuleLoader.LoadStatus = function() { |
| /** |
| * The request uris. |
| * @type {Array<string>} |
| */ |
| this.requestUris = null; |
| |
| /** |
| * The response texts. |
| * @type {Array<string>} |
| */ |
| this.responseTexts = null; |
| |
| /** |
| * Whether loadModules was called for the set of modules referred by this |
| * status. |
| * @type {boolean} |
| */ |
| this.loadRequested = false; |
| |
| /** |
| * Success callback. |
| * @type {?function()} |
| */ |
| this.successFn = null; |
| |
| /** |
| * Error callback. |
| * @type {?function(?number)} |
| */ |
| this.errorFn = null; |
| }; |