return xhr.status < 400 ? xhr.responseText : "";
}
+/**
+ * @param {string} url
+ * @return {!Promise.<string>}
+ */
+function loadResourcePromise(url)
+{
+ return new Promise(load);
+
+ /**
+ * @param {function(?)} fulfill
+ * @param {function(*)} reject
+ */
+ function load(fulfill, reject)
+ {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.onreadystatechange = onreadystatechange;
+
+ /**
+ * @param {Event} e
+ */
+ function onreadystatechange(e)
+ {
+ if (xhr.readyState !== 4)
+ return;
+
+ if ([0, 200, 304].indexOf(xhr.status) === -1) // Testing harness file:/// results in 0.
+ reject(new Error("While loading from url " + url + " server responded with a status of " + xhr.status));
+ else
+ fulfill(e.target.response);
+ }
+ xhr.send(null);
+ }
+}
/**
* http://tools.ietf.org/html/rfc3986#section-5.2.4
}
/**
- * @param {string} scriptName
+ * @param {!Array.<string>} scriptNames
+ * @return {!Promise.<undefined>}
*/
-function loadScript(scriptName)
+function loadScriptsPromise(scriptNames)
{
- var sourceURL = self._importScriptPathPrefix + scriptName;
- var schemaIndex = sourceURL.indexOf("://") + 3;
- sourceURL = sourceURL.substring(0, schemaIndex) + normalizePath(sourceURL.substring(schemaIndex));
- if (_loadedScripts[sourceURL])
- return;
- _loadedScripts[sourceURL] = true;
- var scriptSource = loadResource(sourceURL);
- if (!scriptSource) {
- console.error("Empty response arrived for script '" + sourceURL + "'");
- return;
+ /** @type {!Array.<!Promise.<string>>} */
+ var promises = [];
+ /** @type {!Array.<string>} */
+ var urls = [];
+ var sources = new Array(scriptNames.length);
+ var scriptToEval = 0;
+ for (var i = 0; i < scriptNames.length; ++i) {
+ var scriptName = scriptNames[i];
+ var sourceURL = self._importScriptPathPrefix + scriptName;
+ var schemaIndex = sourceURL.indexOf("://") + 3;
+ sourceURL = sourceURL.substring(0, schemaIndex) + normalizePath(sourceURL.substring(schemaIndex));
+ if (_loadedScripts[sourceURL])
+ continue;
+ urls.push(sourceURL);
+ promises.push(loadResourcePromise(sourceURL).then(scriptSourceLoaded.bind(null, i), scriptSourceLoaded.bind(null, i, undefined)));
}
- var oldPrefix = self._importScriptPathPrefix;
- self._importScriptPathPrefix += scriptName.substring(0, scriptName.lastIndexOf("/") + 1);
- try {
+ return Promise.all(promises).then(undefined);
+
+ /**
+ * @param {number} scriptNumber
+ * @param {string=} scriptSource
+ */
+ function scriptSourceLoaded(scriptNumber, scriptSource)
+ {
+ sources[scriptNumber] = scriptSource || "";
+ // Eval scripts as fast as possible.
+ while (typeof sources[scriptToEval] !== "undefined") {
+ evaluateScript(urls[scriptToEval], sources[scriptToEval]);
+ ++scriptToEval;
+ }
+ }
+
+ /**
+ * @param {string} sourceURL
+ * @param {string=} scriptSource
+ */
+ function evaluateScript(sourceURL, scriptSource)
+ {
+ _loadedScripts[sourceURL] = true;
+ if (!scriptSource) {
+ // Do not reject, as this is normal in the hosted mode.
+ console.error("Empty response arrived for script '" + sourceURL + "'");
+ return;
+ }
self.eval(scriptSource + "\n//# sourceURL=" + sourceURL);
- } finally {
- self._importScriptPathPrefix = oldPrefix;
}
}
/**
* @constructor
+ * @param {!Array.<!Runtime.ModuleDescriptor>} descriptors
+ * @param {!Array.<string>=} coreModuleNames
*/
-var Runtime = function()
+function Runtime(descriptors, coreModuleNames)
{
/**
* @type {!Array.<!Runtime.Module>}
*/
this._descriptorsMap = {};
- for (var i = 0; i < allDescriptors.length; ++i)
- this._descriptorsMap[allDescriptors[i]["name"]] = allDescriptors[i];
+ for (var i = 0; i < descriptors.length; ++i)
+ this._registerModule(descriptors[i]);
+ if (coreModuleNames)
+ this._loadAutoStartModules(coreModuleNames).catch(Runtime._reportError);
}
/**
Runtime._queryParamsObject = { __proto__: null };
/**
+ * @type {!Object.<string, string>}
+ */
+Runtime.cachedResources = { __proto__: null };
+
+/**
* @return {boolean}
*/
Runtime.isReleaseMode = function()
Runtime.startApplication = function(appName)
{
console.timeStamp("Runtime.startApplication");
- var experiments = Runtime._experimentsSetting();
var allDescriptorsByName = {};
for (var i = 0; Runtime.isReleaseMode() && i < allDescriptors.length; ++i) {
var d = allDescriptors[i];
allDescriptorsByName[d["name"]] = d;
}
- var moduleDescriptors = applicationDescriptor || Runtime._parseJsonURL(appName + ".json");
- var allModules = [];
- var coreModuleNames = [];
- moduleDescriptors.forEach(populateModules);
+
+ var applicationPromise;
+ if (applicationDescriptor)
+ applicationPromise = Promise.resolve(applicationDescriptor);
+ else
+ applicationPromise = loadResourcePromise(appName + ".json").then(JSON.parse.bind(JSON));
+
+ applicationPromise.then(parseModuleDescriptors);
/**
- * @param {!Object} desc
+ * @param {!Array.<!Object>} configuration
*/
- function populateModules(desc)
+ function parseModuleDescriptors(configuration)
{
- if (!isActive(desc))
- return;
- var name = desc.name;
- var moduleJSON = allDescriptorsByName[name];
- if (!moduleJSON) {
- moduleJSON = Runtime._parseJsonURL(name + "/module.json");
- moduleJSON["name"] = name;
+ var moduleJSONPromises = [];
+ var coreModuleNames = [];
+ for (var i = 0; i < configuration.length; ++i) {
+ var descriptor = configuration[i];
+ if (descriptor["type"] === "worker")
+ continue;
+ var name = descriptor["name"];
+ var moduleJSON = allDescriptorsByName[name];
+ if (moduleJSON)
+ moduleJSONPromises.push(Promise.resolve(moduleJSON));
+ else
+ moduleJSONPromises.push(loadResourcePromise(name + "/module.json").then(JSON.parse.bind(JSON)));
+ if (descriptor["type"] === "autostart")
+ coreModuleNames.push(name);
}
- allModules.push(moduleJSON);
- if (desc["type"] === "autostart")
- coreModuleNames.push(name);
- }
- /**
- * @param {!Object} descriptor
- * @return {boolean}
- */
- function isActive(descriptor)
- {
- var activatorExperiment = descriptor["experiment"];
- if (activatorExperiment) {
- var shouldBePresent = activatorExperiment.charAt(0) !== "!";
- if (!shouldBePresent)
- activatorExperiment = activatorExperiment.substr(1);
- if (!!experiments[activatorExperiment] !== shouldBePresent)
- return false;
+ Promise.all(moduleJSONPromises).then(instantiateRuntime).catch(Runtime._reportError);
+ /**
+ * @param {!Array.<!Object>} moduleDescriptors
+ */
+ function instantiateRuntime(moduleDescriptors)
+ {
+ for (var i = 0; !Runtime.isReleaseMode() && i < moduleDescriptors.length; ++i)
+ moduleDescriptors[i]["name"] = configuration[i]["name"];
+ self.runtime = new Runtime(moduleDescriptors, coreModuleNames);
}
- return descriptor["type"] !== "worker";
}
-
- Runtime._initializeApplication(allModules);
- self.runtime.loadAutoStartModules(coreModuleNames);
}
/**
}
/**
- * @param {!Array.<!Runtime.ModuleDescriptor>} descriptors
+ * @param {!Array.<!Promise.<T, !Error>>} promises
+ * @return {!Promise.<!Array.<T>>}
+ * @template T
*/
-Runtime._initializeApplication = function(descriptors)
+Runtime._some = function(promises)
{
- self.runtime = new Runtime();
- var names = [];
- for (var i = 0; i < descriptors.length; ++i) {
- var name = descriptors[i]["name"];
- self.runtime._descriptorsMap[name] = descriptors[i];
- names.push(name);
+ var all = [];
+ var wasRejected = [];
+ for (var i = 0; i < promises.length; ++i) {
+ // Workaround closure compiler bug.
+ var handlerFunction = /** @type {function()} */ (handler.bind(promises[i], i));
+ all.push(promises[i].catch(handlerFunction));
+ }
+
+ return Promise.all(all).then(filterOutFailuresResults);
+
+ /**
+ * @param {!Array.<T>} results
+ * @return {!Array.<T>}
+ * @template T
+ */
+ function filterOutFailuresResults(results)
+ {
+ var filtered = [];
+ for (var i = 0; i < results.length; ++i) {
+ if (!wasRejected[i])
+ filtered.push(results[i]);
+ }
+ return filtered;
+ }
+
+ /**
+ * @this {!Promise}
+ * @param {number} index
+ * @param {!Error} e
+ */
+ function handler(index, e)
+ {
+ wasRejected[index] = true;
+ console.error(e.stack);
}
- self.runtime._registerModules(names);
+}
+
+Runtime._console = console;
+Runtime._originalAssert = console.assert;
+Runtime._assert = function(value, message)
+{
+ if (value)
+ return;
+ Runtime._originalAssert.call(Runtime._console, value, message);
}
/**
- * @param {string} url
- * @return {*}
+ * @param {*} e
*/
-Runtime._parseJsonURL = function(url)
+Runtime._reportError = function(e)
{
- var json = loadResource(url);
- if (!json)
- throw new Error("Resource not found at " + url + " " + new Error().stack);
- return JSON.parse(json);
+ if (e instanceof Error)
+ console.error(e.stack);
+ else
+ console.error(e);
}
Runtime.prototype = {
- /**
- * @param {!Array.<string>} configuration
- */
- _registerModules: function(configuration)
- {
- for (var i = 0; i < configuration.length; ++i)
- this._registerModule(configuration[i]);
- },
/**
- * @param {string} moduleName
+ * @param {!Runtime.ModuleDescriptor} descriptor
*/
- _registerModule: function(moduleName)
+ _registerModule: function(descriptor)
{
- if (!this._descriptorsMap[moduleName]) {
- var content = loadResource(moduleName + "/module.json");
- if (!content)
- throw new Error("Module is not defined: " + moduleName + " " + new Error().stack);
-
- var module = /** @type {!Runtime.ModuleDescriptor} */ (self.eval("(" + content + ")"));
- module["name"] = moduleName;
- this._descriptorsMap[moduleName] = module;
- }
- var module = new Runtime.Module(this, this._descriptorsMap[moduleName]);
+ var module = new Runtime.Module(this, descriptor);
this._modules.push(module);
- this._modulesMap[moduleName] = module;
+ this._modulesMap[descriptor["name"]] = module;
},
/**
* @param {string} moduleName
+ * @return {!Promise.<undefined>}
*/
- loadModule: function(moduleName)
+ loadModulePromise: function(moduleName)
{
- this._modulesMap[moduleName]._load();
+ return this._modulesMap[moduleName]._loadPromise();
},
/**
* @param {!Array.<string>} moduleNames
+ * @return {!Promise.<!Array.<*>>}
*/
- loadAutoStartModules: function(moduleNames)
+ _loadAutoStartModules: function(moduleNames)
{
+ var promises = [];
for (var i = 0; i < moduleNames.length; ++i) {
if (Runtime.isReleaseMode())
- self.runtime._modulesMap[moduleNames[i]]._loaded = true;
+ this._modulesMap[moduleNames[i]]._loaded = true;
else
- self.runtime.loadModule(moduleNames[i]);
+ promises.push(this.loadModulePromise(moduleNames[i]));
}
+ return Promise.all(promises);
},
/**
/**
* @param {!Runtime.Extension} extension
- * @param {!Array.<!Function>=} currentContextTypes
+ * @param {!Set.<!Function>=} currentContextTypes
* @return {boolean}
*/
isExtensionApplicableToContextTypes: function(extension, currentContextTypes)
if (!extension.descriptor().contextTypes)
return true;
- // FIXME: Remove this workaround once Set is available natively.
- for (var i = 0; i < currentContextTypes.length; ++i)
- currentContextTypes[i]["__applicable"] = true;
- var result = this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null);
- for (var i = 0; i < currentContextTypes.length; ++i)
- delete currentContextTypes[i]["__applicable"];
- return result;
+ return this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null);
/**
* @param {!Function} targetType
*/
function isContextTypeKnown(targetType)
{
- return !!targetType["__applicable"];
+ return currentContextTypes.has(targetType);
}
},
*/
extensions: function(type, context)
{
+ return this._extensions.filter(filter).sort(orderComparator);
+
/**
* @param {!Runtime.Extension} extension
* @return {boolean}
var activatorExperiment = extension.descriptor()["experiment"];
if (activatorExperiment && !Runtime.experiments.isEnabled(activatorExperiment))
return false;
+ activatorExperiment = extension._module._descriptor["experiment"];
+ if (activatorExperiment && !Runtime.experiments.isEnabled(activatorExperiment))
+ return false;
return !context || extension.isApplicable(context);
}
- return this._extensions.filter(filter);
+
+ /**
+ * @param {!Runtime.Extension} extension1
+ * @param {!Runtime.Extension} extension2
+ * @return {number}
+ */
+ function orderComparator(extension1, extension2)
+ {
+ var order1 = extension1.descriptor()["order"] || 0;
+ var order2 = extension2.descriptor()["order"] || 0;
+ return order1 - order2;
+ }
},
/**
/**
* @param {*} type
* @param {?Object=} context
- * @return {!Array.<!Object>}
+ * @return {!Promise.<!Array.<!Object>>}
*/
- instances: function(type, context)
+ instancesPromise: function(type, context)
{
- /**
- * @param {!Runtime.Extension} extension
- * @return {?Object}
- */
- function instantiate(extension)
- {
- return extension.instance();
- }
- return this.extensions(type, context).filter(instantiate).map(instantiate);
+ var extensions = this.extensions(type, context);
+ var promises = [];
+ for (var i = 0; i < extensions.length; ++i)
+ promises.push(extensions[i].instancePromise());
+ return Runtime._some(promises);
},
/**
* @param {*} type
* @param {?Object=} context
- * @return {?Object}
+ * @return {!Promise.<!Object>}
*/
- instance: function(type, context)
+ instancePromise: function(type, context)
{
var extension = this.extension(type, context);
- return extension ? extension.instance() : null;
- },
-
- /**
- * @param {string|!Function} type
- * @param {string} nameProperty
- * @param {string} orderProperty
- * @return {function(string, string):number}
- */
- orderComparator: function(type, nameProperty, orderProperty)
- {
- var extensions = this.extensions(type);
- var orderForName = {};
- for (var i = 0; i < extensions.length; ++i) {
- var descriptor = extensions[i].descriptor();
- orderForName[descriptor[nameProperty]] = descriptor[orderProperty];
- }
-
- /**
- * @param {string} name1
- * @param {string} name2
- * @return {number}
- */
- function result(name1, name2)
- {
- if (name1 in orderForName && name2 in orderForName)
- return orderForName[name1] - orderForName[name2];
- if (name1 in orderForName)
- return -1;
- if (name2 in orderForName)
- return 1;
- return compare(name1, name2);
- }
-
- /**
- * @param {string} left
- * @param {string} right
- * @return {number}
- */
- function compare(left, right)
- {
- if (left > right)
- return 1;
- if (left < right)
- return -1;
- return 0;
- }
- return result;
+ if (!extension)
+ return Promise.reject(new Error("No such extension: " + type + " in given context."));
+ return extension.instancePromise();
},
/**
if (object)
this._cachedTypeClasses[typeName] = /** @type function(new:Object) */(object);
}
- return this._cachedTypeClasses[typeName];
+ return this._cachedTypeClasses[typeName] || null;
}
}
this._manager = manager;
this._descriptor = descriptor;
this._name = descriptor.name;
+ /** @type {!Object.<string, ?Object>} */
+ this._instanceMap = {};
var extensions = /** @type {?Array.<!Runtime.ExtensionDescriptor>} */ (descriptor.extensions);
for (var i = 0; extensions && i < extensions.length; ++i)
this._manager._extensions.push(new Runtime.Extension(this, extensions[i]));
return this._name;
},
- _load: function()
+ /**
+ * @return {!Promise.<undefined>}
+ */
+ _loadPromise: function()
{
if (this._loaded)
- return;
+ return Promise.resolve();
- if (this._isLoading) {
- var oldStackTraceLimit = Error.stackTraceLimit;
- Error.stackTraceLimit = 50;
- console.assert(false, "Module " + this._name + " is loaded from itself: " + new Error().stack);
- Error.stackTraceLimit = oldStackTraceLimit;
- return;
- }
+ if (this._pendingLoadPromise)
+ return this._pendingLoadPromise;
- this._isLoading = true;
var dependencies = this._descriptor.dependencies;
+ var dependencyPromises = [];
for (var i = 0; dependencies && i < dependencies.length; ++i)
- this._manager.loadModule(dependencies[i]);
- if (this._descriptor.scripts) {
- if (Runtime.isReleaseMode()) {
- loadScript(this._name + "_module.js");
- } else {
- var scripts = this._descriptor.scripts;
- for (var i = 0; i < scripts.length; ++i)
- loadScript(this._name + "/" + scripts[i]);
- }
+ dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise());
+
+ this._pendingLoadPromise = Promise.all(dependencyPromises)
+ .then(this._loadScripts.bind(this))
+ .then(markAsLoaded.bind(this));
+
+ return this._pendingLoadPromise;
+
+ /**
+ * @this {Runtime.Module}
+ */
+ function markAsLoaded()
+ {
+ delete this._pendingLoadPromise;
+ this._loaded = true;
+ }
+ },
+
+ /**
+ * @return {!Promise.<undefined>}
+ */
+ _loadScripts: function()
+ {
+ if (!this._descriptor.scripts)
+ return Promise.resolve(undefined);
+
+ if (Runtime.isReleaseMode())
+ return loadScriptsPromise([this._name + "_module.js"]);
+
+ return loadScriptsPromise(this._descriptor.scripts.map(modularizeURL, this)).catch(Runtime._reportError);
+
+ /**
+ * @param {string} scriptName
+ * @this {Runtime.Module}
+ */
+ function modularizeURL(scriptName)
+ {
+ return this._name + "/" + scriptName;
}
- this._isLoading = false;
- this._loaded = true;
+ },
+
+ /**
+ * @param {string} className
+ * @return {?Object}
+ */
+ _instance: function(className)
+ {
+ if (className in this._instanceMap)
+ return this._instanceMap[className];
+
+ var constructorFunction = window.eval(className);
+ if (!(constructorFunction instanceof Function)) {
+ this._instanceMap[className] = null;
+ return null;
+ }
+
+ var instance = new constructorFunction();
+ this._instanceMap[className] = instance;
+ return instance;
}
}
},
/**
- * @return {?Object}
+ * @return {!Promise.<!Object>}
*/
- instance: function()
+ instancePromise: function()
{
if (!this._className)
- return null;
-
- if (!this._instance) {
- this._module._load();
+ return Promise.reject(new Error("No class name in extension"));
+ var className = this._className;
+ if (this._instance)
+ return Promise.resolve(this._instance);
- var constructorFunction = window.eval(this._className);
- if (!(constructorFunction instanceof Function))
- return null;
+ return this._module._loadPromise().then(constructInstance.bind(this));
- this._instance = new constructorFunction();
+ /**
+ * @return {!Object}
+ * @this {Runtime.Extension}
+ */
+ function constructInstance()
+ {
+ var result = this._module._instance(className);
+ if (!result)
+ return Promise.reject("Could not instantiate: " + className);
+ return result;
}
- return this._instance;
}
}
/**
* @return {!Array.<!Runtime.Experiment>}
*/
- allExperiments: function()
+ allConfigurableExperiments: function()
{
- return this._experiments.slice();
+ var result = [];
+ for (var i = 0; i < this._experiments.length; i++) {
+ var experiment = this._experiments[i];
+ if (!this._enabledTransiently[experiment.name])
+ result.push(experiment);
+ }
+ return result;
},
/**
*/
register: function(experimentName, experimentTitle, hidden)
{
- console.assert(!this._experimentNames[experimentName], "Duplicate registration of experiment " + experimentName);
+ Runtime._assert(!this._experimentNames[experimentName], "Duplicate registration of experiment " + experimentName);
this._experimentNames[experimentName] = true;
this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden));
},
setEnabled: function(experimentName, enabled)
{
this._checkExperiment(experimentName);
- if (!enabled)
- delete this._enabledTransiently[experimentName];
var experimentsSetting = Runtime._experimentsSetting();
experimentsSetting[experimentName] = enabled;
this._setExperimentsSetting(experimentsSetting);
*/
_checkExperiment: function(experimentName)
{
- console.assert(this._experimentNames[experimentName], "Unknown experiment " + experimentName);
+ Runtime._assert(this._experimentNames[experimentName], "Unknown experiment " + experimentName);
}
}
}
})();}
+
// This must be constructed after the query parameters have been parsed.
Runtime.experiments = new Runtime.ExperimentsSupport();