blob: 817c59379b068c965dc881a3e982fce59a256b0f [file] [log] [blame]
/*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// This gets all concatenated module descriptors in the release mode.
var allDescriptors = [];
var _loadedScripts = {};
/**
* @param {string} url
* @return {string}
*/
function loadResource(url)
{
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
try {
xhr.send(null);
} catch (e) {
console.error(url + " -> " + new Error().stack);
throw e;
}
return xhr.responseText;
}
/**
* http://tools.ietf.org/html/rfc3986#section-5.2.4
* @param {string} path
* @return {string}
*/
function normalizePath(path)
{
if (path.indexOf("..") === -1 && path.indexOf('.') === -1)
return path;
var normalizedSegments = [];
var segments = path.split("/");
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
if (segment === ".")
continue;
else if (segment === "..")
normalizedSegments.pop();
else if (segment)
normalizedSegments.push(segment);
}
var normalizedPath = normalizedSegments.join("/");
if (normalizedPath[normalizedPath.length - 1] === "/")
return normalizedPath;
if (path[0] === "/" && normalizedPath)
normalizedPath = "/" + normalizedPath;
if ((path[path.length - 1] === "/") || (segments[segments.length - 1] === ".") || (segments[segments.length - 1] === ".."))
normalizedPath = normalizedPath + "/";
return normalizedPath;
}
/**
* @param {string} scriptName
*/
function loadScript(scriptName)
{
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)
throw "empty response arrived for script '" + sourceURL + "'";
var oldPrefix = self._importScriptPathPrefix;
self._importScriptPathPrefix += scriptName.substring(0, scriptName.lastIndexOf("/") + 1);
try {
self.eval(scriptSource + "\n//# sourceURL=" + sourceURL);
} finally {
self._importScriptPathPrefix = oldPrefix;
}
}
(function() {
var baseUrl = location.origin + location.pathname;
self._importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1);
})();
/**
* @constructor
*/
var Runtime = function()
{
/**
* @type {!Array.<!Runtime.Module>}
*/
this._modules = [];
/**
* @type {!Object.<string, !Runtime.Module>}
*/
this._modulesMap = {};
/**
* @type {!Array.<!Runtime.Extension>}
*/
this._extensions = [];
/**
* @type {!Object.<string, !function(new:Object)>}
*/
this._cachedTypeClasses = {};
/**
* @type {!Object.<string, !Runtime.ModuleDescriptor>}
*/
this._descriptorsMap = {};
/**
* @type {!Object.<string, boolean>}
*/
this._coreModules = {};
for (var i = 0; i < allDescriptors.length; ++i)
this._descriptorsMap[allDescriptors[i]["name"]] = allDescriptors[i];
}
/**
* @return {boolean}
*/
Runtime.isReleaseMode = function()
{
return !!allDescriptors.length;
}
/**
* @param {string} moduleName
* @param {string} workerName
* @return {!SharedWorker}
*/
Runtime.startSharedWorker = function(moduleName, workerName)
{
if (Runtime.isReleaseMode())
return new SharedWorker(moduleName + ".js", workerName);
var content = loadResource(moduleName + "/module.json");
if (!content)
throw new Error("Worker is not defined: " + moduleName + " " + new Error().stack);
var scripts = JSON.parse(content)["scripts"];
if (scripts.length !== 1)
throw Error("Runtime.startSharedWorker supports modules with only one script!");
return new SharedWorker(moduleName + "/" + scripts[0], workerName);
}
/**
* @param {string} moduleName
* @return {!Worker}
*/
Runtime.startWorker = function(moduleName)
{
if (Runtime.isReleaseMode())
return new Worker(moduleName + ".js");
var content = loadResource(moduleName + "/module.json");
if (!content)
throw new Error("Worker is not defined: " + moduleName + " " + new Error().stack);
var message = [];
var scripts = JSON.parse(content)["scripts"];
for (var i = 0; i < scripts.length; ++i) {
var url = self._importScriptPathPrefix + moduleName + "/" + scripts[i];
var parts = url.split("://");
url = parts.length === 1 ? url : parts[0] + "://" + normalizePath(parts[1]);
message.push({
source: loadResource(moduleName + "/" + scripts[i]),
url: url
});
}
/**
* @suppress {checkTypes}
*/
var loader = function() {
self.onmessage = function(event) {
self.onmessage = null;
var scripts = event.data;
for (var i = 0; i < scripts.length; ++i) {
var source = scripts[i]["source"];
self.eval(source + "\n//# sourceURL=" + scripts[i]["url"]);
}
};
};
var blob = new Blob(["(" + loader.toString() + ")()\n//# sourceURL=" + moduleName], { type: "text/javascript" });
var workerURL = window.URL.createObjectURL(blob);
try {
var worker = new Worker(workerURL);
worker.postMessage(message);
return worker;
} finally {
window.URL.revokeObjectURL(workerURL);
}
}
Runtime.prototype = {
/**
* @param {!Array.<string>} configuration
* @param {!Array.<string>} coreModules
*/
registerModules: function(configuration, coreModules)
{
for (var i = 0; i < coreModules.length; ++i)
this._coreModules[coreModules[i]] = true;
for (var i = 0; i < configuration.length; ++i)
this._registerModule(configuration[i]);
},
/**
* @param {string} moduleName
*/
_registerModule: function(moduleName)
{
if (!this._descriptorsMap[moduleName]) {
var content;
// FIXME: This is a temp workaround to avoid core module loading attempts in the release mode.
// It should be removed when the new application loader is implemented.
if (!this._coreModules.hasOwnProperty(moduleName))
content = loadResource(moduleName + "/module.json");
else
content = "{}";
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]);
this._modules.push(module);
this._modulesMap[moduleName] = module;
},
/**
* @param {string} moduleName
*/
loadModule: function(moduleName)
{
this._modulesMap[moduleName]._load();
},
/**
* @param {!Runtime.Extension} extension
* @param {?function(function(new:Object)):boolean} predicate
* @return {boolean}
*/
_checkExtensionApplicability: function(extension, predicate)
{
if (!predicate)
return false;
var contextTypes = /** @type {!Array.<string>|undefined} */ (extension.descriptor().contextTypes);
if (!contextTypes)
return true;
for (var i = 0; i < contextTypes.length; ++i) {
var contextType = this._resolve(contextTypes[i]);
var isMatching = !!contextType && predicate(contextType);
if (isMatching)
return true;
}
return false;
},
/**
* @param {!Runtime.Extension} extension
* @param {?Object} context
* @return {boolean}
*/
isExtensionApplicableToContext: function(extension, context)
{
if (!context)
return true;
return this._checkExtensionApplicability(extension, isInstanceOf);
/**
* @param {!Function} targetType
* @return {boolean}
*/
function isInstanceOf(targetType)
{
return context instanceof targetType;
}
},
/**
* @param {!Runtime.Extension} extension
* @param {!Array.<!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;
/**
* @param {!Function} targetType
* @return {boolean}
*/
function isContextTypeKnown(targetType)
{
return !!targetType["__applicable"];
}
},
/**
* @param {*} type
* @param {?Object=} context
* @return {!Array.<!Runtime.Extension>}
*/
extensions: function(type, context)
{
/**
* @param {!Runtime.Extension} extension
* @return {boolean}
*/
function filter(extension)
{
if (extension._type !== type && extension._typeClass() !== type)
return false;
return !context || extension.isApplicable(context);
}
return this._extensions.filter(filter);
},
/**
* @param {*} type
* @param {?Object=} context
* @return {?Runtime.Extension}
*/
extension: function(type, context)
{
return this.extensions(type, context)[0] || null;
},
/**
* @param {*} type
* @param {?Object=} context
* @return {!Array.<!Object>}
*/
instances: function(type, context)
{
/**
* @param {!Runtime.Extension} extension
* @return {?Object}
*/
function instantiate(extension)
{
return extension.instance();
}
return this.extensions(type, context).filter(instantiate).map(instantiate);
},
/**
* @param {*} type
* @param {?Object=} context
* @return {?Object}
*/
instance: 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;
},
/**
* @return {?function(new:Object)}
*/
_resolve: function(typeName)
{
if (!this._cachedTypeClasses[typeName]) {
var path = typeName.split(".");
var object = window;
for (var i = 0; object && (i < path.length); ++i)
object = object[path[i]];
if (object)
this._cachedTypeClasses[typeName] = /** @type function(new:Object) */(object);
}
return this._cachedTypeClasses[typeName];
}
}
/**
* @constructor
*/
Runtime.ModuleDescriptor = function()
{
/**
* @type {string}
*/
this.name;
/**
* @type {!Array.<!Runtime.ExtensionDescriptor>}
*/
this.extensions;
/**
* @type {!Array.<string>|undefined}
*/
this.dependencies;
/**
* @type {!Array.<string>}
*/
this.scripts;
}
/**
* @constructor
*/
Runtime.ExtensionDescriptor = function()
{
/**
* @type {string}
*/
this.type;
/**
* @type {string|undefined}
*/
this.className;
/**
* @type {!Array.<string>|undefined}
*/
this.contextTypes;
}
/**
* @constructor
* @param {!Runtime} manager
* @param {!Runtime.ModuleDescriptor} descriptor
*/
Runtime.Module = function(manager, descriptor)
{
this._manager = manager;
this._descriptor = descriptor;
this._name = descriptor.name;
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]));
this._loaded = false;
}
Runtime.Module.prototype = {
/**
* @return {string}
*/
name: function()
{
return this._name;
},
_load: function()
{
if (this._loaded)
return;
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;
}
this._isLoading = true;
var dependencies = this._descriptor.dependencies;
for (var i = 0; dependencies && i < dependencies.length; ++i)
this._manager.loadModule(dependencies[i]);
if (this._descriptor.scripts) {
if (Runtime.isReleaseMode()) {
// FIXME: This is a temp workaround to avoid core module loading attempts in the release mode.
// It should be removed when the new application loader is implemented.
if (!this._manager._coreModules.hasOwnProperty(this._name))
loadScript(this._name + ".js");
} else {
var scripts = this._descriptor.scripts;
for (var i = 0; i < scripts.length; ++i)
loadScript(this._name + "/" + scripts[i]);
}
}
this._isLoading = false;
this._loaded = true;
}
}
/**
* @constructor
* @param {!Runtime.Module} module
* @param {!Runtime.ExtensionDescriptor} descriptor
*/
Runtime.Extension = function(module, descriptor)
{
this._module = module;
this._descriptor = descriptor;
this._type = descriptor.type;
this._hasTypeClass = this._type.charAt(0) === "@";
/**
* @type {?string}
*/
this._className = descriptor.className || null;
}
Runtime.Extension.prototype = {
/**
* @return {!Object}
*/
descriptor: function()
{
return this._descriptor;
},
/**
* @return {!Runtime.Module}
*/
module: function()
{
return this._module;
},
/**
* @return {?function(new:Object)}
*/
_typeClass: function()
{
if (!this._hasTypeClass)
return null;
return this._module._manager._resolve(this._type.substring(1));
},
/**
* @param {?Object} context
* @return {boolean}
*/
isApplicable: function(context)
{
return this._module._manager.isExtensionApplicableToContext(this, context);
},
/**
* @return {?Object}
*/
instance: function()
{
if (!this._className)
return null;
if (!this._instance) {
this._module._load();
var constructorFunction = window.eval(this._className);
if (!(constructorFunction instanceof Function))
return null;
this._instance = new constructorFunction();
}
return this._instance;
}
}
/**
* @type {!Runtime}
*/
var runtime;