• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
32var allDescriptors = [];
33var applicationDescriptor;
34var _loadedScripts = {};
35
36/**
37 * @param {string} url
38 * @return {string}
39 */
40function 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 */
60function 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 */
90function 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 */
120var 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 */
152Runtime._queryParamsObject = { __proto__: null };
153
154/**
155 * @return {boolean}
156 */
157Runtime.isReleaseMode = function()
158{
159    return !!allDescriptors.length;
160}
161
162/**
163 * @param {string} moduleName
164 * @param {string} workerName
165 * @return {!SharedWorker}
166 */
167Runtime.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 */
185Runtime.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 */
233Runtime.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 */
291Runtime.queryParam = function(name)
292{
293    return Runtime._queryParamsObject[name] || null;
294}
295
296/**
297 * @return {!Object}
298 */
299Runtime._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 */
312Runtime._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 */
328Runtime._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
336Runtime.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 */
585Runtime.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 */
611Runtime.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 */
634Runtime.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
645Runtime.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 */
690Runtime.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
704Runtime.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 */
764Runtime.ExperimentsSupport = function()
765{
766    this._supportEnabled = Runtime.queryParam("experiments") !== null;
767    this._experiments = [];
768    this._experimentNames = {};
769    this._enabledTransiently = {};
770}
771
772Runtime.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 */
889Runtime.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
897Runtime.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.
940Runtime.experiments = new Runtime.ExperimentsSupport();
941
942/** @type {!Runtime} */
943var runtime;
944