Merge "Add part to update ctd_user_defined in populateChangeTagDef"
[lhc/web/wiklou.git] / resources / src / startup / mediawiki.js
index f927aad..ba8869b 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * Base library for MediaWiki.
  *
- * Exposed globally as `mediaWiki` with `mw` as shortcut.
+ * Exposed globally as `mw`, with `mediaWiki` as alias.
  *
  * @class mw
  * @alternateClassName mediaWiki
                log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
                        obj[ key ] = val;
                } : function ( obj, key, val, msg, logName ) {
-                       var logged = new StringSet();
-                       logName = logName || key;
-                       msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
-                       function uniqueTrace() {
-                               var trace = new Error().stack;
-                               if ( logged.has( trace ) ) {
-                                       return false;
+                       var stacks;
+                       function maybeLog() {
+                               var name,
+                                       trace = new Error().stack;
+                               if ( !stacks ) {
+                                       stacks = new StringSet();
+                               }
+                               if ( !stacks.has( trace ) ) {
+                                       stacks.add( trace );
+                                       name = logName || key;
+                                       mw.track( 'mw.deprecate', name );
+                                       mw.log.warn(
+                                               'Use of "' + name + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' )
+                                       );
                                }
-                               logged.add( trace );
-                               return true;
                        }
                        // Support: Safari 5.0
                        // Throws "not supported on DOM Objects" for Node or Element objects (incl. document)
                                        configurable: true,
                                        enumerable: true,
                                        get: function () {
-                                               if ( uniqueTrace() ) {
-                                                       mw.track( 'mw.deprecate', logName );
-                                                       mw.log.warn( msg );
-                                               }
+                                               maybeLog();
                                                return val;
                                        },
                                        set: function ( newVal ) {
-                                               if ( uniqueTrace() ) {
-                                                       mw.track( 'mw.deprecate', logName );
-                                                       mw.log.warn( msg );
-                                               }
+                                               maybeLog();
                                                val = newVal;
                                        }
                                } );
                /**
                 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
                 *
-                * On browsers that implement the Navigation Timing API, this function will produce floating-point
-                * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
-                * it will fall back to using `Date`.
+                * On browsers that implement the Navigation Timing API, this function will produce
+                * floating-point values with microsecond precision that are guaranteed to be monotonic.
+                * On all other browsers, it will fall back to using `Date`.
                 *
                 * @return {number} Current time
                 */
-               now: ( function () {
+               now: function () {
+                       // Optimisation: Define the shortcut on first call, not at module definition.
                        var perf = window.performance,
                                navStart = perf && perf.timing && perf.timing.navigationStart;
-                       return navStart && typeof perf.now === 'function' ?
+
+                       // Define the relevant shortcut
+                       mw.now = navStart && typeof perf.now === 'function' ?
                                function () { return navStart + perf.now(); } :
-                               function () { return Date.now(); };
-               }() ),
+                               Date.now;
+
+                       return mw.now();
+               },
 
                /**
                 * List of all analytic events emitted so far.
                         *   - resolve: failed to sort dependencies for a module in mw.loader.load
                         *   - store-eval: could not evaluate module code cached in localStorage
                         *   - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
-                        *   - store-localstorage-json: JSON conversion error in mw.loader.store.set
-                        *   - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
+                        *   - store-localstorage-json: JSON conversion error in mw.loader.store
+                        *   - store-localstorage-update: localStorage conversion error in mw.loader.store.
                         */
 
                        /**
 
                                // The current module became 'ready'.
                                if ( registry[ module ].state === 'ready' ) {
-                                       // Save it to the module store.
-                                       mw.loader.store.set( module, registry[ module ] );
+                                       // Queue it for later syncing to the module store.
+                                       mw.loader.store.add( module );
                                        // Recursively execute all dependent modules that were already loaded
                                        // (waiting for execution) and no longer have unsatisfied dependencies.
                                        for ( m in registry ) {
                        /**
                         * Resolve indexed dependencies.
                         *
-                        * ResourceLoader uses an optimization to save space which replaces module names in
+                        * ResourceLoader uses an optimisation to save space which replaces module names in
                         * dependency lists with the index of that module within the array of module
                         * registration data if it exists. The benefit is a significant reduction in the data
                         * size of the startup module. This function changes those dependency lists back to
                                };
                        }
 
+                       /**
+                        * @private
+                        * @param {string} module
+                        * @param {string|number} [version]
+                        * @param {string[]} [dependencies]
+                        * @param {string} [group]
+                        * @param {string} [source]
+                        * @param {string} [skip]
+                        */
+                       function registerOne( module, version, dependencies, group, source, skip ) {
+                               if ( hasOwn.call( registry, module ) ) {
+                                       throw new Error( 'module already registered: ' + module );
+                               }
+                               registry[ module ] = {
+                                       // Exposed to execute() for mw.loader.implement() closures.
+                                       // Import happens via require().
+                                       module: {
+                                               exports: {}
+                                       },
+                                       version: String( version || '' ),
+                                       dependencies: dependencies || [],
+                                       group: typeof group === 'string' ? group : null,
+                                       source: typeof source === 'string' ? source : 'local',
+                                       state: 'registered',
+                                       skip: typeof skip === 'string' ? skip : null
+                               };
+                       }
+
                        /* Public Members */
                        return {
                                /**
                                /**
                                 * Start loading of all queued module dependencies.
                                 *
-                                * @protected
+                                * @private
                                 */
                                work: function () {
                                        var q, batch, implementations, sourceModules;
                                 *
                                 * The #work() method will use this information to split up requests by source.
                                 *
-                                *     mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
+                                *     mw.loader.addSource( { mediawikiwiki: 'https://www.mediawiki.org/w/load.php' } );
                                 *
-                                * @param {string|Object} id Source ID, or object mapping ids to load urls
-                                * @param {string} loadUrl Url to a load.php end point
+                                * @private
+                                * @param {Object} ids An object mapping ids to load.php end point urls
                                 * @throws {Error} If source id is already registered
                                 */
-                               addSource: function ( id, loadUrl ) {
-                                       var source;
-                                       // Allow multiple additions
-                                       if ( typeof id === 'object' ) {
-                                               for ( source in id ) {
-                                                       mw.loader.addSource( source, id[ source ] );
+                               addSource: function ( ids ) {
+                                       var id;
+                                       for ( id in ids ) {
+                                               if ( hasOwn.call( sources, id ) ) {
+                                                       throw new Error( 'source already registered: ' + id );
                                                }
-                                               return;
+                                               sources[ id ] = ids[ id ];
                                        }
-
-                                       if ( hasOwn.call( sources, id ) ) {
-                                               throw new Error( 'source already registered: ' + id );
-                                       }
-
-                                       sources[ id ] = loadUrl;
                                },
 
                                /**
                                 * Register a module, letting the system know about it and its properties.
                                 *
-                                * The startup modules contain calls to this method.
+                                * The startup module calls this method.
                                 *
                                 * When using multiple module registration by passing an array, dependencies that
                                 * are specified as references to modules within the array will be resolved before
                                 * the modules are registered.
                                 *
-                                * @param {string|Array} module Module name or array of arrays, each containing
+                                * @param {string|Array} modules Module name or array of arrays, each containing
                                 *  a list of arguments compatible with this method
-                                * @param {string|number} version Module version hash (falls backs to empty string)
+                                * @param {string|number} [version] Module version hash (falls backs to empty string)
                                 *  Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
                                 * @param {string[]} [dependencies] Array of module names on which this module depends.
                                 * @param {string} [group=null] Group which the module is in
                                 * @param {string} [source='local'] Name of the source
                                 * @param {string} [skip=null] Script body of the skip function
                                 */
-                               register: function ( module, version, dependencies, group, source, skip ) {
+                               register: function ( modules ) {
                                        var i;
-                                       // Allow multiple registration
-                                       if ( typeof module === 'object' ) {
-                                               resolveIndexedDependencies( module );
-                                               for ( i = 0; i < module.length; i++ ) {
-                                                       // module is an array of module names
-                                                       if ( typeof module[ i ] === 'string' ) {
-                                                               mw.loader.register( module[ i ] );
-                                                       // module is an array of arrays
-                                                       } else if ( typeof module[ i ] === 'object' ) {
-                                                               mw.loader.register.apply( mw.loader, module[ i ] );
-                                                       }
+                                       if ( typeof modules === 'object' ) {
+                                               resolveIndexedDependencies( modules );
+                                               // Optimisation: Up to 55% faster.
+                                               // Typically called only once, and with a batch.
+                                               // See <https://gist.github.com/Krinkle/f06fdb3de62824c6c16f02a0e6ce0e66>
+                                               // Benchmarks taught us that the code for adding an object to `registry`
+                                               // should actually be inline, or in a simple function that does no
+                                               // arguments manipulation, and isn't also the caller itself.
+                                               // JS semantics make it hard to optimise recursion to a different
+                                               // signature of itself.
+                                               for ( i = 0; i < modules.length; i++ ) {
+                                                       registerOne.apply( null, modules[ i ] );
                                                }
-                                               return;
-                                       }
-                                       if ( hasOwn.call( registry, module ) ) {
-                                               throw new Error( 'module already registered: ' + module );
+                                       } else {
+                                               registerOne.apply( null, arguments );
                                        }
-                                       // List the module as registered
-                                       registry[ module ] = {
-                                               // Exposed to execute() for mw.loader.implement() closures.
-                                               // Import happens via require().
-                                               module: {
-                                                       exports: {}
-                                               },
-                                               version: String( version || '' ),
-                                               dependencies: dependencies || [],
-                                               group: typeof group === 'string' ? group : null,
-                                               source: typeof source === 'string' ? source : 'local',
-                                               state: 'registered',
-                                               skip: typeof skip === 'string' ? skip : null
-                                       };
                                },
 
                                /**
                                 * The reason css strings are not concatenated anymore is T33676. We now check
                                 * whether it's safe to extend the stylesheet.
                                 *
-                                * @protected
+                                * @private
                                 * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
                                 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
                                 */
                                                mw.loader.register( name );
                                        }
                                        // Check for duplicate implementation
-                                       if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
+                                       if ( registry[ name ].script !== undefined ) {
                                                throw new Error( 'module already implemented: ' + name );
                                        }
                                        if ( version ) {
                                        // to module implementations.
                                        items: {},
 
+                                       // Names of modules to be stored during the next update.
+                                       // See add() and update().
+                                       queue: [],
+
                                        // Cache hit stats
                                        stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
 
                                                        // Disabled because localStorage quotas are tight and (in Firefox's case)
                                                        // shared by multiple origins.
                                                        // See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
-                                                       /Firefox|Opera/.test( navigator.userAgent ) ||
+                                                       /Firefox/.test( navigator.userAgent ) ||
 
                                                        // Disabled by configuration.
                                                        !mw.config.get( 'wgResourceLoaderStorageEnabled' )
                                        },
 
                                        /**
-                                        * Stringify a module and queue it for storage.
+                                        * Queue the name of a module that the next update should consider storing.
                                         *
+                                        * @since 1.32
                                         * @param {string} module Module name
-                                        * @param {Object} descriptor The module's descriptor as set in the registry
                                         */
-                                       set: function ( module, descriptor ) {
-                                               var args, key, src;
-
+                                       add: function ( module ) {
                                                if ( !this.enabled ) {
                                                        return;
                                                }
+                                               this.queue.push( module );
+                                               this.requestUpdate();
+                                       },
+
+                                       /**
+                                        * Add the contents of the named module to the in-memory store.
+                                        *
+                                        * This method does not guarantee that the module will be stored.
+                                        * Inspection of the module's meta data and size will ultimately decide that.
+                                        *
+                                        * This method is considered internal to mw.loader.store and must only
+                                        * be called if the store is enabled.
+                                        *
+                                        * @private
+                                        * @param {string} module Module name
+                                        */
+                                       set: function ( module ) {
+                                               var key, args, src,
+                                                       descriptor = mw.loader.moduleRegistry[ module ];
 
                                                key = getModuleKey( module );
 
                                                        // Already stored a copy of this exact version
                                                        key in this.items ||
                                                        // Module failed to load
+                                                       !descriptor ||
                                                        descriptor.state !== 'ready' ||
                                                        // Unversioned, private, or site-/user-specific
                                                        !descriptor.version ||
                                                        return;
                                                }
                                                this.items[ key ] = src;
-                                               this.update();
                                        },
 
                                        /**
                                        },
 
                                        /**
-                                        * Sync in-memory store back to localStorage.
+                                        * Request a sync of the in-memory store back to persisted localStorage.
+                                        *
+                                        * This function debounces updates. The debouncing logic should account
+                                        * for the following factors:
                                         *
-                                        * This function debounces updates. When called with a flush already pending, the
-                                        * scheduled flush is postponed. The call to localStorage.setItem will be keep
-                                        * being deferred until the page is quiescent for 2 seconds.
+                                        * - Writing to localStorage is an expensive operation that must not happen
+                                        *   during the critical path of initialising and executing module code.
+                                        *   Instead, it should happen at a later time after modules have been given
+                                        *   time and priority to do their thing first.
                                         *
-                                        * Because localStorage is shared by all pages from the same origin, if multiple
-                                        * pages are loaded with different module sets, the possibility exists that
-                                        * modules saved by one page will be clobbered by another. The only impact of this
-                                        * is minor (merely a less efficient cache use) and the problem would be corrected
-                                        * by subsequent page views.
+                                        * - This method is called from mw.loader.store.add(), which will be called
+                                        *   hundreds of times on a typical page, including within the same call-stack
+                                        *   and eventloop-tick. This is because responses from load.php happen in
+                                        *   batches. As such, we want to allow all modules from the same load.php
+                                        *   response to be written to disk with a single flush, not many.
                                         *
+                                        * - Repeatedly deleting and creating timers is non-trivial.
+                                        *
+                                        * - localStorage is shared by all pages from the same origin, if multiple
+                                        *   pages are loaded with different module sets, the possibility exists that
+                                        *   modules saved by one page will be clobbered by another. The impact of
+                                        *   this is minor, it merely causes a less efficient cache use, and the
+                                        *   problem would be corrected by subsequent page views.
+                                        *
+                                        * This method is considered internal to mw.loader.store and must only
+                                        * be called if the store is enabled.
+                                        *
+                                        * @private
                                         * @method
                                         */
-                                       update: ( function () {
-                                               var timer, hasPendingWrites = false;
+                                       requestUpdate: ( function () {
+                                               var hasPendingWrites = false;
 
                                                function flushWrites() {
                                                        var data, key;
-                                                       if ( !mw.loader.store.enabled ) {
-                                                               return;
-                                                       }
 
+                                                       // Remove anything from the in-memory store that came from previous page
+                                                       // loads that no longer corresponds with current module names and versions.
                                                        mw.loader.store.prune();
+                                                       // Process queued module names, serialise their contents to the in-memory store.
+                                                       while ( mw.loader.store.queue.length ) {
+                                                               mw.loader.store.set( mw.loader.store.queue.shift() );
+                                                       }
+
                                                        key = mw.loader.store.getStoreKey();
                                                        try {
                                                                // Replacing the content of the module store might fail if the new
                                                                } );
                                                        }
 
+                                                       // Let the next call to requestUpdate() create a new timer.
                                                        hasPendingWrites = false;
                                                }
 
-                                               function request() {
-                                                       // If another mw.loader.store.set()/update() call happens in the narrow
-                                                       // time window between requestIdleCallback() and flushWrites firing, ignore it.
-                                                       // It'll be saved by the already-scheduled flush.
-                                                       if ( !hasPendingWrites ) {
-                                                               hasPendingWrites = true;
-                                                               mw.requestIdleCallback( flushWrites );
-                                                       }
+                                               function onTimeout() {
+                                                       // Defer the actual write via requestIdleCallback
+                                                       mw.requestIdleCallback( flushWrites );
                                                }
 
                                                return function () {
-                                                       // Cancel the previous timer (if it hasn't fired yet)
-                                                       clearTimeout( timer );
-                                                       timer = setTimeout( request, 2000 );
+                                                       // On the first call to requestUpdate(), create a timer that
+                                                       // waits at least two seconds, then calls onTimeout.
+                                                       // The main purpose is to allow the current batch of load.php
+                                                       // responses to complete before we do anything. This batch can
+                                                       // trigger many hundreds of calls to requestUpdate().
+                                                       if ( !hasPendingWrites ) {
+                                                               hasPendingWrites = true;
+                                                               setTimeout( onTimeout, 2000 );
+                                                       }
                                                };
                                        }() )
                                }