Merge "Revert "resourceloader: Make cache-eval in mw.loader.work asynchronous""
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 23 Sep 2016 20:58:20 +0000 (20:58 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 23 Sep 2016 20:58:20 +0000 (20:58 +0000)
1  2 
resources/src/mediawiki/mediawiki.js

@@@ -11,7 -11,7 +11,7 @@@
  ( function ( $ ) {
        'use strict';
  
 -      var mw,
 +      var mw, StringSet, log,
                hasOwn = Object.prototype.hasOwnProperty,
                slice = Array.prototype.slice,
                trackCallbacks = $.Callbacks( 'memory' ),
                return hash;
        }
  
 +      StringSet = window.Set || ( function () {
 +              /**
 +               * @private
 +               * @class
 +               */
 +              function StringSet() {
 +                      this.set = {};
 +              }
 +              StringSet.prototype.add = function ( value ) {
 +                      this.set[ value ] = true;
 +              };
 +              StringSet.prototype.has = function ( value ) {
 +                      return this.set.hasOwnProperty( value );
 +              };
 +              return StringSet;
 +      }() );
 +
        /**
         * Create an object that can be read from or written to from methods that allow
         * interaction both with single and multiple properties at once.
                }
        };
  
 +      log = ( function () {
 +              // Also update the restoration of methods in mediawiki.log.js
 +              // when adding or removing methods here.
 +              var log = function () {},
 +                      console = window.console;
 +
 +              /**
 +               * @class mw.log
 +               * @singleton
 +               */
 +
 +              /**
 +               * Write a message to the console's warning channel.
 +               * Actions not supported by the browser console are silently ignored.
 +               *
 +               * @param {...string} msg Messages to output to console
 +               */
 +              log.warn = console && console.warn && Function.prototype.bind ?
 +                      Function.prototype.bind.call( console.warn, console ) :
 +                      $.noop;
 +
 +              /**
 +               * Write a message to the console's error channel.
 +               *
 +               * Most browsers provide a stacktrace by default if the argument
 +               * is a caught Error object.
 +               *
 +               * @since 1.26
 +               * @param {Error|...string} msg Messages to output to console
 +               */
 +              log.error = console && console.error && Function.prototype.bind ?
 +                      Function.prototype.bind.call( console.error, console ) :
 +                      $.noop;
 +
 +              /**
 +               * Create a property in a host object that, when accessed, will produce
 +               * a deprecation warning in the console.
 +               *
 +               * @param {Object} obj Host object of deprecated property
 +               * @param {string} key Name of property to create in `obj`
 +               * @param {Mixed} val The value this property should return when accessed
 +               * @param {string} [msg] Optional text to include in the deprecation message
 +               */
 +              log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
 +                      obj[ key ] = val;
 +              } : function ( obj, key, val, msg ) {
 +                      msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
 +                      var logged = new StringSet();
 +                      function uniqueTrace() {
 +                              var trace = new Error().stack;
 +                              if ( logged.has( trace ) ) {
 +                                      return false;
 +                              }
 +                              logged.add( trace );
 +                              return true;
 +                      }
 +                      Object.defineProperty( obj, key, {
 +                              configurable: true,
 +                              enumerable: true,
 +                              get: function () {
 +                                      if ( uniqueTrace() ) {
 +                                              mw.track( 'mw.deprecate', key );
 +                                              mw.log.warn( msg );
 +                                      }
 +                                      return val;
 +                              },
 +                              set: function ( newVal ) {
 +                                      if ( uniqueTrace() ) {
 +                                              mw.track( 'mw.deprecate', key );
 +                                              mw.log.warn( msg );
 +                                      }
 +                                      val = newVal;
 +                              }
 +                      } );
 +
 +              };
 +
 +              return log;
 +      }() );
 +
        /**
         * @class mw
         */
                },
  
                /**
 -               * Dummy placeholder for {@link mw.log}
 +               * No-op dummy placeholder for {@link mw.log} in debug mode.
                 *
                 * @method
                 */
 -              log: ( function () {
 -                      // Also update the restoration of methods in mediawiki.log.js
 -                      // when adding or removing methods here.
 -                      var log = function () {},
 -                              console = window.console;
 -
 -                      /**
 -                       * @class mw.log
 -                       * @singleton
 -                       */
 -
 -                      /**
 -                       * Write a message to the console's warning channel.
 -                       * Actions not supported by the browser console are silently ignored.
 -                       *
 -                       * @param {...string} msg Messages to output to console
 -                       */
 -                      log.warn = console && console.warn && Function.prototype.bind ?
 -                              Function.prototype.bind.call( console.warn, console ) :
 -                              $.noop;
 -
 -                      /**
 -                       * Write a message to the console's error channel.
 -                       *
 -                       * Most browsers provide a stacktrace by default if the argument
 -                       * is a caught Error object.
 -                       *
 -                       * @since 1.26
 -                       * @param {Error|...string} msg Messages to output to console
 -                       */
 -                      log.error = console && console.error && Function.prototype.bind ?
 -                              Function.prototype.bind.call( console.error, console ) :
 -                              $.noop;
 -
 -                      /**
 -                       * Create a property in a host object that, when accessed, will produce
 -                       * a deprecation warning in the console with backtrace.
 -                       *
 -                       * @param {Object} obj Host object of deprecated property
 -                       * @param {string} key Name of property to create in `obj`
 -                       * @param {Mixed} val The value this property should return when accessed
 -                       * @param {string} [msg] Optional text to include in the deprecation message
 -                       */
 -                      log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
 -                              obj[ key ] = val;
 -                      } : function ( obj, key, val, msg ) {
 -                              /*globals Set */
 -                              msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
 -                              var logged, loggedIsSet, uniqueTrace;
 -                              if ( window.Set ) {
 -                                      logged = new Set();
 -                                      loggedIsSet = true;
 -                              } else {
 -                                      logged = {};
 -                                      loggedIsSet = false;
 -                              }
 -                              uniqueTrace = function () {
 -                                      var trace = new Error().stack;
 -                                      if ( loggedIsSet ) {
 -                                              if ( logged.has( trace ) ) {
 -                                                      return false;
 -                                              }
 -                                              logged.add( trace );
 -                                              return true;
 -                                      } else {
 -                                              if ( logged.hasOwnProperty( trace ) ) {
 -                                                      return false;
 -                                              }
 -                                              logged[ trace ] = 1;
 -                                              return true;
 -                                      }
 -                              };
 -                              Object.defineProperty( obj, key, {
 -                                      configurable: true,
 -                                      enumerable: true,
 -                                      get: function () {
 -                                              if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', key );
 -                                                      mw.log.warn( msg );
 -                                              }
 -                                              return val;
 -                                      },
 -                                      set: function ( newVal ) {
 -                                              if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', key );
 -                                                      mw.log.warn( msg );
 -                                              }
 -                                              val = newVal;
 -                                      }
 -                              } );
 -
 -                      };
 -
 -                      return log;
 -              }() ),
 +              log: log,
  
                /**
                 * Client for ResourceLoader server end point.
                                cssBuffer = '',
                                cssBufferTimer = null,
                                cssCallbacks = $.Callbacks(),
 -                              isIE9 = document.documentMode === 9;
 +                              isIE9 = document.documentMode === 9,
 +                              rAF = window.requestAnimationFrame || setTimeout;
  
                        function getMarker() {
                                if ( !marker ) {
                                        if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
                                                // Linebreak for somewhat distinguishable sections
                                                cssBuffer += '\n' + cssText;
 -                                              // TODO: Using requestAnimationFrame would perform better by not injecting
 -                                              // styles while the browser is busy painting.
                                                if ( !cssBufferTimer ) {
 -                                                      cssBufferTimer = setTimeout( function () {
 +                                                      cssBufferTimer = rAF( function () {
 +                                                              // Wrap in anonymous function that takes no arguments
                                                                // Support: Firefox < 13
                                                                // Firefox 12 has non-standard behaviour of passing a number
                                                                // as first argument to a setTimeout callback.
                         *  dependencies, such that later modules depend on earlier modules. The array
                         *  contains the module names. If the array contains already some module names,
                         *  this function appends its result to the pre-existing array.
 -                       * @param {Object} [unresolved] Hash used to track the current dependency
 -                       *  chain; used to report loops in the dependency graph.
 +                       * @param {StringSet} [unresolved] Used to track the current dependency
 +                       *  chain, and to report loops in the dependency graph.
                         * @throws {Error} If any unregistered module or a dependency loop is encountered
                         */
                        function sortDependencies( module, resolved, unresolved ) {
                                }
                                // Create unresolved if not passed in
                                if ( !unresolved ) {
 -                                      unresolved = {};
 +                                      unresolved = new StringSet();
                                }
                                // Tracks down dependencies
                                deps = registry[ module ].dependencies;
                                for ( i = 0; i < deps.length; i++ ) {
                                        if ( $.inArray( deps[ i ], resolved ) === -1 ) {
 -                                              if ( unresolved[ deps[ i ] ] ) {
 +                                              if ( unresolved.has( deps[ i ] ) ) {
                                                        throw new Error( mw.format(
                                                                'Circular reference detected: $1 -> $2',
                                                                module,
                                                        ) );
                                                }
  
 -                                              // Add to unresolved
 -                                              unresolved[ module ] = true;
 +                                              unresolved.add(  module );
                                                sortDependencies( deps[ i ], resolved, unresolved );
                                        }
                                }
                                registry[ module ].state = 'executing';
  
                                runScript = function () {
 -                                      var script, markModuleReady, nestedAddScript, legacyWait,
 +                                      var script, markModuleReady, nestedAddScript, legacyWait, implicitDependencies,
                                                // Expand to include dependencies since we have to exclude both legacy modules
                                                // and their dependencies from the legacyWait (to prevent a circular dependency).
                                                legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
 -                                      try {
 -                                              script = registry[ module ].script;
 -                                              markModuleReady = function () {
 -                                                      registry[ module ].state = 'ready';
 -                                                      handlePending( module );
 -                                              };
 -                                              nestedAddScript = function ( arr, callback, i ) {
 -                                                      // Recursively call queueModuleScript() in its own callback
 -                                                      // for each element of arr.
 -                                                      if ( i >= arr.length ) {
 -                                                              // We're at the end of the array
 -                                                              callback();
 -                                                              return;
 -                                                      }
  
 -                                                      queueModuleScript( arr[ i ], module ).always( function () {
 -                                                              nestedAddScript( arr, callback, i + 1 );
 -                                                      } );
 -                                              };
 +                                      script = registry[ module ].script;
 +                                      markModuleReady = function () {
 +                                              registry[ module ].state = 'ready';
 +                                              handlePending( module );
 +                                      };
 +                                      nestedAddScript = function ( arr, callback, i ) {
 +                                              // Recursively call queueModuleScript() in its own callback
 +                                              // for each element of arr.
 +                                              if ( i >= arr.length ) {
 +                                                      // We're at the end of the array
 +                                                      callback();
 +                                                      return;
 +                                              }
 +
 +                                              queueModuleScript( arr[ i ], module ).always( function () {
 +                                                      nestedAddScript( arr, callback, i + 1 );
 +                                              } );
 +                                      };
 +
 +                                      implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 )
 +                                              ? []
 +                                              : legacyModules;
 +
 +                                      if ( module === 'user' ) {
 +                                              // Implicit dependency on the site module. Not real dependency because
 +                                              // it should run after 'site' regardless of whether it succeeds or fails.
 +                                              implicitDependencies.push( 'site' );
 +                                      }
  
 -                                              legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
 -                                                      ? $.Deferred().resolve()
 -                                                      : mw.loader.using( legacyModules );
 +                                      legacyWait = implicitDependencies.length
 +                                              ? mw.loader.using( implicitDependencies )
 +                                              : $.Deferred().resolve();
  
 -                                              legacyWait.always( function () {
 +                                      legacyWait.always( function () {
 +                                              try {
                                                        if ( $.isArray( script ) ) {
                                                                nestedAddScript( script, markModuleReady, 0 );
                                                        } else if ( typeof script === 'function' ) {
                                                                // Site and user modules are legacy scripts that run in the global scope.
                                                                // This is transported as a string instead of a function to avoid needing
                                                                // to use string manipulation to undo the function wrapper.
 -                                                              if ( module === 'user' ) {
 -                                                                      // Implicit dependency on the site module. Not real dependency because
 -                                                                      // it should run after 'site' regardless of whether it succeeds or fails.
 -                                                                      mw.loader.using( 'site' ).always( function () {
 -                                                                              $.globalEval( script );
 -                                                                              markModuleReady();
 -                                                                      } );
 -                                                              } else {
 -                                                                      $.globalEval( script );
 -                                                                      markModuleReady();
 -                                                              }
 +                                                              $.globalEval( script );
 +                                                              markModuleReady();
 +
                                                        } else {
                                                                // Module without script
                                                                markModuleReady();
                                                        }
 -                                              } );
 -                                      } catch ( e ) {
 -                                              // This needs to NOT use mw.log because these errors are common in production mode
 -                                              // and not in debug mode, such as when a symbol that should be global isn't exported
 -                                              registry[ module ].state = 'error';
 -                                              mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
 -                                              handlePending( module );
 -                                      }
 +                                              } catch ( e ) {
 +                                                      // Use mw.track instead of mw.log because these errors are common in production mode
 +                                                      // (e.g. undefined variable), and mw.log is only enabled in debug mode.
 +                                                      registry[ module ].state = 'error';
 +                                                      mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
 +                                                      handlePending( module );
 +                                              }
 +                                      } );
                                };
  
                                // Add localizations to message system
                                }
                        }
  
