* State machine:
*
* - `registered`:
- * The module is known to the system but not yet requested.
+ * The module is known to the system but not yet required.
* Meta data is registered via mw.loader#register. Calls to that method are
* generated server-side by the startup module.
* - `loading`:
- * The module is requested through mw.loader (either directly or as dependency of
- * another module). The client will be fetching module contents from the server.
+ * The module was required through mw.loader (either directly or as dependency of
+ * another module). The client will fetch module contents from the server.
* The contents are then stashed in the registry via mw.loader#implement.
* - `loaded`:
- * The module has been requested from the server and stashed via mw.loader#implement.
- * If the module has no more dependencies in-fight, the module will be executed
- * right away. Otherwise execution is deferred, controlled via #handlePending.
+ * 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.
* - `executing`:
* The module is being executed.
* - `ready`:
//
sources = {},
- // List of modules which will be loaded as when ready
- batch = [],
-
- // Pending queueModuleScript() requests
+ // For queueModuleScript()
handlingPendingRequests = false,
pendingRequests = [],
/**
* List of callback jobs waiting for modules to be ready.
*
- * Jobs are created by #request() and run by #handlePending().
+ * Jobs are created by #enqueue() and run by #handlePending().
*
* Typically when a job is created for a module, the job's dependencies contain
- * both the module being requested and all its recursive dependencies.
+ * both the required module and all its recursive dependencies.
*
* Format:
*
cssBuffer = '',
cssBufferTimer = null,
cssCallbacks = $.Callbacks(),
- isIE9 = document.documentMode === 9;
+ isIE9 = document.documentMode === 9,
+ rAF = window.requestAnimationFrame || setTimeout;
function getMarker() {
if ( !marker ) {
if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
// Linebreak for somewhat distinguishable sections
cssBuffer += '\n' + cssText;
- // TODO: Using requestAnimationFrame would perform better by not injecting
- // styles while the browser is busy painting.
if ( !cssBufferTimer ) {
- cssBufferTimer = setTimeout( function () {
+ cssBufferTimer = rAF( function () {
+ // Wrap in anonymous function that takes no arguments
// Support: Firefox < 13
// Firefox 12 has non-standard behaviour of passing a number
// as first argument to a setTimeout callback.
j -= 1;
try {
if ( hasErrors ) {
- if ( $.isFunction( job.error ) ) {
+ if ( typeof job.error === 'function' ) {
job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
}
} else {
- if ( $.isFunction( job.ready ) ) {
+ if ( typeof job.ready === 'function' ) {
job.ready();
}
}
}
// Resolves dynamic loader function and replaces it with its own results
- if ( $.isFunction( registry[ module ].dependencies ) ) {
+ if ( typeof registry[ module ].dependencies === 'function' ) {
registry[ module ].dependencies = registry[ module ].dependencies();
// Ensures the module's dependencies are always in an array
if ( typeof registry[ module ].dependencies !== 'object' ) {
// Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
// XHR for a same domain request instead of <script>, which changes the request
// headers (potentially missing a cache hit), and reduces caching in general
- // since browsers cache XHR much less (if at all). And XHR means we retreive
+ // since browsers cache XHR much less (if at all). And XHR means we retrieve
// text, so we'd need to $.globalEval, which then messes up line numbers.
crossDomain: true,
cache: true
legacyWait.always( function () {
if ( $.isArray( script ) ) {
nestedAddScript( script, markModuleReady, 0 );
- } else if ( $.isFunction( script ) ) {
+ } else if ( typeof script === 'function' ) {
// Pass jQuery twice so that the signature of the closure which wraps
// the script can bind both '$' and 'jQuery'.
script( $, $, mw.loader.require, registry[ module ].module );
}
/**
- * Adds all dependencies to the queue with optional callbacks to be run
- * when the dependencies are ready or fail
+ * Add one or more modules to the module load queue.
+ *
+ * See also #work().
*
* @private
* @param {string|string[]} dependencies Module name or array of string module names
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute when any dependency fails
*/
- function request( dependencies, ready, error ) {
+ function enqueue( dependencies, ready, error ) {
// Allow calling by single module name
if ( typeof dependencies === 'string' ) {
dependencies = [ dependencies ];
}
/**
- * Load modules from load.php
+ * Make a network request to load modules from the server.
*
* @private
* @param {Object} moduleMap Module map, see #buildModulesString
* @param {string} sourceLoadScript URL of load.php
*/
function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
- var request = $.extend(
+ var query = $.extend(
{ modules: buildModulesString( moduleMap ) },
currReqBase
);
- request = sortQuery( request );
- addScript( sourceLoadScript + '?' + $.param( request ) );
+ query = sortQuery( query );
+ addScript( sourceLoadScript + '?' + $.param( query ) );
}
/**
* @param {Array} modules Modules array
*/
function resolveIndexedDependencies( modules ) {
- $.each( modules, function ( idx, module ) {
- if ( module[ 2 ] ) {
- module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
- return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
- } );
+ var i, j, deps;
+ function resolveIndex( dep ) {
+ return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
+ }
+ for ( i = 0; i < modules.length; i++ ) {
+ deps = modules[ i ][ 2 ];
+ if ( deps ) {
+ for ( j = 0; j < deps.length; j++ ) {
+ deps[ j ] = resolveIndex( deps[ j ] );
+ }
+ }
+ }
+ }
+
+ /**
+ * Create network requests for a batch of modules.
+ *
+ * This is an internal method for #work(). This must not be called directly
+ * unless the modules are already registered, and no request is in progress,
+ * and the module state has already been set to `loading`.
+ *
+ * @private
+ * @param {string[]} batch
+ */
+ function batchRequest( batch ) {
+ var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+ source, group, i, modules, sourceLoadScript,
+ currReqBase, currReqBaseLength, moduleMap, l,
+ lastDotIndex, prefix, suffix, bytesAdded;
+
+ if ( !batch.length ) {
+ return;
+ }
+
+ // Always order modules alphabetically to help reduce cache
+ // misses for otherwise identical content.
+ batch.sort();
+
+ // Build a list of query parameters common to all requests
+ reqBase = {
+ skin: mw.config.get( 'skin' ),
+ lang: mw.config.get( 'wgUserLanguage' ),
+ debug: mw.config.get( 'debug' )
+ };
+ maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+
+ // Split module list by source and by group.
+ splits = {};
+ for ( b = 0; b < batch.length; b++ ) {
+ bSource = registry[ batch[ b ] ].source;
+ bGroup = registry[ batch[ b ] ].group;
+ if ( !hasOwn.call( splits, bSource ) ) {
+ splits[ bSource ] = {};
+ }
+ if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+ splits[ bSource ][ bGroup ] = [];
+ }
+ bSourceGroup = splits[ bSource ][ bGroup ];
+ bSourceGroup.push( batch[ b ] );
+ }
+
+ for ( source in splits ) {
+
+ sourceLoadScript = sources[ source ];
+
+ for ( group in splits[ source ] ) {
+
+ // Cache access to currently selected list of
+ // modules for this group from this source.
+ modules = splits[ source ][ group ];
+
+ currReqBase = $.extend( {
+ version: getCombinedVersion( modules )
+ }, reqBase );
+ // For user modules append a user name to the query string.
+ if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
+ currReqBase.user = mw.config.get( 'wgUserName' );
+ }
+ currReqBaseLength = $.param( currReqBase ).length;
+ // We may need to split up the request to honor the query string length limit,
+ // so build it piece by piece.
+ l = currReqBaseLength + 9; // '&modules='.length == 9
+
+ moduleMap = {}; // { prefix: [ suffixes ] }
+
+ for ( i = 0; i < modules.length; i++ ) {
+ // Determine how many bytes this module would add to the query string
+ lastDotIndex = modules[ i ].lastIndexOf( '.' );
+
+ // If lastDotIndex is -1, substr() returns an empty string
+ prefix = modules[ i ].substr( 0, lastDotIndex );
+ suffix = modules[ i ].slice( lastDotIndex + 1 );
+
+ bytesAdded = hasOwn.call( moduleMap, prefix )
+ ? suffix.length + 3 // '%2C'.length == 3
+ : modules[ i ].length + 3; // '%7C'.length == 3
+
+ // If the url would become too long, create a new one,
+ // but don't create empty requests
+ if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
+ // This url would become too long, create a new one, and start the old one
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ moduleMap = {};
+ l = currReqBaseLength + 9;
+ mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
+ }
+ if ( !hasOwn.call( moduleMap, prefix ) ) {
+ moduleMap[ prefix ] = [];
+ }
+ moduleMap[ prefix ].push( suffix );
+ l += bytesAdded;
+ }
+ // If there's anything left in moduleMap, request that too
+ if ( !$.isEmptyObject( moduleMap ) ) {
+ doRequest( moduleMap, currReqBase, sourceLoadScript );
+ }
+ }
+ }
+ }
+
+ /**
+ * Evaluate a batch of load.php responses retrieved from mw.loader.store.
+ *
+ * @private
+ * @param {string[]} implementations Array containing pieces of JavaScript code in the
+ * form of calls to mw.loader#implement().
+ * @param {Function} cb Callback in case of failure
+ * @param {Error} cb.err
+ */
+ function batchEval( implementations, cb ) {
+ if ( !implementations.length ) {
+ return;
+ }
+ mw.requestIdleCallback( function iterate( deadline ) {
+ while ( implementations[ 0 ] && deadline.timeRemaining() > 5 ) {
+ try {
+ $.globalEval( implementations.shift() );
+ } catch ( err ) {
+ cb( err );
+ return;
+ }
+ }
+ if ( implementations[ 0 ] ) {
+ mw.requestIdleCallback( iterate );
}
} );
}
addStyleTag: newStyleTag,
/**
- * Batch-request queued dependencies from the server.
+ * Start loading of all queued module dependencies.
*
* @protected
*/
work: function () {
- var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
- source, concatSource, origBatch, group, i, modules, sourceLoadScript,
- currReqBase, currReqBaseLength, moduleMap, l,
- lastDotIndex, prefix, suffix, bytesAdded;
-
- // Build a list of request parameters common to all requests.
- reqBase = {
- skin: mw.config.get( 'skin' ),
- lang: mw.config.get( 'wgUserLanguage' ),
- debug: mw.config.get( 'debug' )
- };
- // Split module batch by source and by group.
- splits = {};
- maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
+ var q, batch, implementations, sourceModules;
+
+ batch = [];
// Appends a list of modules from the queue to the batch
for ( q = 0; q < queue.length; q++ ) {
- // Only request modules which are registered
+ // Only load modules which are registered
if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
// Prevent duplicate entries
if ( $.inArray( queue[ q ], batch ) === -1 ) {
}
}
+ // Now that the queue has been processed into a batch, clear the queue.
+ // This MUST happen before we initiate any eval or network request. Otherwise,
+ // it is possible for a cached script to instantly trigger the same work queue
+ // again; all before we've cleared it causing each request to include modules
+ // which are already loaded.
+ queue = [];
+
+ if ( !batch.length ) {
+ return;
+ }
+
mw.loader.store.init();
if ( mw.loader.store.enabled ) {
- concatSource = [];
- origBatch = batch;
+ implementations = [];
+ sourceModules = [];
batch = $.grep( batch, function ( module ) {
- var source = mw.loader.store.get( module );
- if ( source ) {
- concatSource.push( source );
+ var implementation = mw.loader.store.get( module );
+ if ( implementation ) {
+ implementations.push( implementation );
+ sourceModules.push( module );
return false;
}
return true;
} );
- try {
- $.globalEval( concatSource.join( ';' ) );
- } catch ( err ) {
+ batchEval( implementations, function ( err ) {
// Not good, the cached mw.loader.implement calls failed! This should
// never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
// Depending on how corrupt the string is, it is likely that some
// modules' implement() succeeded while the ones after the error will
// never run and leave their modules in the 'loading' state forever.
-
// Since this is an error not caused by an individual module but by
// something that infected the implement call itself, don't take any
// risks and clear everything in this cache.
mw.loader.store.clear();
- // Re-add the ones still pending back to the batch and let the server
- // repopulate these modules to the cache.
- // This means that at most one module will be useless (the one that had
- // the error) instead of all of them.
mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
- origBatch = $.grep( origBatch, function ( module ) {
+
+ // Re-add the failed ones that are still pending back to the batch
+ var failed = $.grep( sourceModules, function ( module ) {
return registry[ module ].state === 'loading';
} );
- batch = batch.concat( origBatch );
- }
- }
-
- // Early exit if there's nothing to load...
- if ( !batch.length ) {
- return;
- }
-
- // The queue has been processed into the batch, clear up the queue.
- queue = [];
-
- // Always order modules alphabetically to help reduce cache
- // misses for otherwise identical content.
- batch.sort();
-
- // Split batch by source and by group.
- for ( b = 0; b < batch.length; b++ ) {
- bSource = registry[ batch[ b ] ].source;
- bGroup = registry[ batch[ b ] ].group;
- if ( !hasOwn.call( splits, bSource ) ) {
- splits[ bSource ] = {};
- }
- if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
- splits[ bSource ][ bGroup ] = [];
- }
- bSourceGroup = splits[ bSource ][ bGroup ];
- bSourceGroup.push( batch[ b ] );
+ batchRequest( failed );
+ } );
}
- // Clear the batch - this MUST happen before we append any
- // script elements to the body or it's possible that a script
- // will be locally cached, instantly load, and work the batch
- // again, all before we've cleared it causing each request to
- // include modules which are already loaded.
- batch = [];
-
- for ( source in splits ) {
-
- sourceLoadScript = sources[ source ];
-
- for ( group in splits[ source ] ) {
-
- // Cache access to currently selected list of
- // modules for this group from this source.
- modules = splits[ source ][ group ];
-
- currReqBase = $.extend( {
- version: getCombinedVersion( modules )
- }, reqBase );
- // For user modules append a user name to the request.
- if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
- currReqBase.user = mw.config.get( 'wgUserName' );
- }
- currReqBaseLength = $.param( currReqBase ).length;
- // We may need to split up the request to honor the query string length limit,
- // so build it piece by piece.
- l = currReqBaseLength + 9; // '&modules='.length == 9
-
- moduleMap = {}; // { prefix: [ suffixes ] }
-
- for ( i = 0; i < modules.length; i++ ) {
- // Determine how many bytes this module would add to the query string
- lastDotIndex = modules[ i ].lastIndexOf( '.' );
-
- // If lastDotIndex is -1, substr() returns an empty string
- prefix = modules[ i ].substr( 0, lastDotIndex );
- suffix = modules[ i ].slice( lastDotIndex + 1 );
-
- bytesAdded = hasOwn.call( moduleMap, prefix )
- ? suffix.length + 3 // '%2C'.length == 3
- : modules[ i ].length + 3; // '%7C'.length == 3
-
- // If the request would become too long, create a new one,
- // but don't create empty requests
- if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
- // This request would become too long, create a new one
- // and fire off the old one
- doRequest( moduleMap, currReqBase, sourceLoadScript );
- moduleMap = {};
- l = currReqBaseLength + 9;
- mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
- }
- if ( !hasOwn.call( moduleMap, prefix ) ) {
- moduleMap[ prefix ] = [];
- }
- moduleMap[ prefix ].push( suffix );
- l += bytesAdded;
- }
- // If there's anything left in moduleMap, request that too
- if ( !$.isEmptyObject( moduleMap ) ) {
- doRequest( moduleMap, currReqBase, sourceLoadScript );
- }
- }
- }
+ batchRequest( batch );
},
/**
* @param {string} [skip=null] Script body of the skip function
*/
register: function ( module, version, dependencies, group, source, skip ) {
- var i;
+ var i, deps;
// Allow multiple registration
if ( typeof module === 'object' ) {
resolveIndexedDependencies( module );
if ( hasOwn.call( registry, module ) ) {
throw new Error( 'module already registered: ' + module );
}
+ if ( typeof dependencies === 'string' ) {
+ // A single module name
+ deps = [ dependencies ];
+ } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
+ // Array of module names or a function that returns an array
+ deps = dependencies;
+ }
// List the module as registered
registry[ module ] = {
// Exposed to execute() for mw.loader.implement() closures.
exports: {}
},
version: version !== undefined ? String( version ) : '',
- dependencies: [],
+ dependencies: deps || [],
group: typeof group === 'string' ? group : null,
source: typeof source === 'string' ? source : 'local',
state: 'registered',
skip: typeof skip === 'string' ? skip : null
};
- if ( typeof dependencies === 'string' ) {
- // Allow dependencies to be given as a single module name
- registry[ module ].dependencies = [ dependencies ];
- } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
- // Allow dependencies to be given as an array of module names
- // or a function which returns an array
- registry[ module ].dependencies = dependencies;
- }
},
/**
* Implement a module given the components that make up the module.
*
- * When #load or #using requests one or more modules, the server
+ * When #load() or #using() requests one or more modules, the server
* response contain calls to this function.
*
* @param {string} module Name of module
dependencies
);
} else {
- // Not all dependencies are ready: queue up a request
- request( dependencies, function () {
+ // Not all dependencies are ready, add to the load queue
+ enqueue( dependencies, function () {
deferred.resolve( mw.loader.require );
}, deferred.reject );
}
if ( allReady( filtered ) || anyFailed( filtered ) ) {
return;
}
- // Since some modules are not yet ready, queue up a request.
- request( filtered, undefined, undefined );
+ // Some modules are not yet ready, add to module load queue.
+ enqueue( filtered, undefined, undefined );
},
/**
if ( !hasOwn.call( registry, module ) ) {
mw.loader.register( module );
}
- if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
- && registry[ module ].state !== state ) {
+ registry[ module ].state = state;
+ if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1 ) {
// Make sure pending modules depending on this one get executed if their
// dependencies are now fulfilled!
- registry[ module ].state = state;
handlePending( module );
- } else {
- registry[ module ].state = state;
}
},
// Unversioned, private, or site-/user-specific
( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
// Partial descriptor
+ // (e.g. skipped module, or style module with state=ready)
$.inArray( undefined, [ descriptor.script, descriptor.style,
descriptor.messages, descriptor.templates ] ) !== -1
) {
var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
return mw.loader.getState( module ) === 'loading';
} );
- // In order to use jQuery.when (which stops early if one of the promises got rejected)
- // cast any loading failures into successes. We only need a callback, not the module.
- loading = $.map( loading, function ( module ) {
- return mw.loader.using( module ).then( null, function () {
- return $.Deferred().resolve();
+ // We only need a callback, not any actual module. First try a single using()
+ // for all loading modules. If one fails, fall back to tracking each module
+ // separately via $.when(), this is expensive.
+ loading = mw.loader.using( loading ).then( null, function () {
+ var all = $.map( loading, function ( module ) {
+ return mw.loader.using( module ).then( null, function () {
+ return $.Deferred().resolve();
+ } );
} );
+ return $.when.apply( $, all );
} );
- $.when.apply( $, loading ).then( function () {
+ loading.then( function () {
mwPerformance.mark( 'mwLoadEnd' );
mw.hook( 'resourceloader.loadEnd' ).fire();
} );