* @alternateClassName mediaWiki
* @singleton
*/
-/* global $VARS */
+/* global $VARS, $CODE */
( function () {
'use strict';
* See also #work().
*
* @private
- * @param {string|string[]} dependencies Module name or array of string module names
+ * @param {string[]} dependencies Array of module names in the registry
* @param {Function} [ready] Callback to execute when all dependencies are ready
* @param {Function} [error] Callback to execute when any dependency fails
*/
function enqueue( dependencies, ready, error ) {
- // Allow calling by single module name
- if ( typeof dependencies === 'string' ) {
- dependencies = [ dependencies ];
- }
-
if ( allReady( dependencies ) ) {
// Run ready immediately
if ( ready !== undefined ) {
}
registry[ module ].state = 'executing';
+ $CODE.profileExecuteStart();
runScript = function () {
var script, markModuleReady, nestedAddScript;
+ $CODE.profileScriptStart();
script = registry[ module ].script;
markModuleReady = function () {
+ $CODE.profileScriptEnd();
registry[ module ].state = 'ready';
handlePending( module );
};
// 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';
+ $CODE.profileScriptEnd();
mw.trackError( 'resourceloader.exception', {
exception: e, module:
module, source: 'module-execute'
}
}
+ // End profiling of execute()-self before we call checkCssHandles(),
+ // which (sometimes asynchronously) calls runScript(), which we want
+ // to measure separately without overlap.
+ $CODE.profileExecuteEnd();
+
// Kick off.
cssHandlesRegistered = true;
checkCssHandles();
* @param {string[]} batch
*/
function batchRequest( batch ) {
- var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+ var reqBase, splits, maxQueryLength, b, bSource, bGroup,
source, group, i, modules, sourceLoadScript,
currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
lastDotIndex, prefix, suffix, bytesAdded;
maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
// Split module list by source and by group.
- splits = {};
+ splits = Object.create( null );
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 ( !splits[ bSource ] ) {
+ splits[ bSource ] = Object.create( null );
}
- if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+ if ( !splits[ bSource ][ bGroup ] ) {
splits[ bSource ][ bGroup ] = [];
}
- bSourceGroup = splits[ bSource ][ bGroup ];
- bSourceGroup.push( batch[ b ] );
+ splits[ bSource ][ bGroup ].push( batch[ b ] );
}
for ( source in splits ) {
-
sourceLoadScript = sources[ source ];
for ( group in splits[ source ] ) {
// We may need to split up the request to honor the query string length limit,
// so build it piece by piece.
l = currReqBaseLength;
- moduleMap = {}; // { prefix: [ suffixes ] }
+ moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
currReqModules = [];
for ( i = 0; i < modules.length; i++ ) {
// 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 ) ?
+ bytesAdded = moduleMap[ prefix ] ?
suffix.length + 3 : // '%2C'.length == 3
modules[ i ].length + 3; // '%7C'.length == 3
doRequest();
// .. and start again.
l = currReqBaseLength;
- moduleMap = {};
+ moduleMap = Object.create( null );
currReqModules = [];
mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
}
- if ( !hasOwn.call( moduleMap, prefix ) ) {
+ if ( !moduleMap[ prefix ] ) {
moduleMap[ prefix ] = [];
}
l += bytesAdded;
* a list of arguments compatible with this method
* @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|Array} dependencies One string or array of strings of module
- * names on which this module depends.
+ * @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 ) {
- var i, deps;
+ var i;
// 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' ) {
- // Array of module names
- deps = dependencies;
- }
// List the module as registered
registry[ module ] = {
// Exposed to execute() for mw.loader.implement() closures.
module: {
exports: {}
},
- version: version !== undefined ? String( version ) : '',
- dependencies: deps || [],
+ version: String( version || '' ),
+ dependencies: dependencies || [],
group: typeof group === 'string' ? group : null,
source: typeof source === 'string' ? source : 'local',
state: 'registered',
/**
* Change the state of one or more modules.
*
- * @param {Object|string} modules Object of module name/state pairs
+ * @param {Object} modules Object of module name/state pairs
*/
state: function ( modules ) {
var module, state;
* modules and cache each of them separately, using each module's versioning scheme
* to determine when the cache should be invalidated.
*
+ * @private
* @singleton
* @class mw.loader.store
*/
* @return {string} String of concatenated vary conditions.
*/
getVary: function () {
- return [
- mw.config.get( 'skin' ),
- mw.config.get( 'wgResourceLoaderStorageVersion' ),
- mw.config.get( 'wgUserLanguage' )
- ].join( ':' );
+ return mw.config.get( 'skin' ) + ':' +
+ mw.config.get( 'wgResourceLoaderStorageVersion' ) + ':' +
+ mw.config.get( 'wgUserLanguage' );
},
/**
}
try {
+ // This a string we stored, or `null` if the key does not (yet) exist.
raw = localStorage.getItem( mw.loader.store.getStoreKey() );
// If we get here, localStorage is available; mark enabled
mw.loader.store.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 we get here, one of four things happened:
+ //
+ // 1. localStorage did not contain our store key.
+ // This means `raw` is `null`, and we're on a fresh page view (cold cache).
+ // The store was enabled, and `items` starts fresh.
+ //
+ // 2. localStorage contained parseable data under our store key,
+ // but it's not applicable to our current context (see getVary).
+ // The store was enabled, and `items` starts fresh.
+ //
+ // 3. JSON.parse threw (localStorage contained corrupt data).
+ // This means `raw` contains a string.
+ // The store was enabled, and `items` starts fresh.
+ //
+ // 4. localStorage threw (disabled or otherwise unavailable).
+ // This means `raw` was never assigned.
+ // We will disable the store below.
if ( raw === undefined ) {
// localStorage failed; disable store
mw.loader.store.enabled = false;
- } else {
- mw.loader.store.update();
}
},
*
* @param {string} module Module name
* @param {Object} descriptor The module's descriptor as set in the registry
- * @return {boolean} Module was set
*/
set: function ( module, descriptor ) {
var args, key, src;
if ( !mw.loader.store.enabled ) {
- return false;
+ return;
}
key = getModuleKey( module );
descriptor.templates ].indexOf( undefined ) !== -1
) {
// Decline to store
- return false;
+ return;
}
try {
exception: e,
source: 'store-localstorage-json'
} );
- return false;
+ return;
}
src = 'mw.loader.implement(' + args.join( ',' ) + ');';
if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
- return false;
+ return;
}
mw.loader.store.items[ key ] = src;
mw.loader.store.update();
- return true;
},
/**
* Iterate through the module store, removing any item that does not correspond
* (in name and version) to an item in the module registry.
- *
- * @return {boolean} Store was pruned
*/
prune: function () {
var key, module;
- if ( !mw.loader.store.enabled ) {
- return false;
- }
-
for ( key in mw.loader.store.items ) {
module = key.slice( 0, key.indexOf( '@' ) );
if ( getModuleKey( module ) !== key ) {
delete mw.loader.store.items[ key ];
}
}
- return true;
},
/**
mw.loader.store.items = {};
try {
localStorage.removeItem( mw.loader.store.getStoreKey() );
- } catch ( ignored ) {}
+ } catch ( e ) {}
},
/**
* Sync in-memory store back to localStorage.
*
- * This function debounces updates. When called with a flush already pending,
- * the call is coalesced into the pending update. The call to
- * localStorage.setItem will be naturally deferred until the page is quiescent.
+ * 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.
*
* 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. But the impact would
- * be minor and the problem would be corrected by subsequent page views.
+ * 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.
*
* @method
*/
update: ( function () {
- var hasPendingWrite = false;
+ var timer, hasPendingWrites = false;
function flushWrites() {
var data, key;
- if ( !hasPendingWrite || !mw.loader.store.enabled ) {
+ if ( !mw.loader.store.enabled ) {
return;
}
} );
}
- hasPendingWrite = false;
+ hasPendingWrites = false;
}
- return function () {
- if ( !hasPendingWrite ) {
- hasPendingWrite = true;
+ 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 );
}
+ }
+
+ return function () {
+ // Cancel the previous timer (if it hasn't fired yet)
+ clearTimeout( timer );
+ timer = setTimeout( request, 2000 );
};
}() )
}