-                       /**
-                        * Evaluate a batch of load.php responses retrieved from mw.loader.store.
-                        *
-                        * @private
-                        * @param {string[]} implementations Array containing pieces of JavaScript code in the
-                        *  form of calls to mw.loader#implement().
-                        * @param {Function} cb Callback in case of failure
-                        * @param {Error} cb.err
-                        */
-                       function batchEval( implementations, cb ) {
-                               if ( !implementations.length ) {
-                                       return;
-                               }
-                               mw.requestIdleCallback( function iterate( deadline ) {
-                                       while ( implementations[ 0 ] && deadline.timeRemaining() > 5 ) {
-                                               try {
-                                                       $.globalEval( implementations.shift() );
-                                               } catch ( err ) {
-                                                       cb( err );
-                                                       return;
-                                               }
-                                       }
-                                       if ( implementations[ 0 ] ) {
-                                               mw.requestIdleCallback( iterate );
-                                       }
-                               } );
-                       }
                        /* Public Members */
                        return {
                                /**
                                 * @protected
                                 */
                                work: function () {
-                                       var q, batch, implementations, sourceModules;
+                                       var q, batch, concatSource, origBatch;
  
                                        batch = [];
  
  
                                        mw.loader.store.init();
                                        if ( mw.loader.store.enabled ) {
-                                               implementations = [];
-                                               sourceModules = [];
+                                               concatSource = [];
+                                               origBatch = batch;
                                                batch = $.grep( batch, function ( module ) {
-                                                       var implementation = mw.loader.store.get( module );
-                                                       if ( implementation ) {
-                                                               implementations.push( implementation );
-                                                               sourceModules.push( module );
+                                                       var source = mw.loader.store.get( module );
+                                                       if ( source ) {
+                                                               concatSource.push( source );
                                                                return false;
                                                        }
                                                        return true;
                                                } );
-                                               batchEval( implementations, function ( err ) {
+                                               try {
+                                                       $.globalEval( concatSource.join( ';' ) );
+                                               } catch ( err ) {
                                                        // Not good, the cached mw.loader.implement calls failed! This should
                                                        // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
                                                        // Depending on how corrupt the string is, it is likely that some
                                                        // modules' implement() succeeded while the ones after the error will
                                                        // never run and leave their modules in the 'loading' state forever.
                                                        // Since this is an error not caused by an individual module but by
                                                        // something that infected the implement call itself, don't take any
                                                        // risks and clear everything in this cache.
                                                        mw.loader.store.clear();
+                                                       // Re-add the ones still pending back to the batch and let the server
+                                                       // repopulate these modules to the cache.
+                                                       // This means that at most one module will be useless (the one that had
+                                                       // the error) instead of all of them.
                                                        mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
-                                                       // Re-add the failed ones that are still pending back to the batch
-                                                       var failed = $.grep( sourceModules, function ( module ) {
+                                                       origBatch = $.grep( origBatch, function ( module ) {
                                                                return registry[ module ].state === 'loading';
                                                        } );
-                                                       batchRequest( failed );
-                                               } );
+                                                       batch = batch.concat( origBatch );
+                                               }
                                        }
  
                                        batchRequest( batch );
                                 * response contain calls to this function.
                                 *
                                 * @param {string} module Name of module
 -                               * @param {Function|Array} [script] Function with module code or Array of URLs to
 -                               *  be used as the src attribute of a new `<script>` tag.
 +                               * @param {Function|Array|string} [script] Function with module code, list of URLs
 +                               *  to load via `<script src>`, or string of module code for `$.globalEval()`.
                                 * @param {Object} [style] Should follow one of the following patterns:
                                 *
                                 *     { "css": [css, ..] }
         * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
         *
         * @private
 -       * @method log_
         * @param {string} topic Stream name passed by mw.track
         * @param {Object} data Data passed by mw.track
         * @param {Error} [data.exception]
         * @param {string} data.source Error source
         * @param {string} [data.module] Name of module which caused the error
         */
 -      function log( topic, data ) {
 +      function logError( topic, data ) {
                var msg,
                        e = data.exception,
                        source = data.source,
        }
  
        // Subscribe to error streams
 -      mw.trackSubscribe( 'resourceloader.exception', log );
 -      mw.trackSubscribe( 'resourceloader.assert', log );
 +      mw.trackSubscribe( 'resourceloader.exception', logError );
 +      mw.trackSubscribe( 'resourceloader.assert', logError );
  
        /**
         * Fired when all modules associated with the page have finished loading.