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=ba8869bd28fb128c043ce2ba58d817ab1f37c095;hb=fc2a88f66642590dda149590bdb72c5fc9ab5795;hpb=f7b003a0ed288b70d4267fe4a5f71b4fa242fdd7 diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index ba8869bd28..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,9 +321,7 @@ * @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 ) { + log.deprecate = function ( obj, key, val, msg, logName ) { var stacks; function maybeLog() { var name, @@ -365,7 +361,6 @@ return log; }() ); - /* eslint-enable no-console */ /** * @class mw @@ -569,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`: @@ -584,7 +579,7 @@ * @property * @private */ - var registry = {}, + var registry = Object.create( null ), // Mapping of sources, keyed by source-id, values are strings. // // Format: @@ -593,7 +588,7 @@ // 'sourceId': 'http://example.org/w/load.php' // } // - sources = {}, + sources = Object.create( null ), // For queueModuleScript() handlingPendingRequests = false, @@ -605,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. * @@ -623,6 +617,10 @@ */ jobs = [], + // For #requestPropagation() and #doPropagation() + willPropagate = false, + errorModules = [], + /** * @private * @property {Array} baseModules @@ -781,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' ) { - // 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 ) { - // 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(); } } @@ -881,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 ); } @@ -892,8 +937,7 @@ if ( skip() ) { registry[ module ].skipped = true; registry[ module ].dependencies = []; - registry[ module ].state = 'ready'; - handlePending( module ); + setAndPropagate( module, 'ready' ); return; } } @@ -1012,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; } @@ -1127,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 ); @@ -1148,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 ); } @@ -1165,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 @@ -1216,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 ); } }; @@ -1592,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; } /** @@ -1625,7 +1663,7 @@ * @param {string} [skip] */ function registerOne( module, version, dependencies, group, source, skip ) { - if ( hasOwn.call( registry, module ) ) { + if ( module in registry ) { throw new Error( 'module already registered: ' + module ); } registry[ module ] = { @@ -1678,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 ] ); @@ -1755,7 +1793,7 @@ addSource: function ( ids ) { var id; for ( id in ids ) { - if ( hasOwn.call( sources, id ) ) { + if ( id in sources ) { throw new Error( 'source already registered: ' + id ); } sources[ id ] = ids[ id ]; @@ -1834,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 @@ -1855,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' ); } }, @@ -1920,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 ); } }, @@ -1946,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; }, /** @@ -1957,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; }, /**