resourceloader: Avoid duplicate existence check
[lhc/web/wiklou.git] / resources / src / startup / mediawiki.js
index f216808..e665403 100644 (file)
                         * @class
                         */
                        function StringSet() {
-                               this.set = {};
+                               this.set = Object.create( null );
                        }
                        StringSet.prototype.add = function ( value ) {
                                this.set[ value ] = true;
                        };
                        StringSet.prototype.has = function ( value ) {
-                               return hasOwn.call( this.set, value );
+                               return value in this.set;
                        };
                        return StringSet;
                }() );
         *  copied in one direction only. Changes to globals do not reflect in the map.
         */
        function Map( global ) {
-               this.values = {};
+               this.values = Object.create( null );
                if ( global === true ) {
                        // Override #set to also set the global variable
                        this.set = function ( selection, value ) {
                                results = {};
                                for ( i = 0; i < selection.length; i++ ) {
                                        if ( typeof selection[ i ] === 'string' ) {
-                                               results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ?
+                                               results[ selection[ i ] ] = selection[ i ] in this.values ?
                                                        this.values[ selection[ i ] ] :
                                                        fallback;
                                        }
                        }
 
                        if ( typeof selection === 'string' ) {
-                               return hasOwn.call( this.values, selection ) ?
+                               return selection in this.values ?
                                        this.values[ selection ] :
                                        fallback;
                        }
                        var i;
                        if ( Array.isArray( selection ) ) {
                                for ( i = 0; i < selection.length; i++ ) {
-                                       if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) {
+                                       if ( typeof selection[ i ] !== 'string' || !( selection[ i ] in this.values ) ) {
                                                return false;
                                        }
                                }
                                return true;
                        }
-                       return typeof selection === 'string' && hasOwn.call( this.values, selection );
+                       return typeof selection === 'string' && selection in this.values;
                }
        };
 
                                 */
                                marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ),
 
-                               // For addEmbeddedCSS()
-                               cssBuffer = '',
-                               cssBufferTimer = null,
-                               cssCallbacks = [],
+                               // For #addEmbeddedCSS()
+                               nextCssBuffer,
                                rAF = window.requestAnimationFrame || setTimeout;
 
                        /**
                                return el;
                        }
 
+                       /**
+                        * @private
+                        * @param {Object} cssBuffer
+                        */
+                       function flushCssBuffer( cssBuffer ) {
+                               var i;
+                               // Mark this object as inactive now so that further calls to addEmbeddedCSS() from
+                               // the callbacks go to a new buffer instead of this one (T105973)
+                               cssBuffer.active = false;
+                               newStyleTag( cssBuffer.cssText, marker );
+                               for ( i = 0; i < cssBuffer.callbacks.length; i++ ) {
+                                       cssBuffer.callbacks[ i ]();
+                               }
+                       }
+
                        /**
                         * Add a bit of CSS text to the current browser page.
                         *
-                        * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
-                        * or create a new one based on whether the given `cssText` is safe for extension.
+                        * The creation and insertion of the `<style>` element is debounced for two reasons:
+                        *
+                        * - Performing the insertion before the next paint round via requestAnimationFrame
+                        *   avoids forced or wasted style recomputations, which are expensive in browsers.
+                        * - Reduce how often new stylesheets are inserted by letting additional calls to this
+                        *   function accumulate into a buffer for at least one JavaScript tick. Modules are
+                        *   received from the server in batches, which means there is likely going to be many
+                        *   calls to this function in a row within the same tick / the same call stack.
+                        *   See also T47810.
                         *
                         * @private
-                        * @param {string} [cssText=cssBuffer] If called without cssText,
-                        *  the internal buffer will be inserted instead.
-                        * @param {Function} [callback]
+                        * @param {string} cssText CSS text to be added in a `<style>` tag.
+                        * @param {Function} callback Called after the insertion has occurred
                         */
                        function addEmbeddedCSS( cssText, callback ) {
-                               function fireCallbacks() {
-                                       var i,
-                                               oldCallbacks = cssCallbacks;
-                                       // Reset cssCallbacks variable so it's not polluted by any calls to
-                                       // addEmbeddedCSS() from one of the callbacks (T105973)
-                                       cssCallbacks = [];
-                                       for ( i = 0; i < oldCallbacks.length; i++ ) {
-                                               oldCallbacks[ i ]();
-                                       }
-                               }
-
-                               if ( callback ) {
-                                       cssCallbacks.push( callback );
+                               // Create a buffer if:
+                               // - We don't have one yet.
+                               // - The previous one is closed.
+                               // - The next CSS chunk syntactically needs to be at the start of a stylesheet (T37562).
+                               if ( !nextCssBuffer || nextCssBuffer.active === false || cssText.slice( 0, '@import'.length ) === '@import' ) {
+                                       nextCssBuffer = {
+                                               cssText: '',
+                                               callbacks: [],
+                                               active: null
+                                       };
                                }
 
-                               // Yield once before creating the <style> tag. This lets multiple stylesheets
-                               // accumulate into one buffer, allowing us to reduce how often new stylesheets
-                               // are inserted in the browser. Appending a stylesheet and waiting for the
-                               // browser to repaint is fairly expensive. (T47810)
-                               if ( cssText ) {
-                                       // Don't extend the buffer if the item needs its own stylesheet.
-                                       // Keywords like `@import` are only valid at the start of a stylesheet (T37562).
-                                       if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
-                                               // Linebreak for somewhat distinguishable sections
-                                               cssBuffer += '\n' + cssText;
-                                               if ( !cssBufferTimer ) {
-                                                       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.
-                                                               // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
-                                                               addEmbeddedCSS();
-                                                       } );
-                                               }
-                                               return;
-                                       }
+                               // Linebreak for somewhat distinguishable sections
+                               nextCssBuffer.cssText += '\n' + cssText;
+                               nextCssBuffer.callbacks.push( callback );
 
