X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=resources%2Fsrc%2Fstartup%2Fmediawiki.js;h=97fc02783a7f1dc52397d4dac3cda66d00a18789;hp=3082603ea554d35ba21c7c358219d09ff4c4a732;hb=fc2a88f66642590dda149590bdb72c5fc9ab5795;hpb=92e3fd2c8eb43e587195490546a5d1b8b6b2ba68 diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index 3082603ea5..97fc02783a 100644 --- a/resources/src/startup/mediawiki.js +++ b/resources/src/startup/mediawiki.js @@ -13,7 +13,6 @@ 'use strict'; var mw, StringSet, log, - hasOwn = Object.prototype.hasOwnProperty, trackQueue = []; /** @@ -259,7 +258,6 @@ defineFallbacks(); - /* eslint-disable no-console */ log = ( function () { /** * Write a verbose message to the browser's console in debug mode. @@ -293,7 +291,7 @@ * * @param {...string} msg Messages to output to console */ - log.warn = console && console.warn && Function.prototype.bind ? + log.warn = console && console.warn ? Function.prototype.bind.call( console.warn, console ) : function () {}; @@ -308,7 +306,7 @@ * @since 1.26 * @param {...Mixed} msg Messages to output to console */ - log.error = console && console.error && Function.prototype.bind ? + log.error = console && console.error ? Function.prototype.bind.call( console.error, console ) : function () {}; @@ -323,19 +321,22 @@ * @param {string} [logName=key] Optional custom name for the feature. * This is used instead of `key` in the message and `mw.deprecate` tracking. */ - 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; + log.deprecate = function ( obj, key, val, msg, logName ) { + 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) @@ -345,17 +346,11 @@ 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; } } ); @@ -366,7 +361,6 @@ return log; }() ); - /* eslint-enable no-console */ /** * @class mw @@ -382,19 +376,24 @@ /** * 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. @@ -511,8 +510,8 @@ * - 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. */ /** @@ -565,8 +564,8 @@ * The contents are then stashed in the registry via mw.loader#implement. * - `loaded`: * The module has been loaded from the server and stashed via mw.loader#implement. - * If the module has no more dependencies in-flight, the module will be executed - * immediately. Otherwise execution is deferred, controlled via #handlePending. + * Once the module has no more dependencies in-flight, the module will be executed, + * controlled via #requestPropagation and #doPropagation. * - `executing`: * The module is being executed. * - `ready`: @@ -580,7 +579,7 @@ * @property * @private */ - var registry = {}, + var registry = Object.create( null ), // Mapping of sources, keyed by source-id, values are strings. // // Format: @@ -589,7 +588,7 @@ // 'sourceId': 'http://example.org/w/load.php' // } // - sources = {}, + sources = Object.create( null ), // For queueModuleScript() handlingPendingRequests = false, @@ -601,8 +600,7 @@ /** * List of callback jobs waiting for modules to be ready. * - * Jobs are created by #enqueue() and run by #handlePending(). - * + * Jobs are created by #enqueue() and run by #doPropagation(). * Typically when a job is created for a module, the job's dependencies contain * both the required module and all its recursive dependencies. * @@ -619,6 +617,10 @@ */ jobs = [], + // For #requestPropagation() and #doPropagation() + willPropagate = false, + errorModules = [], + /** * @private * @property {Array} baseModules @@ -777,86 +779,133 @@ } /** - * A module has entered state 'ready', 'error', or 'missing'. Automatically update - * pending jobs and modules that depend upon this module. If the given module failed, - * propagate the 'error' state up the dependency tree. Otherwise, go ahead and execute - * all jobs/modules now having their dependencies satisfied. + * Handle propagation of module state changes and reactions to them. * - * Jobs that depend on a failed module, will have their error callback ran (if any). + * - When a module reaches a failure state, this should be propagated to + * modules that depend on the failed module. + * - When a module reaches a final state, pending job callbacks for the + * module from mw.loader.using() should be called. + * - When a module reaches the 'ready' state from #execute(), consider + * executing dependant modules now having their dependencies satisfied. + * - When a module reaches the 'loaded' state from mw.loader.implement, + * consider executing it, if it has no unsatisfied dependencies. * * @private - * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. */ - function handlePending( module ) { - var j, job, hasErrors, m, stateChange, fromBaseModule; - - if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) { - fromBaseModule = baseModules.indexOf( module ) !== -1; - // If the current module failed, mark all dependent modules also as failed. - // Iterate until steady-state to propagate the error state upwards in the - // dependency tree. - do { - stateChange = false; - for ( m in registry ) { - if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) { - // Always propagate errors from base modules to regular modules (implicit dependency). - // Between base modules or regular modules, consider direct dependencies only. - if ( - ( fromBaseModule && baseModules.indexOf( m ) === -1 ) || - anyFailed( registry[ m ].dependencies ) - ) { - registry[ m ].state = 'error'; - stateChange = true; + function doPropagation() { + var errorModule, baseModuleError, module, i, failed, job, + didPropagate = true; + + // Keep going until the last iteration performed no actions. + do { + didPropagate = false; + + // Stage 1: Propagate failures + while ( errorModules.length ) { + errorModule = errorModules.shift(); + baseModuleError = baseModules.indexOf( errorModule ) !== -1; + for ( module in registry ) { + if ( registry[ module ].state !== 'error' && registry[ module ].state !== 'missing' ) { + if ( baseModuleError && baseModules.indexOf( module ) === -1 ) { + // Propate error from base module to all regular (non-base) modules + registry[ module ].state = 'error'; + didPropagate = true; + } else if ( registry[ module ].dependencies.indexOf( errorModule ) !== -1 ) { + // Propagate error from dependency to depending module + registry[ module ].state = 'error'; + // .. and propagate it further + errorModules.push( module ); + didPropagate = true; } } } - } while ( stateChange ); - } + } - // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module. - for ( j = 0; j < jobs.length; j++ ) { - hasErrors = anyFailed( jobs[ j ].dependencies ); - if ( hasErrors || allReady( jobs[ j ].dependencies ) ) { - // All dependencies satisfied, or some have errors - job = jobs[ j ]; - jobs.splice( j, 1 ); - j -= 1; - try { - if ( hasErrors ) { - if ( typeof job.error === 'function' ) { - job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] ); - } - } else { - if ( typeof job.ready === 'function' ) { + // Stage 2: Execute 'loaded' modules with no unsatisfied dependencies + for ( module in registry ) { + if ( registry[ module ].state === 'loaded' && allWithImplicitReady( module ) ) { + // Recursively execute all dependent modules that were already loaded + // (waiting for execution) and no longer have unsatisfied dependencies. + // Base modules may have dependencies amongst eachother to ensure correct + // execution order. Regular modules wait for all base modules. + // eslint-disable-next-line no-use-before-define + execute( module ); + didPropagate = true; + } + } + + // Stage 3: Invoke job callbacks that are no longer blocked + for ( i = 0; i < jobs.length; i++ ) { + job = jobs[ i ]; + failed = anyFailed( job.dependencies ); + if ( failed || allReady( job.dependencies ) ) { + jobs.splice( i, 1 ); + i -= 1; + try { + if ( failed && job.error ) { + job.error( new Error( 'Module has failed dependencies' ), job.dependencies ); + } else if ( !failed && job.ready ) { job.ready(); } + } catch ( e ) { + // A user-defined callback raised an exception. + // Swallow it to protect our state machine! + mw.trackError( 'resourceloader.exception', { + exception: e, + source: 'load-callback' + } ); } - } catch ( e ) { - // A user-defined callback raised an exception. - // Swallow it to protect our state machine! - mw.trackError( 'resourceloader.exception', { - exception: e, - module: module, - source: 'load-callback' - } ); + didPropagate = true; } } + } while ( didPropagate ); + + willPropagate = false; + } + + /** + * Request a (debounced) call to doPropagation(). + * + * @private + */ + function requestPropagation() { + if ( willPropagate ) { + // Already scheduled, or, we're already in a doPropagation stack. + return; } + willPropagate = true; + // Yield for two reasons: + // * Allow successive calls to mw.loader.implement() from the same + // load.php response, or from the same asyncEval() to be in the + // propagation batch. + // * Allow the browser to breathe between the reception of + // module source code and the execution of it. + // + // Use a high priority because the user may be waiting for interactions + // to start being possible. But, first provide a moment (up to 'timeout') + // for native input event handling (e.g. scrolling/typing/clicking). + mw.requestIdleCallback( doPropagation, { timeout: 1 } ); + } - // The current module became 'ready'. - if ( registry[ module ].state === 'ready' ) { - // Save it to the module store. - mw.loader.store.set( module, registry[ module ] ); - // Recursively execute all dependent modules that were already loaded - // (waiting for execution) and no longer have unsatisfied dependencies. - for ( m in registry ) { - // Base modules may have dependencies amongst eachother to ensure correct - // execution order. Regular modules wait for all base modules. - if ( registry[ m ].state === 'loaded' && allWithImplicitReady( m ) ) { - // eslint-disable-next-line no-use-before-define - execute( m ); - } + /** + * Update a module's state in the registry and make sure any neccesary + * propagation will occur. See #doPropagation for more about propagation. + * See #registry for more about how states are used. + * + * @private + * @param {string} module + * @param {string} state + */ + function setAndPropagate( module, state ) { + registry[ module ].state = state; + if ( state === 'loaded' || state === 'ready' || state === 'error' || state === 'missing' ) { + if ( state === 'ready' ) { + // Queue to later be synced to the local module store. + mw.loader.store.add( module ); + } else if ( state === 'error' || state === 'missing' ) { + errorModules.push( module ); } + requestPropagation(); } } @@ -877,7 +926,7 @@ function sortDependencies( module, resolved, unresolved ) { var i, deps, skip; - if ( !hasOwn.call( registry, module ) ) { + if ( !( module in registry ) ) { throw new Error( 'Unknown dependency: ' + module ); } @@ -888,8 +937,7 @@ if ( skip() ) { registry[ module ].skipped = true; registry[ module ].dependencies = []; - registry[ module ].state = 'ready'; - handlePending( module ); + setAndPropagate( module, 'ready' ); return; } } @@ -1008,7 +1056,7 @@ function queueModuleScript( src, moduleName, callback ) { pendingRequests.push( function () { // Keep in sync with execute()/runScript(). - if ( moduleName !== 'jquery' && hasOwn.call( registry, moduleName ) ) { + if ( moduleName !== 'jquery' ) { window.require = mw.loader.require; window.module = registry[ moduleName ].module; } @@ -1123,8 +1171,7 @@ // 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' ) { - registry[ module ].state = 'error'; - handlePending( module ); + setAndPropagate( module, 'error' ); return; } queue.push( module ); @@ -1144,9 +1191,6 @@ 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 ); - } if ( registry[ module ].state !== 'loaded' ) { throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module ); } @@ -1161,8 +1205,7 @@ script = registry[ module ].script; markModuleReady = function () { $CODE.profileScriptEnd(); - registry[ module ].state = 'ready'; - handlePending( module ); + setAndPropagate( module, 'ready' ); }; nestedAddScript = function ( arr, callback, i ) { // Recursively call queueModuleScript() in its own callback @@ -1212,13 +1255,13 @@ } 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'; + setAndPropagate( module, 'error' ); $CODE.profileScriptEnd(); mw.trackError( 'resourceloader.exception', { - exception: e, module: - module, source: 'module-execute' + exception: e, + module: module, + source: 'module-execute' } ); - handlePending( module ); } }; @@ -1393,7 +1436,7 @@ /** * 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 @@ -1588,8 +1631,7 @@ * or null if the module does not exist */ function getModuleKey( module ) { - return hasOwn.call( registry, module ) ? - ( module + '@' + registry[ module ].version ) : null; + return module in registry ? ( module + '@' + registry[ module ].version ) : null; } /** @@ -1611,6 +1653,34 @@ }; } + /** + * @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 ( module in registry ) { + 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 { /** @@ -1636,7 +1706,7 @@ /** * Start loading of all queued module dependencies. * - * @protected + * @private */ work: function () { var q, batch, implementations, sourceModules; @@ -1646,7 +1716,7 @@ // Appends a list of modules from the queue to the batch for ( q = 0; q < queue.length; q++ ) { // Only load modules which are registered - if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) { + if ( queue[ q ] in registry && registry[ queue[ q ] ].state === 'registered' ) { // Prevent duplicate entries if ( batch.indexOf( queue[ q ] ) === -1 ) { batch.push( queue[ q ] ); @@ -1714,76 +1784,58 @@ * * 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 ( id in sources ) { + 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 ); - // module is an array of arrays - for ( i = 0; i < module.length; i++ ) { - // module is an array of module names - 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 + // 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 - }; }, /** @@ -1811,7 +1863,7 @@ * 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. */ @@ -1820,7 +1872,7 @@ name = split.name, version = split.version; // Automatically register module - if ( !hasOwn.call( registry, name ) ) { + if ( !( name in registry ) ) { mw.loader.register( name ); } // Check for duplicate implementation @@ -1841,10 +1893,7 @@ registry[ name ].templates = templates || null; // The module may already have been marked as erroneous if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) { - registry[ name ].state = 'loaded'; - if ( allWithImplicitReady( name ) ) { - execute( name ); - } + setAndPropagate( name, 'loaded' ); } }, @@ -1906,21 +1955,16 @@ /** * Change the state of one or more modules. * - * @param {Object} modules Object of module name/state pairs + * @param {Object} states Object of module name/state pairs */ - state: function ( modules ) { + state: function ( states ) { var module, state; - for ( module in modules ) { - state = modules[ module ]; - if ( !hasOwn.call( registry, module ) ) { + for ( module in states ) { + state = states[ module ]; + if ( !( module in registry ) ) { mw.loader.register( module ); } - registry[ module ].state = state; - if ( state === 'ready' || state === 'error' || state === 'missing' ) { - // Make sure pending modules depending on this one get executed if their - // dependencies are now fulfilled! - handlePending( module ); - } + setAndPropagate( module, state ); } }, @@ -1932,7 +1976,7 @@ * in the registry. */ getVersion: function ( module ) { - return hasOwn.call( registry, module ) ? registry[ module ].version : null; + return module in registry ? registry[ module ].version : null; }, /** @@ -1943,7 +1987,7 @@ * in the registry. */ getState: function ( module ) { - return hasOwn.call( registry, module ) ? registry[ module ].state : null; + return module in registry ? registry[ module ].state : null; }, /** @@ -2005,6 +2049,10 @@ // 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 }, @@ -2061,7 +2109,7 @@ // Disabled because localStorage quotas are tight and (in Firefox's case) // shared by multiple origins. // See T66721, and . - /Firefox|Opera/.test( navigator.userAgent ) || + /Firefox/.test( navigator.userAgent ) || // Disabled by configuration. !mw.config.get( 'wgResourceLoaderStorageEnabled' ) @@ -2141,17 +2189,34 @@ }, /** - * 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 ); @@ -2159,6 +2224,7 @@ // 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 || @@ -2196,7 +2262,6 @@ return; } this.items[ key ] = src; - this.update(); }, /** @@ -2229,30 +2294,50 @@ }, /** - * 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: + * + * - 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. + * + * - 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. * - * 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. + * - Repeatedly deleting and creating timers is non-trivial. * - * 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. + * - 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 @@ -2269,23 +2354,25 @@ } ); } + // 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 ); + } }; }() ) }