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