-                               // This is a scheduled flush for the buffer
-                               } else {
-                                       cssBufferTimer = null;
-                                       cssText = cssBuffer;
-                                       cssBuffer = '';
+                               if ( nextCssBuffer.active === null ) {
+                                       nextCssBuffer.active = true;
+                                       // The flushCssBuffer callback has its parameter bound by reference, which means
+                                       // 1) We can still extend the buffer from our object reference after this point.
+                                       // 2) We can safely re-assign the variable (not the object) to start a new buffer.
+                                       rAF( flushCssBuffer.bind( null, nextCssBuffer ) );
                                }
-
-                               newStyleTag( cssText, marker );
-
-                               fireCallbacks();
                        }
 
                        /**
                                        if ( ready !== undefined ) {
                                                ready();
                                        }
-
                                        return;
                                }
 
                                                        dependencies
                                                );
                                        }
-
                                        return;
                                }
 
                                        jobs.push( {
                                                // Narrow down the list to modules that are worth waiting for
                                                dependencies: dependencies.filter( function ( module ) {
-                                                       var state = mw.loader.getState( module );
+                                                       var state = registry[ module ].state;
                                                        return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
                                                } ),
                                                ready: ready,
                                }
 
                                dependencies.forEach( function ( module ) {
-                                       var state = mw.loader.getState( module );
                                        // Only queue modules that are still in the initial 'registered' state
                                        // (not ones already loading, ready or error).
-                                       if ( state === 'registered' && queue.indexOf( module ) === -1 ) {
+                                       if ( registry[ module ].state === 'registered' && queue.indexOf( module ) === -1 ) {
                                                // Private modules must be embedded in the page. Don't bother queuing
                                                // these as the server will deny them anyway (T101806).
                                                if ( registry[ module ].group === 'private' ) {
                         * @param {string} module Module name to execute
                         */
                        function execute( module ) {
-                               var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
-                                       cssHandlesRegistered = false;
+                               var key, value, media, i, urls, cssHandle, siteDeps, siteDepErr, runScript,
+                                       cssPending = 0;
 
                                if ( !hasOwn.call( registry, module ) ) {
                                        throw new Error( 'Module has not been registered yet: ' + module );
                                        mw.templates.set( module, registry[ module ].templates );
                                }
 
-                               // Make sure we don't run the scripts until all stylesheet insertions have completed.
-                               ( function () {
-                                       var pending = 0;
-                                       checkCssHandles = function () {
-                                               var ex, dependencies;
-                                               // cssHandlesRegistered ensures we don't take off too soon, e.g. when
-                                               // one of the cssHandles is fired while we're still creating more handles.
-                                               if ( cssHandlesRegistered && pending === 0 && runScript ) {
-                                                       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.
-                                                               // Note: This is a simplified version of mw.loader.using(), inlined here
-                                                               // as using() depends on jQuery (T192623).
-                                                               try {
-                                                                       dependencies = resolve( [ 'site' ] );
-                                                               } catch ( e ) {
-                                                                       ex = e;
-                                                                       runScript();
-                                                               }
-                                                               if ( ex === undefined ) {
-                                                                       enqueue( dependencies, runScript, runScript );
-                                                               }
-                                                       } else {
-                                                               runScript();
-                                                       }
-                                                       runScript = undefined; // Revoke
+                               // Adding of stylesheets is asynchronous via addEmbeddedCSS().
+                               // The below function uses a counting semaphore to make sure we don't call
+                               // runScript() until after this module's stylesheets have been inserted
+                               // into the DOM.
+                               cssHandle = function () {
+                                       // Increase semaphore, when creating a callback for addEmbeddedCSS.
+                                       cssPending++;
+                                       return function () {
+                                               var runScriptCopy;
+                                               // Decrease semaphore, when said callback is invoked.
+                                               cssPending--;
+                                               if ( cssPending === 0 ) {
+                                                       // Paranoia:
+                                                       // This callback is exposed to addEmbeddedCSS, which is outside the execute()
+                                                       // function and is not concerned with state-machine integrity. In turn,
+                                                       // addEmbeddedCSS() actually exposes stuff further into the browser (rAF).
+                                                       // If increment and decrement callbacks happen in the wrong order, or start
+                                                       // again afterwards, then this branch could be reached multiple times.
+                                                       // To protect the integrity of the state-machine, prevent that from happening
+                                                       // by making runScript() cannot be called more than once.  We store a private
+                                                       // reference when we first reach this branch, then deference the original, and
+                                                       // call our reference to it.
+                                                       runScriptCopy = runScript;
+                                                       runScript = undefined;
+                                                       runScriptCopy();
                                                }
                                        };
-                                       cssHandle = function () {
-                                               var check = checkCssHandles;
-                                               pending++;
-                                               return function () {
-                                                       if ( check ) {
-                                                               pending--;
-                                                               check();
-                                                               check = undefined; // Revoke
-                                                       }
-                                               };
-                                       };
-                               }() );
+                               };
 
                                // Process styles (see also mw.loader.implement)
                                // * back-compat: { <media>: css }
                                        }
                                }
 
