Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2014 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 // This gets all concatenated module descriptors in the release mode.
     32 var allDescriptors = [];
     33 var applicationDescriptor;
     34 var _loadedScripts = {};
     35 
     36 /**
     37  * @param {string} url
     38  * @return {string}
     39  */
     40 function loadResource(url)
     41 {
     42     var xhr = new XMLHttpRequest();
     43     xhr.open("GET", url, false);
     44     try {
     45         xhr.send(null);
     46     } catch (e) {
     47         console.error(url + " -> " + new Error().stack);
     48         throw e;
     49     }
     50     // xhr.status === 0 if loading from bundle.
     51     return xhr.status < 400 ? xhr.responseText : "";
     52 }
     53 
     54 
     55 /**
     56  * http://tools.ietf.org/html/rfc3986#section-5.2.4
     57  * @param {string} path
     58  * @return {string}
     59  */
     60 function normalizePath(path)
     61 {
     62     if (path.indexOf("..") === -1 && path.indexOf('.') === -1)
     63         return path;
     64 
     65     var normalizedSegments = [];
     66     var segments = path.split("/");
     67     for (var i = 0; i < segments.length; i++) {
     68         var segment = segments[i];
     69         if (segment === ".")
     70             continue;
     71         else if (segment === "..")
     72             normalizedSegments.pop();
     73         else if (segment)
     74             normalizedSegments.push(segment);
     75     }
     76     var normalizedPath = normalizedSegments.join("/");
     77     if (normalizedPath[normalizedPath.length - 1] === "/")
     78         return normalizedPath;
     79     if (path[0] === "/" && normalizedPath)
     80         normalizedPath = "/" + normalizedPath;
     81     if ((path[path.length - 1] === "/") || (segments[segments.length - 1] === ".") || (segments[segments.length - 1] === ".."))
     82         normalizedPath = normalizedPath + "/";
     83 
     84     return normalizedPath;
     85 }
     86 
     87 /**
     88  * @param {string} scriptName
     89  */
     90 function loadScript(scriptName)
     91 {
     92     var sourceURL = self._importScriptPathPrefix + scriptName;
     93     var schemaIndex = sourceURL.indexOf("://") + 3;
     94     sourceURL = sourceURL.substring(0, schemaIndex) + normalizePath(sourceURL.substring(schemaIndex));
     95     if (_loadedScripts[sourceURL])
     96         return;
     97     _loadedScripts[sourceURL] = true;
     98     var scriptSource = loadResource(sourceURL);
     99     if (!scriptSource) {
    100         console.error("Empty response arrived for script '" + sourceURL + "'");
    101         return;
    102     }
    103     var oldPrefix = self._importScriptPathPrefix;
    104     self._importScriptPathPrefix += scriptName.substring(0, scriptName.lastIndexOf("/") + 1);
    105     try {
    106         self.eval(scriptSource + "\n//# sourceURL=" + sourceURL);
    107     } finally {
    108         self._importScriptPathPrefix = oldPrefix;
    109     }
    110 }
    111 
    112 (function() {
    113     var baseUrl = self.location ? self.location.origin + self.location.pathname : "";
    114     self._importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1);
    115 })();
    116 
    117 /**
    118  * @constructor
    119  */
    120 var Runtime = function()
    121 {
    122     /**
    123      * @type {!Array.<!Runtime.Module>}
    124      */
    125     this._modules = [];
    126     /**
    127      * @type {!Object.<string, !Runtime.Module>}
    128      */
    129     this._modulesMap = {};
    130     /**
    131      * @type {!Array.<!Runtime.Extension>}
    132      */
    133     this._extensions = [];
    134 
    135     /**
    136      * @type {!Object.<string, !function(new:Object)>}
    137      */
    138     this._cachedTypeClasses = {};
    139 
    140     /**
    141      * @type {!Object.<string, !Runtime.ModuleDescriptor>}
    142      */
    143     this._descriptorsMap = {};
    144 
    145     for (var i = 0; i < allDescriptors.length; ++i)
    146         this._descriptorsMap[allDescriptors[i]["name"]] = allDescriptors[i];
    147 }
    148 
    149 /**
    150  * @type {!Object.<string, string>}
    151  */
    152 Runtime._queryParamsObject = { __proto__: null };
    153 
    154 /**
    155  * @return {boolean}
    156  */
    157 Runtime.isReleaseMode = function()
    158 {
    159     return !!allDescriptors.length;
    160 }
    161 
    162 /**
    163  * @param {string} moduleName
    164  * @param {string} workerName
    165  * @return {!SharedWorker}
    166  */
    167 Runtime.startSharedWorker = function(moduleName, workerName)
    168 {
    169     if (Runtime.isReleaseMode())
    170         return new SharedWorker(moduleName + "_module.js", workerName);
    171 
    172     var content = loadResource(moduleName + "/module.json");
    173     if (!content)
    174         throw new Error("Worker is not defined: " + moduleName + " " + new Error().stack);
    175     var scripts = JSON.parse(content)["scripts"];
    176     if (scripts.length !== 1)
    177         throw Error("Runtime.startSharedWorker supports modules with only one script!");
    178     return new SharedWorker(moduleName + "/" + scripts[0], workerName);
    179 }
    180 
    181 /**
    182  * @param {string} moduleName
    183  * @return {!Worker}
    184  */
    185 Runtime.startWorker = function(moduleName)
    186 {
    187     if (Runtime.isReleaseMode())
    188         return new Worker(moduleName + "_module.js");
    189 
    190     var content = loadResource(moduleName + "/module.json");
    191     if (!content)
    192         throw new Error("Worker is not defined: " + moduleName + " " + new Error().stack);
    193     var message = [];
    194     var scripts = JSON.parse(content)["scripts"];
    195     for (var i = 0; i < scripts.length; ++i) {
    196         var url = self._importScriptPathPrefix + moduleName + "/" + scripts[i];
    197         var parts = url.split("://");
    198         url = parts.length === 1 ? url : parts[0] + "://" + normalizePath(parts[1]);
    199         message.push({
    200             source: loadResource(moduleName + "/" + scripts[i]),
    201             url: url
    202         });
    203     }
    204 
    205     /**
    206      * @suppress {checkTypes}
    207      */
    208     var loader = function() {
    209         self.onmessage = function(event) {
    210             self.onmessage = null;
    211             var scripts = event.data;
    212             for (var i = 0; i < scripts.length; ++i) {
    213                 var source = scripts[i]["source"];
    214                 self.eval(source + "\n//# sourceURL=" + scripts[i]["url"]);
    215             }
    216         };
    217     };
    218 
    219     var blob = new Blob(["(" + loader.toString() + ")()\n//# sourceURL=" + moduleName], { type: "text/javascript" });
    220     var workerURL = window.URL.createObjectURL(blob);
    221     try {
    222         var worker = new Worker(workerURL);
    223         worker.postMessage(message);
    224         return worker;
    225     } finally {
    226         window.URL.revokeObjectURL(workerURL);
    227     }
    228 }
    229 
    230 /**
    231  * @param {string} appName
    232  */
    233 Runtime.startApplication = function(appName)
    234 {
    235     console.timeStamp("Runtime.startApplication");
    236     var experiments = Runtime._experimentsSetting();
    237 
    238     var allDescriptorsByName = {};
    239     for (var i = 0; Runtime.isReleaseMode() && i < allDescriptors.length; ++i) {
    240         var d = allDescriptors[i];
    241         allDescriptorsByName[d["name"]] = d;
    242     }
    243     var moduleDescriptors = applicationDescriptor || Runtime._parseJsonURL(appName + ".json");
    244     var allModules = [];
    245     var coreModuleNames = [];
    246     moduleDescriptors.forEach(populateModules);
    247 
    248     /**
    249      * @param {!Object} desc
    250      */
    251     function populateModules(desc)
    252     {
    253         if (!isActive(desc))
    254             return;
    255         var name = desc.name;
    256         var moduleJSON = allDescriptorsByName[name];
    257         if (!moduleJSON) {
    258             moduleJSON = Runtime._parseJsonURL(name + "/module.json");
    259             moduleJSON["name"] = name;
    260         }
    261         allModules.push(moduleJSON);
    262         if (desc["type"] === "autostart")
    263             coreModuleNames.push(name);
    264     }
    265 
    266     /**
    267      * @param {!Object} descriptor
    268      * @return {boolean}
    269      */
    270     function isActive(descriptor)
    271     {
    272         var activatorExperiment = descriptor["experiment"];
    273         if (activatorExperiment) {
    274             var shouldBePresent = activatorExperiment.charAt(0) !== "!";
    275             if (!shouldBePresent)
    276                 activatorExperiment = activatorExperiment.substr(1);
    277             if (!!experiments[activatorExperiment] !== shouldBePresent)
    278                 return false;
    279         }
    280         return descriptor["type"] !== "worker";
    281     }
    282 
    283     Runtime._initializeApplication(allModules);
    284     self.runtime.loadAutoStartModules(coreModuleNames);
    285 }
    286 
    287 /**
    288  * @param {string} name
    289  * @return {?string}
    290  */
    291 Runtime.queryParam = function(name)
    292 {
    293     return Runtime._queryParamsObject[name] || null;
    294 }
    295 
    296 /**
    297  * @return {!Object}
    298  */
    299 Runtime._experimentsSetting = function()
    300 {
    301     try {
    302         return /** @type {!Object} */ (JSON.parse(self.localStorage && self.localStorage["experiments"] ? self.localStorage["experiments"] : "{}"));
    303     } catch (e) {
    304         console.error("Failed to parse localStorage['experiments']");
    305         return {};
    306     }
    307 }
    308 
    309 /**
    310  * @param {!Array.<!Runtime.ModuleDescriptor>} descriptors
    311  */
    312 Runtime._initializeApplication = function(descriptors)
    313 {
    314     self.runtime = new Runtime();
    315     var names = [];
    316     for (var i = 0; i < descriptors.length; ++i) {
    317         var name = descriptors[i]["name"];
    318         self.runtime._descriptorsMap[name] = descriptors[i];
    319         names.push(name);
    320     }
    321     self.runtime._registerModules(names);
    322 }
    323 
    324 /**
    325  * @param {string} url
    326  * @return {*}
    327  */
    328 Runtime._parseJsonURL = function(url)
    329 {
    330     var json = loadResource(url);
    331     if (!json)
    332         throw new Error("Resource not found at " + url + " " + new Error().stack);
    333     return JSON.parse(json);
    334 }
    335 
    336 Runtime.prototype = {
    337     /**
    338      * @param {!Array.<string>} configuration
    339      */
    340     _registerModules: function(configuration)
    341     {
    342         for (var i = 0; i < configuration.length; ++i)
    343             this._registerModule(configuration[i]);
    344     },
    345 
    346     /**
    347      * @param {string} moduleName
    348      */
    349     _registerModule: function(moduleName)
    350     {
    351         if (!this._descriptorsMap[moduleName]) {
    352             var content = loadResource(moduleName + "/module.json");
    353             if (!content)
    354                 throw new Error("Module is not defined: " + moduleName + " " + new Error().stack);
    355 
    356             var module = /** @type {!Runtime.ModuleDescriptor} */ (self.eval("(" + content + ")"));
    357             module["name"] = moduleName;
    358             this._descriptorsMap[moduleName] = module;
    359         }
    360         var module = new Runtime.Module(this, this._descriptorsMap[moduleName]);
    361         this._modules.push(module);
    362         this._modulesMap[moduleName] = module;
    363     },
    364 
    365     /**
    366      * @param {string} moduleName
    367      */
    368     loadModule: function(moduleName)
    369     {
    370         this._modulesMap[moduleName]._load();
    371     },
    372 
    373     /**
    374      * @param {!Array.<string>} moduleNames
    375      */
    376     loadAutoStartModules: function(moduleNames)
    377     {
    378         for (var i = 0; i < moduleNames.length; ++i) {
    379             if (Runtime.isReleaseMode())
    380                 self.runtime._modulesMap[moduleNames[i]]._loaded = true;
    381             else
    382                 self.runtime.loadModule(moduleNames[i]);
    383         }
    384     },
    385 
    386     /**
    387      * @param {!Runtime.Extension} extension
    388      * @param {?function(function(new:Object)):boolean} predicate
    389      * @return {boolean}
    390      */
    391     _checkExtensionApplicability: function(extension, predicate)
    392     {
    393         if (!predicate)
    394             return false;
    395         var contextTypes = /** @type {!Array.<string>|undefined} */ (extension.descriptor().contextTypes);
    396         if (!contextTypes)
    397             return true;
    398         for (var i = 0; i < contextTypes.length; ++i) {
    399             var contextType = this._resolve(contextTypes[i]);
    400             var isMatching = !!contextType && predicate(contextType);
    401             if (isMatching)
    402                 return true;
    403         }
    404         return false;
    405     },
    406 
    407     /**
    408      * @param {!Runtime.Extension} extension
    409      * @param {?Object} context
    410      * @return {boolean}
    411      */
    412     isExtensionApplicableToContext: function(extension, context)
    413     {
    414         if (!context)
    415             return true;
    416         return this._checkExtensionApplicability(extension, isInstanceOf);
    417 
    418         /**
    419          * @param {!Function} targetType
    420          * @return {boolean}
    421          */
    422         function isInstanceOf(targetType)
    423         {
    424             return context instanceof targetType;
    425         }
    426     },
    427 
    428     /**
    429      * @param {!Runtime.Extension} extension
    430      * @param {!Array.<!Function>=} currentContextTypes
    431      * @return {boolean}
    432      */
    433     isExtensionApplicableToContextTypes: function(extension, currentContextTypes)
    434     {
    435         if (!extension.descriptor().contextTypes)
    436             return true;
    437 
    438         // FIXME: Remove this workaround once Set is available natively.
    439         for (var i = 0; i < currentContextTypes.length; ++i)
    440             currentContextTypes[i]["__applicable"] = true;
    441         var result = this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null);
    442         for (var i = 0; i < currentContextTypes.length; ++i)
    443             delete currentContextTypes[i]["__applicable"];
    444         return result;
    445 
    446         /**
    447          * @param {!Function} targetType
    448          * @return {boolean}
    449          */
    450         function isContextTypeKnown(targetType)
    451         {
    452             return !!targetType["__applicable"];
    453         }
    454     },
    455 
    456     /**
    457      * @param {*} type
    458      * @param {?Object=} context
    459      * @return {!Array.<!Runtime.Extension>}
    460      */
    461     extensions: function(type, context)
    462     {
    463         /**
    464          * @param {!Runtime.Extension} extension
    465          * @return {boolean}
    466          */
    467         function filter(extension)
    468         {
    469             if (extension._type !== type && extension._typeClass() !== type)
    470                 return false;
    471             var activatorExperiment = extension.descriptor()["experiment"];
    472             if (activatorExperiment && !Runtime.experiments.isEnabled(activatorExperiment))
    473                 return false;
    474             return !context || extension.isApplicable(context);
    475         }
    476         return this._extensions.filter(filter);
    477     },
    478 
    479     /**
    480      * @param {*} type
    481      * @param {?Object=} context
    482      * @return {?Runtime.Extension}
    483      */
    484     extension: function(type, context)
    485     {
    486         return this.extensions(type, context)[0] || null;
    487     },
    488 
    489     /**
    490      * @param {*} type
    491      * @param {?Object=} context
    492      * @return {!Array.<!Object>}
    493      */
    494     instances: function(type, context)
    495     {
    496         /**
    497          * @param {!Runtime.Extension} extension
    498          * @return {?Object}
    499          */
    500         function instantiate(extension)
    501         {
    502             return extension.instance();
    503         }
    504         return this.extensions(type, context).filter(instantiate).map(instantiate);
    505     },
    506 
    507     /**
    508      * @param {*} type
    509      * @param {?Object=} context
    510      * @return {?Object}
    511      */
    512     instance: function(type, context)
    513     {
    514         var extension = this.extension(type, context);
    515         return extension ? extension.instance() : null;
    516     },
    517 
    518     /**
    519      * @param {string|!Function} type
    520      * @param {string} nameProperty
    521      * @param {string} orderProperty
    522      * @return {function(string, string):number}
    523      */
    524     orderComparator: function(type, nameProperty, orderProperty)
    525     {
    526         var extensions = this.extensions(type);
    527         var orderForName = {};
    528         for (var i = 0; i < extensions.length; ++i) {
    529             var descriptor = extensions[i].descriptor();
    530             orderForName[descriptor[nameProperty]] = descriptor[orderProperty];
    531         }
    532 
    533         /**
    534          * @param {string} name1
    535          * @param {string} name2
    536          * @return {number}
    537          */
    538         function result(name1, name2)
    539         {
    540             if (name1 in orderForName && name2 in orderForName)
    541                 return orderForName[name1] - orderForName[name2];
    542             if (name1 in orderForName)
    543                 return -1;
    544             if (name2 in orderForName)
    545                 return 1;
    546             return compare(name1, name2);
    547         }
    548 
    549         /**
    550          * @param {string} left
    551          * @param {string} right
    552          * @return {number}
    553          */
    554         function compare(left, right)
    555         {
    556             if (left > right)
    557               return 1;
    558             if (left < right)
    559                 return -1;
    560             return 0;
    561         }
    562         return result;
    563     },
    564 
    565     /**
    566      * @return {?function(new:Object)}
    567      */
    568     _resolve: function(typeName)
    569     {
    570         if (!this._cachedTypeClasses[typeName]) {
    571             var path = typeName.split(".");
    572             var object = window;
    573             for (var i = 0; object && (i < path.length); ++i)
    574                 object = object[path[i]];
    575             if (object)
    576                 this._cachedTypeClasses[typeName] = /** @type function(new:Object) */(object);
    577         }
    578         return this._cachedTypeClasses[typeName];
    579     }
    580 }
    581 
    582 /**
    583  * @constructor
    584  */
    585 Runtime.ModuleDescriptor = function()
    586 {
    587     /**
    588      * @type {string}
    589      */
    590     this.name;
    591 
    592     /**
    593      * @type {!Array.<!Runtime.ExtensionDescriptor>}
    594      */
    595     this.extensions;
    596 
    597     /**
    598      * @type {!Array.<string>|undefined}
    599      */
    600     this.dependencies;
    601 
    602     /**
    603      * @type {!Array.<string>}
    604      */
    605     this.scripts;
    606 }
    607 
    608 /**
    609  * @constructor
    610  */
    611 Runtime.ExtensionDescriptor = function()
    612 {
    613     /**
    614      * @type {string}
    615      */
    616     this.type;
    617 
    618     /**
    619      * @type {string|undefined}
    620      */
    621     this.className;
    622 
    623     /**
    624      * @type {!Array.<string>|undefined}
    625      */
    626     this.contextTypes;
    627 }
    628 
    629 /**
    630  * @constructor
    631  * @param {!Runtime} manager
    632  * @param {!Runtime.ModuleDescriptor} descriptor
    633  */
    634 Runtime.Module = function(manager, descriptor)
    635 {
    636     this._manager = manager;
    637     this._descriptor = descriptor;
    638     this._name = descriptor.name;
    639     var extensions = /** @type {?Array.<!Runtime.ExtensionDescriptor>} */ (descriptor.extensions);
    640     for (var i = 0; extensions && i < extensions.length; ++i)
    641         this._manager._extensions.push(new Runtime.Extension(this, extensions[i]));
    642     this._loaded = false;
    643 }
    644 
    645 Runtime.Module.prototype = {
    646     /**
    647      * @return {string}
    648      */
    649     name: function()
    650     {
    651         return this._name;
    652     },
    653 
    654     _load: function()
    655     {
    656         if (this._loaded)
    657             return;
    658 
    659         if (this._isLoading) {
    660             var oldStackTraceLimit = Error.stackTraceLimit;
    661             Error.stackTraceLimit = 50;
    662             console.assert(false, "Module " + this._name + " is loaded from itself: " + new Error().stack);
    663             Error.stackTraceLimit = oldStackTraceLimit;
    664             return;
    665         }
    666 
    667         this._isLoading = true;
    668         var dependencies = this._descriptor.dependencies;
    669         for (var i = 0; dependencies && i < dependencies.length; ++i)
    670             this._manager.loadModule(dependencies[i]);
    671         if (this._descriptor.scripts) {
    672             if (Runtime.isReleaseMode()) {
    673                 loadScript(this._name + "_module.js");
    674             } else {
    675                 var scripts = this._descriptor.scripts;
    676                 for (var i = 0; i < scripts.length; ++i)
    677                     loadScript(this._name + "/" + scripts[i]);
    678             }
    679         }
    680         this._isLoading = false;
    681         this._loaded = true;
    682     }
    683 }
    684 
    685 /**
    686  * @constructor
    687  * @param {!Runtime.Module} module
    688  * @param {!Runtime.ExtensionDescriptor} descriptor
    689  */
    690 Runtime.Extension = function(module, descriptor)
    691 {
    692     this._module = module;
    693     this._descriptor = descriptor;
    694 
    695     this._type = descriptor.type;
    696     this._hasTypeClass = this._type.charAt(0) === "@";
    697 
    698     /**
    699      * @type {?string}
    700      */
    701     this._className = descriptor.className || null;
    702 }
    703 
    704 Runtime.Extension.prototype = {
    705     /**
    706      * @return {!Object}
    707      */
    708     descriptor: function()
    709     {
    710         return this._descriptor;
    711     },
    712 
    713     /**
    714      * @return {!Runtime.Module}
    715      */
    716     module: function()
    717     {
    718         return this._module;
    719     },
    720 
    721     /**
    722      * @return {?function(new:Object)}
    723      */
    724     _typeClass: function()
    725     {
    726         if (!this._hasTypeClass)
    727             return null;
    728         return this._module._manager._resolve(this._type.substring(1));
    729     },
    730 
    731     /**
    732      * @param {?Object} context
    733      * @return {boolean}
    734      */
    735     isApplicable: function(context)
    736     {
    737         return this._module._manager.isExtensionApplicableToContext(this, context);
    738     },
    739 
    740     /**
    741      * @return {?Object}
    742      */
    743     instance: function()
    744     {
    745         if (!this._className)
    746             return null;
    747 
    748         if (!this._instance) {
    749             this._module._load();
    750 
    751             var constructorFunction = window.eval(this._className);
    752             if (!(constructorFunction instanceof Function))
    753                 return null;
    754 
    755             this._instance = new constructorFunction();
    756         }
    757         return this._instance;
    758     }
    759 }
    760 
    761 /**
    762  * @constructor
    763  */
    764 Runtime.ExperimentsSupport = function()
    765 {
    766     this._supportEnabled = Runtime.queryParam("experiments") !== null;
    767     this._experiments = [];
    768     this._experimentNames = {};
    769     this._enabledTransiently = {};
    770 }
    771 
    772 Runtime.ExperimentsSupport.prototype = {
    773     /**
    774      * @return {!Array.<!Runtime.Experiment>}
    775      */
    776     allExperiments: function()
    777     {
    778         return this._experiments.slice();
    779     },
    780 
    781     /**
    782      * @return {boolean}
    783      */
    784     supportEnabled: function()
    785     {
    786         return this._supportEnabled;
    787     },
    788 
    789     /**
    790      * @param {!Object} value
    791      */
    792     _setExperimentsSetting: function(value)
    793     {
    794         if (!self.localStorage)
    795             return;
    796         self.localStorage["experiments"] = JSON.stringify(value);
    797     },
    798 
    799     /**
    800      * @param {string} experimentName
    801      * @param {string} experimentTitle
    802      * @param {boolean=} hidden
    803      */
    804     register: function(experimentName, experimentTitle, hidden)
    805     {
    806         console.assert(!this._experimentNames[experimentName], "Duplicate registration of experiment " + experimentName);
    807         this._experimentNames[experimentName] = true;
    808         this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden));
    809     },
    810 
    811     /**
    812      * @param {string} experimentName
    813      * @return {boolean}
    814      */
    815     isEnabled: function(experimentName)
    816     {
    817         this._checkExperiment(experimentName);
    818 
    819         if (this._enabledTransiently[experimentName])
    820             return true;
    821         if (!this.supportEnabled())
    822             return false;
    823 
    824         return !!Runtime._experimentsSetting()[experimentName];
    825     },
    826 
    827     /**
    828      * @param {string} experimentName
    829      * @param {boolean} enabled
    830      */
    831     setEnabled: function(experimentName, enabled)
    832     {
    833         this._checkExperiment(experimentName);
    834         if (!enabled)
    835             delete this._enabledTransiently[experimentName];
    836         var experimentsSetting = Runtime._experimentsSetting();
    837         experimentsSetting[experimentName] = enabled;
    838         this._setExperimentsSetting(experimentsSetting);
    839     },
    840 
    841     /**
    842      * @param {!Array.<string>} experimentNames
    843      */
    844     setDefaultExperiments: function(experimentNames)
    845     {
    846         for (var i = 0; i < experimentNames.length; ++i) {
    847             this._checkExperiment(experimentNames[i]);
    848             this._enabledTransiently[experimentNames[i]] = true;
    849         }
    850     },
    851 
    852     /**
    853      * @param {string} experimentName
    854      */
    855     enableForTest: function(experimentName)
    856     {
    857         this._checkExperiment(experimentName);
    858         this._enabledTransiently[experimentName] = true;
    859     },
    860 
    861     cleanUpStaleExperiments: function()
    862     {
    863         var experimentsSetting = Runtime._experimentsSetting();
    864         var cleanedUpExperimentSetting = {};
    865         for (var i = 0; i < this._experiments.length; ++i) {
    866             var experimentName = this._experiments[i].name;
    867             if (experimentsSetting[experimentName])
    868                 cleanedUpExperimentSetting[experimentName] = true;
    869         }
    870         this._setExperimentsSetting(cleanedUpExperimentSetting);
    871     },
    872 
    873     /**
    874      * @param {string} experimentName
    875      */
    876     _checkExperiment: function(experimentName)
    877     {
    878         console.assert(this._experimentNames[experimentName], "Unknown experiment " + experimentName);
    879     }
    880 }
    881 
    882 /**
    883  * @constructor
    884  * @param {!Runtime.ExperimentsSupport} experiments
    885  * @param {string} name
    886  * @param {string} title
    887  * @param {boolean} hidden
    888  */
    889 Runtime.Experiment = function(experiments, name, title, hidden)
    890 {
    891     this.name = name;
    892     this.title = title;
    893     this.hidden = hidden;
    894     this._experiments = experiments;
    895 }
    896 
    897 Runtime.Experiment.prototype = {
    898     /**
    899      * @return {boolean}
    900      */
    901     isEnabled: function()
    902     {
    903         return this._experiments.isEnabled(this.name);
    904     },
    905 
    906     /**
    907      * @param {boolean} enabled
    908      */
    909     setEnabled: function(enabled)
    910     {
    911         this._experiments.setEnabled(this.name, enabled);
    912     }
    913 }
    914 
    915 {(function parseQueryParameters()
    916 {
    917     var queryParams = location.search;
    918     if (!queryParams)
    919         return;
    920     var params = queryParams.substring(1).split("&");
    921     for (var i = 0; i < params.length; ++i) {
    922         var pair = params[i].split("=");
    923         Runtime._queryParamsObject[pair[0]] = pair[1];
    924     }
    925 
    926     // Patch settings from the URL param (for tests).
    927     var settingsParam = Runtime.queryParam("settings");
    928     if (settingsParam) {
    929         try {
    930             var settings = JSON.parse(window.decodeURI(settingsParam));
    931             for (var key in settings)
    932                 window.localStorage[key] = settings[key];
    933         } catch(e) {
    934             // Ignore malformed settings.
    935         }
    936     }
    937 })();}
    938 
    939 // This must be constructed after the query parameters have been parsed.
    940 Runtime.experiments = new Runtime.ExperimentsSupport();
    941 
    942 /** @type {!Runtime} */
    943 var runtime;
    944