/**
* Base library for MediaWiki.
*
- * Exposed globally as `mediaWiki` with `mw` as shortcut.
+ * Exposed globally as `mw`, with `mediaWiki` as alias.
*
* @class mw
* @alternateClassName mediaWiki
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;
+ 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)
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;
}
} );
/**
* 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.
* - 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.
*/
/**
// The current module became 'ready'.
if ( registry[ module ].state === 'ready' ) {
- // Save it to the module store.
- mw.loader.store.set( module, registry[ module ] );
+ // 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 ) {
/**
* 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
};
}
+ /**
+ * @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 ( hasOwn.call( registry, module ) ) {
+ 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 {
/**
/**
* Start loading of all queued module dependencies.
*
- * @protected
+ * @private
*/
work: function () {
var q, batch, implementations, sourceModules;
*
* 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 ( hasOwn.call( sources, id ) ) {
+ 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 );
- for ( i = 0; i < module.length; i++ ) {
- // module is an array of module names
- if ( typeof module[ i ] === 'string' ) {
- mw.loader.register( module[ i ] );
- // module is an array of arrays
- } else if ( typeof module[ i ] === 'object' ) {
- 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 <https://gist.github.com/Krinkle/f06fdb3de62824c6c16f02a0e6ce0e66>
+ // 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
- };
},
/**
* 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.
*/
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 ) {
// 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 },
init: function () {
var raw, data;
- if ( mw.loader.store.enabled !== null ) {
+ if ( this.enabled !== null ) {
// Init already ran
return;
}
// Disabled because localStorage quotas are tight and (in Firefox's case)
// shared by multiple origins.
// See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
- /Firefox|Opera/.test( navigator.userAgent ) ||
+ /Firefox/.test( navigator.userAgent ) ||
// Disabled by configuration.
!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;
},
/**
- * 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;
-
- if ( !mw.loader.store.enabled ) {
+ 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 );
if (
// Already stored a copy of this exact version
- key in mw.loader.store.items ||
+ key in this.items ||
// Module failed to load
+ !descriptor ||
descriptor.state !== 'ready' ||
// Unversioned, private, or site-/user-specific
!descriptor.version ||
}
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;
},
/**
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 ) {}
},
/**
- * 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:
*
- * 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.
+ * - 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.
*
- * 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.
+ * - 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.
*
+ * - Repeatedly deleting and creating timers is non-trivial.
+ *
+ * - 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
} );
}
+ // 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 );
+ }
};
}() )
}