-                               // End profiling of execute()-self before we call checkCssHandles(),
-                               // which (sometimes asynchronously) calls runScript(), which we want
-                               // to measure separately without overlap.
+                               // End profiling of execute()-self before we call runScript(),
+                               // which we want to measure separately without overlap.
                                $CODE.profileExecuteEnd();
 
-                               // Kick off.
-                               cssHandlesRegistered = true;
-                               checkCssHandles();
+                               if ( module === 'user' ) {
+                                       // Implicit dependency on the site module. Not a real dependency because it should
+                                       // run after 'site' regardless of whether it succeeds or fails.
+                                       // Note: This is a simplified version of mw.loader.using(), inlined here because
+                                       // mw.loader.using() is part of mediawiki.base (depends on jQuery; T192623).
+                                       try {
+                                               siteDeps = resolve( [ 'site' ] );
+                                       } catch ( e ) {
+                                               siteDepErr = e;
+                                               runScript();
+                                       }
+                                       if ( siteDepErr === undefined ) {
+                                               enqueue( siteDeps, runScript, runScript );
+                                       }
+                               } else if ( cssPending === 0 ) {
+                                       // Regular module without styles
+                                       runScript();
+                               }
+                               // else: runScript will get called via cssHandle()
                        }
 
                        function sortQuery( o ) {
                                                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 ) {
                                        init: function () {
                                                var raw, data;
 
-                                               if ( mw.loader.store.enabled !== null ) {
+                                               if ( this.enabled !== null ) {
                                                        // Init already ran
                                                        return;
                                                }
                                                        !mw.config.get( 'wgResourceLoaderStorageEnabled' )
                                                ) {
                                                        // Clear any previous store to free up space. (T66721)
-                                                       mw.loader.store.clear();
-                                                       mw.loader.store.enabled = false;
+                                                       this.clear();
+                                                       this.enabled = false;
                                                        return;
                                                }
                                                if ( mw.config.get( 'debug' ) ) {
                                                        // Disable module store in debug mode
-                                                       mw.loader.store.enabled = false;
+                                                       this.enabled = false;
                                                        return;
                                                }
 
                                                try {
                                                        // This a string we stored, or `null` if the key does not (yet) exist.
-                                                       raw = localStorage.getItem( mw.loader.store.getStoreKey() );
+                                                       raw = localStorage.getItem( this.getStoreKey() );
                                                        // If we get here, localStorage is available; mark enabled
-                                                       mw.loader.store.enabled = true;
+                                                       this.enabled = true;
                                                        // If null, JSON.parse() will cast to string and re-parse, still null.
                                                        data = JSON.parse( raw );
-                                                       if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
-                                                               mw.loader.store.items = data.items;
+                                                       if ( data && typeof data.items === 'object' && data.vary === this.getVary() ) {
+                                                               this.items = data.items;
                                                                return;
                                                        }
                                                } catch ( e ) {
                                                //    We will disable the store below.
                                                if ( raw === undefined ) {
                                                        // localStorage failed; disable store
-                                                       mw.loader.store.enabled = false;
+                                                       this.enabled = false;
                                                }
                                        },
 
                                        get: function ( module ) {
                                                var key;
 
-                                               if ( !mw.loader.store.enabled ) {
+                                               if ( !this.enabled ) {
                                                        return false;
                                                }
 
                                                key = getModuleKey( module );
-                                               if ( key in mw.loader.store.items ) {
-                                                       mw.loader.store.stats.hits++;
-                                                       return mw.loader.store.items[ key ];
+                                               if ( key in this.items ) {
+                                                       this.stats.hits++;
+                                                       return this.items[ key ];
                                                }
-                                               mw.loader.store.stats.misses++;
+                                               this.stats.misses++;
                                                return false;
                                        },
 
                                        set: function ( module, descriptor ) {
                                                var args, key, src;
 
-                                               if ( !mw.loader.store.enabled ) {
+                                               if ( !this.enabled ) {
                                                        return;
                                                }
 
 
                                                if (
                                                        // Already stored a copy of this exact version
-                                                       key in mw.loader.store.items ||
+                                                       key in this.items ||
                                                        // Module failed to load
                                                        descriptor.state !== 'ready' ||
                                                        // Unversioned, private, or site-/user-specific
                                                                JSON.stringify( descriptor.messages ),
                                                                JSON.stringify( descriptor.templates )
                                                        ];
-                                                       // Attempted workaround for a possible Opera bug (bug T59567).
-                                                       // This regex should never match under sane conditions.
-                                                       if ( /^\s*\(/.test( args[ 1 ] ) ) {
-                                                               args[ 1 ] = 'function' + args[ 1 ];
-                                                               mw.trackError( 'resourceloader.assert', { source: 'bug-T59567' } );
-                                                       }
                                                } catch ( e ) {
                                                        mw.trackError( 'resourceloader.exception', {
                                                                exception: e,
                                                }
 
                                                src = 'mw.loader.implement(' + args.join( ',' ) + ');';
-                                               if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
+                                               if ( src.length > this.MODULE_SIZE_MAX ) {
                                                        return;
                                                }
-                                               mw.loader.store.items[ key ] = src;
-                                               mw.loader.store.update();
+                                               this.items[ key ] = src;
+                                               this.update();
                                        },
 
                                        /**
                                        prune: function () {
                                                var key, module;
 
-                                               for ( key in mw.loader.store.items ) {
+                                               for ( key in this.items ) {
                                                        module = key.slice( 0, key.indexOf( '@' ) );
                                                        if ( getModuleKey( module ) !== key ) {
-                                                               mw.loader.store.stats.expired++;
-                                                               delete mw.loader.store.items[ key ];
-                                                       } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
+                                                               this.stats.expired++;
+                                                               delete this.items[ key ];
+                                                       } else if ( this.items[ key ].length > this.MODULE_SIZE_MAX ) {
                                                                // This value predates the enforcement of a size limit on cached modules.
-                                                               delete mw.loader.store.items[ key ];
+                                                               delete this.items[ key ];
                                                        }
                                                }
                                        },
                                         * Clear the entire module store right now.
                                         */
                                        clear: function () {
-                                               mw.loader.store.items = {};
+                                               this.items = {};
                                                try {
-                                                       localStorage.removeItem( mw.loader.store.getStoreKey() );
+                                                       localStorage.removeItem( this.getStoreKey() );
                                                } catch ( e ) {}
                                        },