Merge "Bump wikimedia/parsoid to 0.21.0-a11"
[mediawiki.git] / resources / src / startup / mediawiki.loader.js
blob8776ec7e1ad4f881d1c9284a06eca619444c0fdb
1 /*!
2  * Defines mw.loader, the infrastructure for loading ResourceLoader
3  * modules.
4  *
5  * This file is appended directly to the code in startup/mediawiki.js
6  */
7 /* global $VARS, $CODE, mw */
9 ( function () {
10         'use strict';
12         var store,
13                 hasOwn = Object.hasOwnProperty;
15         /**
16          * Client for ResourceLoader server end point.
17          *
18          * This client is in charge of maintaining the module registry and state
19          * machine, initiating network (batch) requests for loading modules, as
20          * well as dependency resolution and execution of source code.
21          *
22          * @see <https://www.mediawiki.org/wiki/ResourceLoader/Features>
23          * @namespace mw.loader
24          */
26         /**
27          * FNV132 hash function
28          *
29          * This function implements the 32-bit version of FNV-1.
30          * It is equivalent to hash( 'fnv132', ... ) in PHP, except
31          * its output is base 36 rather than hex.
32          * See <https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function>
33          *
34          * @private
35          * @param {string} str String to hash
36          * @return {string} hash as a five-character base 36 string
37          */
38         function fnv132( str ) {
39                 var hash = 0x811C9DC5;
41                 /* eslint-disable no-bitwise */
42                 for ( var i = 0; i < str.length; i++ ) {
43                         hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
44                         hash ^= str.charCodeAt( i );
45                 }
47                 hash = ( hash >>> 0 ).toString( 36 ).slice( 0, 5 );
48                 /* eslint-enable no-bitwise */
50                 while ( hash.length < 5 ) {
51                         hash = '0' + hash;
52                 }
53                 return hash;
54         }
56         /**
57          * Fired via mw.track on various resource loading errors.
58          *
59          * eslint-disable jsdoc/valid-types
60          *
61          * @event ~'resourceloader.exception'
62          * @ignore
63          * @param {Error|Mixed} e The error that was thrown. Almost always an Error
64          *   object, but in theory module code could manually throw something else, and that
65          *   might also end up here.
66          * @param {string} [module] Name of the module which caused the error. Omitted if the
67          *   error is not module-related or the module cannot be easily identified due to
68          *   batched handling.
69          * @param {string} source Source of the error. Possible values:
70          *
71          *   - load-callback: exception thrown by user callback
72          *   - module-execute: exception thrown by module code
73          *   - resolve: failed to sort dependencies for a module in mw.loader.load
74          *   - store-eval: could not evaluate module code cached in localStorage
75          *   - store-localstorage-json: JSON conversion error in mw.loader.store
76          *   - store-localstorage-update: localStorage conversion error in mw.loader.store.
77          */
79         /**
80          * Mapping of registered modules.
81          *
82          * See #implement and #execute for exact details on support for script, style and messages.
83          *
84          * @example // Format:
85          * {
86          *     'moduleName': {
87          *         // From mw.loader.register()
88          *         'version': '#####' (five-character hash)
89          *         'dependencies': ['required.foo', 'bar.also', ...]
90          *         'group': string, integer, (or) null
91          *         'source': 'local', (or) 'anotherwiki'
92          *         'skip': 'return !!window.Example;', (or) null, (or) boolean result of skip
93          *         'module': export Object
94          *
95          *         // Set by execute() or mw.loader.state()
96          *         // See mw.loader.getState() for documentation of the state machine
97          *         'state': 'registered', 'loading', 'loaded', 'executing', 'ready', 'error', or 'missing'
98          *
99          *         // Optionally added at run-time by mw.loader.impl()
100          *         'script': closure, array of urls, or string
101          *         'style': { ... } (see #execute)
102          *         'messages': { 'key': 'value', ... }
103          *     }
104          * }
105          *
106          * @property {Object}
107          * @private
108          */
109         var registry = Object.create( null ),
110                 // Mapping of sources, keyed by source-id, values are strings.
111                 //
112                 // Format:
113                 //
114                 //     {
115                 //         'sourceId': 'http://example.org/w/load.php'
116                 //     }
117                 //
118                 sources = Object.create( null ),
120                 // For queueModuleScript()
121                 handlingPendingRequests = false,
122                 pendingRequests = [],
124                 // List of modules to be loaded
125                 queue = [],
127                 /**
128                  * List of callback jobs waiting for modules to be ready.
129                  *
130                  * Jobs are created by #enqueue() and run by #doPropagation().
131                  * Typically when a job is created for a module, the job's dependencies contain
132                  * both the required module and all its recursive dependencies.
133                  *
134                  * @example // Format:
135                  * {
136                  *     'dependencies': [ module names ],
137                  *     'ready': Function callback
138                  *     'error': Function callback
139                  * }
140                  *
141                  * @property {Object[]} jobs
142                  * @private
143                  */
144                 jobs = [],
146                 // For #setAndPropagate() and #doPropagation()
147                 willPropagate = false,
148                 errorModules = [],
150                 /**
151                  * @private
152                  * @property {Array} baseModules
153                  */
154                 baseModules = $VARS.baseModules,
156                 /**
157                  * For #addEmbeddedCSS() and #addLink()
158                  *
159                  * @private
160                  * @property {HTMLElement|null} marker
161                  */
162                 marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ),
164                 // For #addEmbeddedCSS()
165                 lastCssBuffer;
167         /**
168          * Append an HTML element to `document.head` or before a specified node.
169          *
170          * @private
171          * @param {HTMLElement} el
172          * @param {Node|null} [nextNode]
173          */
174         function addToHead( el, nextNode ) {
175                 if ( nextNode && nextNode.parentNode ) {
176                         nextNode.parentNode.insertBefore( el, nextNode );
177                 } else {
178                         document.head.appendChild( el );
179                 }
180         }
182         /**
183          * Create a new style element and add it to the DOM.
184          * Stable for use in gadgets.
185          *
186          * @method mw.loader.addStyleTag
187          * @param {string} text CSS text
188          * @param {Node|null} [nextNode] The element where the style tag
189          *  should be inserted before
190          * @return {HTMLStyleElement} Reference to the created style element
191          */
192         function newStyleTag( text, nextNode ) {
193                 var el = document.createElement( 'style' );
194                 el.appendChild( document.createTextNode( text ) );
195                 addToHead( el, nextNode );
196                 return el;
197         }
199         /**
200          * @private
201          * @param {Object} cssBuffer
202          */
203         function flushCssBuffer( cssBuffer ) {
204                 // Make sure the next call to addEmbeddedCSS() starts a new buffer.
205                 // This must be done before we run the callbacks, as those may end up
206                 // queueing new chunks which would be lost otherwise (T105973).
207                 //
208                 // There can be more than one buffer in-flight (given "@import", and
209                 // generally due to race conditions). Only tell addEmbeddedCSS() to
210                 // start a new buffer if we're currently flushing the last one that it
211                 // started. If we're flushing an older buffer, keep the last one open.
212                 if ( cssBuffer === lastCssBuffer ) {
213                         lastCssBuffer = null;
214                 }
215                 newStyleTag( cssBuffer.cssText, marker );
216                 for ( var i = 0; i < cssBuffer.callbacks.length; i++ ) {
217                         cssBuffer.callbacks[ i ]();
218                 }
219         }
221         /**
222          * Add a bit of CSS text to the current browser page.
223          *
224          * The creation and insertion of the `<style>` element is debounced for two reasons:
225          *
226          * - Performing the insertion before the next paint round via requestAnimationFrame
227          *   avoids forced or wasted style recomputations, which are expensive in browsers.
228          * - Reduce how often new stylesheets are inserted by letting additional calls to this
229          *   function accumulate into a buffer for at least one JavaScript tick. Modules are
230          *   received from the server in batches, which means there is likely going to be many
231          *   calls to this function in a row within the same tick / the same call stack.
232          *   See also T47810.
233          *
234          * @private
235          * @param {string} cssText CSS text to be added in a `<style>` tag.
236          * @param {Function} callback Called after the insertion has occurred.
237          */
238         function addEmbeddedCSS( cssText, callback ) {
239                 // Start a new buffer if one of the following is true:
240                 // - We've never started a buffer before, this will be our first.
241                 // - The last buffer we created was flushed meanwhile, so start a new one.
242                 // - The next CSS chunk syntactically needs to be at the start of a stylesheet (T37562).
243                 if ( !lastCssBuffer || cssText.startsWith( '@import' ) ) {
244                         lastCssBuffer = {
245                                 cssText: '',
246                                 callbacks: []
247                         };
248                         requestAnimationFrame( flushCssBuffer.bind( null, lastCssBuffer ) );
249                 }
251                 // Linebreak for somewhat distinguishable sections
252                 lastCssBuffer.cssText += '\n' + cssText;
253                 lastCssBuffer.callbacks.push( callback );
254         }
256         /**
257          * See also `ResourceLoader.php#makeVersionQuery` on the server.
258          *
259          * @private
260          * @param {string[]} modules List of module names
261          * @return {string} Hash of concatenated version hashes.
262          */
263         function getCombinedVersion( modules ) {
264                 var hashes = modules.reduce( function ( result, module ) {
265                         return result + registry[ module ].version;
266                 }, '' );
267                 return fnv132( hashes );
268         }
270         /**
271          * Determine whether all dependencies are in state 'ready', which means we may
272          * execute the module or job now.
273          *
274          * @private
275          * @param {string[]} modules Names of modules to be checked
276          * @return {boolean} True if all modules are in state 'ready', false otherwise
277          */
278         function allReady( modules ) {
279                 for ( var i = 0; i < modules.length; i++ ) {
280                         if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
281                                 return false;
282                         }
283                 }
284                 return true;
285         }
287         /**
288          * Determine whether all direct and base dependencies are in state 'ready'
289          *
290          * @private
291          * @param {string} module Name of the module to be checked
292          * @return {boolean} True if all direct/base dependencies are in state 'ready'; false otherwise
293          */
294         function allWithImplicitReady( module ) {
295                 return allReady( registry[ module ].dependencies ) &&
296                         ( baseModules.indexOf( module ) !== -1 || allReady( baseModules ) );
297         }
299         /**
300          * Determine whether all dependencies are in state 'ready', which means we may
301          * execute the module or job now.
302          *
303          * @private
304          * @param {string[]} modules Names of modules to be checked
305          * @return {boolean|string} False if no modules are in state 'error' or 'missing';
306          *  failed module otherwise
307          */
308         function anyFailed( modules ) {
309                 for ( var i = 0; i < modules.length; i++ ) {
310                         var state = mw.loader.getState( modules[ i ] );
311                         if ( state === 'error' || state === 'missing' ) {
312                                 return modules[ i ];
313                         }
314                 }
315                 return false;
316         }
318         /**
319          * Handle propagation of module state changes and reactions to them.
320          *
321          * - When a module reaches a failure state, this should be propagated to
322          *   modules that depend on the failed module.
323          * - When a module reaches a final state, pending job callbacks for the
324          *   module from mw.loader.using() should be called.
325          * - When a module reaches the 'ready' state from #execute(), consider
326          *   executing dependent modules now having their dependencies satisfied.
327          * - When a module reaches the 'loaded' state from mw.loader.impl,
328          *   consider executing it, if it has no unsatisfied dependencies.
329          *
330          * @private
331          */
332         function doPropagation() {
333                 var didPropagate = true;
334                 var module;
336                 // Keep going until the last iteration performed no actions.
337                 while ( didPropagate ) {
338                         didPropagate = false;
340                         // Stage 1: Propagate failures
341                         while ( errorModules.length ) {
342                                 var errorModule = errorModules.shift(),
343                                         baseModuleError = baseModules.indexOf( errorModule ) !== -1;
344                                 for ( module in registry ) {
345                                         if ( registry[ module ].state !== 'error' && registry[ module ].state !== 'missing' ) {
346                                                 if ( baseModuleError && baseModules.indexOf( module ) === -1 ) {
347                                                         // Propate error from base module to all regular (non-base) modules
348                                                         registry[ module ].state = 'error';
349                                                         didPropagate = true;
350                                                 } else if ( registry[ module ].dependencies.indexOf( errorModule ) !== -1 ) {
351                                                         // Propagate error from dependency to depending module
352                                                         registry[ module ].state = 'error';
353                                                         // .. and propagate it further
354                                                         errorModules.push( module );
355                                                         didPropagate = true;
356                                                 }
357                                         }
358                                 }
359                         }
361                         // Stage 2: Execute 'loaded' modules with no unsatisfied dependencies
362                         for ( module in registry ) {
363                                 if ( registry[ module ].state === 'loaded' && allWithImplicitReady( module ) ) {
364                                         // Recursively execute all dependent modules that were already loaded
365                                         // (waiting for execution) and no longer have unsatisfied dependencies.
366                                         // Base modules may have dependencies amongst eachother to ensure correct
367                                         // execution order. Regular modules wait for all base modules.
368                                         execute( module );
369                                         didPropagate = true;
370                                 }
371                         }
373                         // Stage 3: Invoke job callbacks that are no longer blocked
374                         for ( var i = 0; i < jobs.length; i++ ) {
375                                 var job = jobs[ i ];
376                                 var failed = anyFailed( job.dependencies );
377                                 if ( failed !== false || allReady( job.dependencies ) ) {
378                                         jobs.splice( i, 1 );
379                                         i -= 1;
380                                         try {
381                                                 if ( failed !== false && job.error ) {
382                                                         job.error( new Error( 'Failed dependency: ' + failed ), job.dependencies );
383                                                 } else if ( failed === false && job.ready ) {
384                                                         job.ready();
385                                                 }
386                                         } catch ( e ) {
387                                                 // A user-defined callback raised an exception.
388                                                 // Swallow it to protect our state machine!
389                                                 mw.trackError( {
390                                                         exception: e,
391                                                         source: 'load-callback'
392                                                 } );
393                                         }
394                                         didPropagate = true;
395                                 }
396                         }
397                 }
399                 willPropagate = false;
400         }
402         /**
403          * Update a module's state in the registry and make sure any necessary
404          * propagation will occur, by adding a (debounced) call to doPropagation().
405          * See #doPropagation for more about propagation.
406          * See #registry for more about how states are used.
407          *
408          * @private
409          * @param {string} module
410          * @param {string} state
411          */
412         function setAndPropagate( module, state ) {
413                 registry[ module ].state = state;
414                 if ( state === 'ready' ) {
415                         // Queue to later be synced to the local module store.
416                         store.add( module );
417                 } else if ( state === 'error' || state === 'missing' ) {
418                         errorModules.push( module );
419                 } else if ( state !== 'loaded' ) {
420                         // We only have something to do in doPropagation for the
421                         // 'loaded', 'ready', 'error', and 'missing' states.
422                         // Avoid scheduling and propagation cost for frequent and short-lived
423                         // transition states, such as 'loading' and 'executing'.
424                         return;
425                 }
426                 if ( willPropagate ) {
427                         // Already scheduled, or, we're already in a doPropagation stack.
428                         return;
429                 }
430                 willPropagate = true;
431                 // Yield for two reasons:
432                 // * Allow successive calls to mw.loader.impl() from the same
433                 //   load.php response, or from the same asyncEval() to be in the
434                 //   propagation batch.
435                 // * Allow the browser to breathe between the reception of
436                 //   module source code and the execution of it.
437                 //
438                 // Use a high priority because the user may be waiting for interactions
439                 // to start being possible. But, first provide a moment (up to 'timeout')
440                 // for native input event handling (e.g. scrolling/typing/clicking).
441                 mw.requestIdleCallback( doPropagation, { timeout: 1 } );
442         }
444         /**
445          * Resolve dependencies and detect circular references.
446          *
447          * @private
448          * @param {string} module Name of the top-level module whose dependencies shall be
449          *  resolved and sorted.
450          * @param {Array} resolved Returns a topological sort of the given module and its
451          *  dependencies, such that later modules depend on earlier modules. The array
452          *  contains the module names. If the array contains already some module names,
453          *  this function appends its result to the pre-existing array.
454          * @param {Set} [unresolved] Used to detect loops in the dependency graph.
455          * @throws {Error} If an unknown module or a circular dependency is encountered
456          */
457         function sortDependencies( module, resolved, unresolved ) {
458                 if ( !( module in registry ) ) {
459                         throw new Error( 'Unknown module: ' + module );
460                 }
462                 if ( typeof registry[ module ].skip === 'string' ) {
463                         // eslint-disable-next-line no-new-func
464                         var skip = ( new Function( registry[ module ].skip )() );
465                         registry[ module ].skip = !!skip;
466                         if ( skip ) {
467                                 registry[ module ].dependencies = [];
468                                 setAndPropagate( module, 'ready' );
469                                 return;
470                         }
471                 }
473                 // Create unresolved if not passed in
474                 if ( !unresolved ) {
475                         unresolved = new Set();
476                 }
478                 // Track down dependencies
479                 var deps = registry[ module ].dependencies;
480                 unresolved.add( module );
481                 for ( var i = 0; i < deps.length; i++ ) {
482                         if ( resolved.indexOf( deps[ i ] ) === -1 ) {
483                                 if ( unresolved.has( deps[ i ] ) ) {
484                                         throw new Error(
485                                                 'Circular reference detected: ' + module + ' -> ' + deps[ i ]
486                                         );
487                                 }
489                                 sortDependencies( deps[ i ], resolved, unresolved );
490                         }
491                 }
493                 resolved.push( module );
494         }
496         /**
497          * Get names of module that a module depends on, in their proper dependency order.
498          *
499          * @private
500          * @param {string[]} modules Array of string module names
501          * @return {Array} List of dependencies, including 'module'.
502          * @throws {Error} If an unregistered module or a dependency loop is encountered
503          */
504         function resolve( modules ) {
505                 // Always load base modules
506                 var resolved = baseModules.slice();
507                 for ( var i = 0; i < modules.length; i++ ) {
508                         sortDependencies( modules[ i ], resolved );
509                 }
510                 return resolved;
511         }
513         /**
514          * Like #resolve(), except it will silently ignore modules that
515          * are missing or have missing dependencies.
516          *
517          * @private
518          * @param {string[]} modules Array of string module names
519          * @return {Array} List of dependencies.
520          */
521         function resolveStubbornly( modules ) {
522                 // Always load base modules
523                 var resolved = baseModules.slice();
524                 for ( var i = 0; i < modules.length; i++ ) {
525                         var saved = resolved.slice();
526                         try {
527                                 sortDependencies( modules[ i ], resolved );
528                         } catch ( err ) {
529                                 resolved = saved;
530                                 // This module is not currently known, or has invalid dependencies.
531                                 //
532                                 // Most likely due to a cached reference after the module was
533                                 // removed, otherwise made redundant, or omitted from the registry
534                                 // by the ResourceLoader "target" system.
535                                 //
536                                 // These errors can be common, e.g. queuing an unavailable module
537                                 // unconditionally from the server-side is OK and should fail gracefully.
538                                 mw.log.warn( 'Skipped unavailable module ' + modules[ i ] );
540                                 // Do not track this error as an exception when the module:
541                                 // - Is valid, but gracefully filtered out by target system.
542                                 // - Was recently valid, but is still referenced in stale cache.
543                                 //
544                                 // Basically the only reason to track this as exception is when the error
545                                 // was circular or invalid dependencies. What the above scenarios have in
546                                 // common is that they don't register the module client-side.
547                                 if ( modules[ i ] in registry ) {
548                                         mw.trackError( {
549                                                 exception: err,
550                                                 source: 'resolve'
551                                         } );
552                                 }
553                         }
554                 }
555                 return resolved;
556         }
558         /**
559          * Resolve a relative file path.
560          *
561          * For example, resolveRelativePath( '../foo.js', 'resources/src/bar/bar.js' )
562          * returns 'resources/src/foo.js'.
563          *
564          * @private
565          * @param {string} relativePath Relative file path, starting with ./ or ../
566          * @param {string} basePath Path of the file (not directory) relativePath is relative to
567          * @return {string|null} Resolved path, or null if relativePath does not start with ./ or ../
568          */
569         function resolveRelativePath( relativePath, basePath ) {
571                 var relParts = relativePath.match( /^((?:\.\.?\/)+)(.*)$/ );
572                 if ( !relParts ) {
573                         return null;
574                 }
576                 var baseDirParts = basePath.split( '/' );
577                 // basePath looks like 'foo/bar/baz.js', so baseDirParts looks like [ 'foo', 'bar, 'baz.js' ]
578                 // Remove the file component at the end, so that we are left with only the directory path
579                 baseDirParts.pop();
581                 var prefixes = relParts[ 1 ].split( '/' );
582                 // relParts[ 1 ] looks like '../../', so prefixes looks like [ '..', '..', '' ]
583                 // Remove the empty element at the end
584                 prefixes.pop();
586                 // For every ../ in the path prefix, remove one directory level from baseDirParts
587                 var prefix;
588                 var reachedRoot = false;
589                 while ( ( prefix = prefixes.pop() ) !== undefined ) {
590                         if ( prefix === '..' ) {
591                                 // Once we reach the package's base dir, preserve all remaining "..".
592                                 reachedRoot = !baseDirParts.length || reachedRoot;
593                                 if ( !reachedRoot ) {
594                                         baseDirParts.pop();
595                                 } else {
596                                         baseDirParts.push( prefix );
597                                 }
598                         }
599                 }
601                 // If there's anything left of the base path, prepend it to the file path
602                 return ( baseDirParts.length ? baseDirParts.join( '/' ) + '/' : '' ) + relParts[ 2 ];
603         }
605         /**
606          * Make a require() function scoped to a package file
607          *
608          * @private
609          * @param {Object} moduleObj Module object from the registry
610          * @param {string} basePath Path of the file this is scoped to. Used for relative paths.
611          * @return {Function}
612          */
613         function makeRequireFunction( moduleObj, basePath ) {
614                 return function require( moduleName ) {
615                         var fileName = resolveRelativePath( moduleName, basePath );
616                         if ( fileName === null ) {
617                                 // Not a relative path, so it's either a module name or,
618                                 // (if in test mode) a private file imported from another module.
619                                 return mw.loader.require( moduleName );
620                         }
622                         if ( hasOwn.call( moduleObj.packageExports, fileName ) ) {
623                                 // File has already been executed, return the cached result
624                                 return moduleObj.packageExports[ fileName ];
625                         }
627                         var scriptFiles = moduleObj.script.files;
628                         if ( !hasOwn.call( scriptFiles, fileName ) ) {
629                                 throw new Error( 'Cannot require undefined file ' + fileName );
630                         }
632                         var result,
633                                 fileContent = scriptFiles[ fileName ];
634                         if ( typeof fileContent === 'function' ) {
635                                 var moduleParam = { exports: {} };
636                                 fileContent( makeRequireFunction( moduleObj, fileName ), moduleParam, moduleParam.exports );
637                                 result = moduleParam.exports;
638                         } else {
639                                 // fileContent is raw data (such as a JSON object), just pass it through
640                                 result = fileContent;
641                         }
642                         moduleObj.packageExports[ fileName ] = result;
643                         return result;
644                 };
645         }
647         /**
648          * Load and execute a script.
649          *
650          * @private
651          * @param {string} src URL to script, will be used as the src attribute in the script tag
652          * @param {Function} [callback] Callback to run after request resolution
653          * @param {string[]} [modules] List of modules being requested, for state to be marked as error
654          * in case the script fails to load
655          * @return {HTMLElement}
656          */
657         function addScript( src, callback, modules ) {
658                 // Use a <script> element rather than XHR. Using XHR changes the request
659                 // headers (potentially missing a cache hit), and reduces caching in general
660                 // since browsers cache XHR much less (if at all). And XHR means we retrieve
661                 // text, so we'd need to eval, which then messes up line numbers.
662                 // The drawback is that <script> does not offer progress events, feedback is
663                 // only given after downloading, parsing, and execution have completed.
664                 var script = document.createElement( 'script' );
665                 script.src = src;
666                 function onComplete() {
667                         if ( script.parentNode ) {
668                                 script.parentNode.removeChild( script );
669                         }
670                         if ( callback ) {
671                                 callback();
672                                 callback = null;
673                         }
674                 }
675                 script.onload = onComplete;
676                 script.onerror = function () {
677                         onComplete();
678                         if ( modules ) {
679                                 for ( var i = 0; i < modules.length; i++ ) {
680                                         setAndPropagate( modules[ i ], 'error' );
681                                 }
682                         }
683                 };
684                 document.head.appendChild( script );
685                 return script;
686         }
688         /**
689          * Queue the loading and execution of a script for a particular module.
690          *
691          * This does for legacy debug mode what runScript() does for production.
692          *
693          * @private
694          * @param {string} src URL of the script
695          * @param {string} moduleName Name of currently executing module
696          * @param {Function} callback Callback to run after addScript() resolution
697          */
698         function queueModuleScript( src, moduleName, callback ) {
699                 pendingRequests.push( function () {
700                         // Keep in sync with execute()/runScript().
701                         if ( moduleName !== 'jquery' ) {
702                                 window.require = mw.loader.require;
703                                 window.module = registry[ moduleName ].module;
704                         }
705                         addScript( src, function () {
706                                 // 'module.exports' should not persist after the file is executed to
707                                 // avoid leakage to unrelated code. 'require' should be kept, however,
708                                 // as asynchronous access to 'require' is allowed and expected. (T144879)
709                                 delete window.module;
710                                 callback();
711                                 // Start the next one (if any)
712                                 if ( pendingRequests[ 0 ] ) {
713                                         pendingRequests.shift()();
714                                 } else {
715                                         handlingPendingRequests = false;
716                                 }
717                         } );
718                 } );
719                 if ( !handlingPendingRequests && pendingRequests[ 0 ] ) {
720                         handlingPendingRequests = true;
721                         pendingRequests.shift()();
722                 }
723         }
725         /**
726          * Utility function for execute()
727          *
728          * @ignore
729          * @param {string} url URL
730          * @param {string} [media] Media attribute
731          * @param {Node|null} [nextNode]
732          * @return {HTMLElement}
733          */
734         function addLink( url, media, nextNode ) {
735                 var el = document.createElement( 'link' );
737                 el.rel = 'stylesheet';
738                 if ( media ) {
739                         el.media = media;
740                 }
741                 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
742                 // see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
743                 el.href = url;
745                 addToHead( el, nextNode );
746                 return el;
747         }
749         /**
750          * Evaluate in the global scope.
751          *
752          * This is used by MediaWiki user scripts, where it is (for example)
753          * important that `var` makes a global variable.
754          *
755          * @private
756          * @param {string} code JavaScript code
757          */
758         function globalEval( code ) {
759                 var script = document.createElement( 'script' );
760                 script.text = code;
761                 document.head.appendChild( script );
762                 script.parentNode.removeChild( script );
763         }
765         /**
766          * Evaluate JS code using indirect eval().
767          *
768          * This is used by mw.loader.store. It is important that we protect the
769          * integrity of mw.loader's private variables (from accidental clashes
770          * or re-assignment), which means we can't use regular `eval()`.
771          *
772          * Optimization: This exists separately from globalEval(), because that
773          * involves slow DOM overhead.
774          *
775          * @private
776          * @param {string} code JavaScript code
777          */
778         function indirectEval( code ) {
779                 // See http://perfectionkills.com/global-eval-what-are-the-options/
780                 // for an explanation of this syntax.
781                 // eslint-disable-next-line no-eval
782                 ( 1, eval )( code );
783         }
785         /**
786          * Add one or more modules to the module load queue.
787          *
788          * See also #work().
789          *
790          * @private
791          * @param {string[]} dependencies Array of module names in the registry
792          * @param {Function} [ready] Callback to execute when all dependencies are ready
793          * @param {Function} [error] Callback to execute when any dependency fails
794          */
795         function enqueue( dependencies, ready, error ) {
796                 if ( allReady( dependencies ) ) {
797                         // Run ready immediately
798                         if ( ready ) {
799                                 ready();
800                         }
801                         return;
802                 }
804                 var failed = anyFailed( dependencies );
805                 if ( failed !== false ) {
806                         if ( error ) {
807                                 // Execute error immediately if any dependencies have errors
808                                 error(
809                                         new Error( 'Dependency ' + failed + ' failed to load' ),
810                                         dependencies
811                                 );
812                         }
813                         return;
814                 }
816                 // Not all dependencies are ready, add to the load queue...
818                 // Add ready and error callbacks if they were given
819                 if ( ready || error ) {
820                         jobs.push( {
821                                 // Narrow down the list to modules that are worth waiting for
822                                 dependencies: dependencies.filter( function ( module ) {
823                                         var state = registry[ module ].state;
824                                         return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
825                                 } ),
826                                 ready: ready,
827                                 error: error
828                         } );
829                 }
831                 dependencies.forEach( function ( module ) {
832                         // Only queue modules that are still in the initial 'registered' state
833                         // (e.g. not ones already loading or loaded etc.).
834                         if ( registry[ module ].state === 'registered' && queue.indexOf( module ) === -1 ) {
835                                 queue.push( module );
836                         }
837                 } );
839                 mw.loader.work();
840         }
842         /**
843          * Executes a loaded module, making it ready to use
844          *
845          * @private
846          * @param {string} module Module name to execute
847          */
848         function execute( module ) {
849                 if ( registry[ module ].state !== 'loaded' ) {
850                         throw new Error( 'Module in state "' + registry[ module ].state + '" may not execute: ' + module );
851                 }
853                 registry[ module ].state = 'executing';
854                 $CODE.profileExecuteStart();
856                 var runScript = function () {
857                         $CODE.profileScriptStart();
858                         var script = registry[ module ].script;
859                         var markModuleReady = function () {
860                                 $CODE.profileScriptEnd();
861                                 setAndPropagate( module, 'ready' );
862                         };
863                         var nestedAddScript = function ( arr, offset ) {
864                                 // Recursively call queueModuleScript() in its own callback
865                                 // for each element of arr.
866                                 if ( offset >= arr.length ) {
867                                         // We're at the end of the array
868                                         markModuleReady();
869                                         return;
870                                 }
872                                 queueModuleScript( arr[ offset ], module, function () {
873                                         nestedAddScript( arr, offset + 1 );
874                                 } );
875                         };
877                         try {
878                                 if ( Array.isArray( script ) ) {
879                                         nestedAddScript( script, 0 );
880                                 } else if ( typeof script === 'function' ) {
881                                         // Keep in sync with queueModuleScript() for debug mode
882                                         if ( module === 'jquery' ) {
883                                                 // This is a special case for when 'jquery' itself is being loaded.
884                                                 // - The standard jquery.js distribution does not set `window.jQuery`
885                                                 //   in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
886                                                 // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
887                                                 //   in a CommonJS-compatible environment, will use require('jquery'),
888                                                 //   but that can't work when we're still inside that module.
889                                                 script();
890                                         } else {
891                                                 // Pass jQuery twice so that the signature of the closure which wraps
892                                                 // the script can bind both '$' and 'jQuery'.
893                                                 script( window.$, window.$, mw.loader.require, registry[ module ].module );
894                                         }
895                                         markModuleReady();
896                                 } else if ( typeof script === 'object' && script !== null ) {
897                                         var mainScript = script.files[ script.main ];
898                                         if ( typeof mainScript !== 'function' ) {
899                                                 throw new Error( 'Main file in module ' + module + ' must be a function' );
900                                         }
901                                         // jQuery parameters are not passed for multi-file modules
902                                         mainScript(
903                                                 makeRequireFunction( registry[ module ], script.main ),
904                                                 registry[ module ].module,
905                                                 registry[ module ].module.exports
906                                         );
907                                         markModuleReady();
908                                 } else if ( typeof script === 'string' ) {
909                                         // Site and user modules are legacy scripts that run in the global scope.
910                                         // This is transported as a string instead of a function to avoid needing
911                                         // to use string manipulation to undo the function wrapper.
912                                         globalEval( script );
913                                         markModuleReady();
915                                 } else {
916                                         // Module without script
917                                         markModuleReady();
918                                 }
919                         } catch ( e ) {
920                                 // Use mw.trackError instead of mw.log because these errors are common in production mode
921                                 // (e.g. undefined variable), and mw.log is only enabled in debug mode.
922                                 setAndPropagate( module, 'error' );
923                                 $CODE.profileScriptEnd();
924                                 mw.trackError( {
925                                         exception: e,
926                                         module: module,
927                                         source: 'module-execute'
928                                 } );
929                         }
930                 };
932                 // Emit deprecation warnings
933                 if ( registry[ module ].deprecationWarning ) {
934                         mw.log.warn( registry[ module ].deprecationWarning );
935                 }
937                 // Add localizations to message system
938                 if ( registry[ module ].messages ) {
939                         mw.messages.set( registry[ module ].messages );
940                 }
942                 // Initialise templates
943                 if ( registry[ module ].templates ) {
944                         mw.templates.set( module, registry[ module ].templates );
945                 }
947                 // Adding of stylesheets is asynchronous via addEmbeddedCSS().
948                 // The below function uses a counting semaphore to make sure we don't call
949                 // runScript() until after this module's stylesheets have been inserted
950                 // into the DOM.
951                 var cssPending = 0;
952                 var cssHandle = function () {
953                         // Increase semaphore, when creating a callback for addEmbeddedCSS.
954                         cssPending++;
955                         return function () {
956                                 // Decrease semaphore, when said callback is invoked.
957                                 cssPending--;
958                                 if ( cssPending === 0 ) {
959                                         // Paranoia:
960                                         // This callback is exposed to addEmbeddedCSS, which is outside the execute()
961                                         // function and is not concerned with state-machine integrity. In turn,
962                                         // addEmbeddedCSS() actually exposes stuff further via requestAnimationFrame.
963                                         // If increment and decrement callbacks happen in the wrong order, or start
964                                         // again afterwards, then this branch could be reached multiple times.
965                                         // To protect the integrity of the state-machine, prevent that from happening
966                                         // by making runScript() cannot be called more than once.  We store a private
967                                         // reference when we first reach this branch, then deference the original, and
968                                         // call our reference to it.
969                                         var runScriptCopy = runScript;
970                                         runScript = undefined;
971                                         runScriptCopy();
972                                 }
973                         };
974                 };
976                 // Process styles (see also mw.loader.impl)
977                 // * { "css": [css, ..] }
978                 // * { "url": { <media>: [url, ..] } }
979                 var style = registry[ module ].style;
980                 if ( style ) {
981                         // Array of CSS strings under key 'css'
982                         // { "css": [css, ..] }
983                         if ( 'css' in style ) {
984                                 for ( var i = 0; i < style.css.length; i++ ) {
985                                         addEmbeddedCSS( style.css[ i ], cssHandle() );
986                                 }
987                         }
989                         // Plain object with array of urls under a media-type key
990                         // { "url": { <media>: [url, ..] } }
991                         if ( 'url' in style ) {
992                                 for ( var media in style.url ) {
993                                         var urls = style.url[ media ];
994                                         for ( var j = 0; j < urls.length; j++ ) {
995                                                 addLink( urls[ j ], media, marker );
996                                         }
997                                 }
998                         }
999                 }
1001                 // End profiling of execute()-self before we call runScript(),
1002                 // which we want to measure separately without overlap.
1003                 $CODE.profileExecuteEnd();
1005                 if ( module === 'user' ) {
1006                         // Implicit dependency on the site module. Not a real dependency because it should
1007                         // run after 'site' regardless of whether it succeeds or fails.
1008                         // Note: This is a simplified version of mw.loader.using(), inlined here because
1009                         // mw.loader.using() is part of mediawiki.base (depends on jQuery; T192623).
1010                         var siteDeps;
1011                         var siteDepErr;
1012                         try {
1013                                 siteDeps = resolve( [ 'site' ] );
1014                         } catch ( e ) {
1015                                 siteDepErr = e;
1016                                 runScript();
1017                         }
1018                         if ( !siteDepErr ) {
1019                                 enqueue( siteDeps, runScript, runScript );
1020                         }
1021                 } else if ( cssPending === 0 ) {
1022                         // Regular module without styles
1023                         runScript();
1024                 }
1025                 // else: runScript will get called via cssHandle()
1026         }
1028         function sortQuery( o ) {
1029                 var sorted = {};
1030                 var list = [];
1032                 for ( var key in o ) {
1033                         list.push( key );
1034                 }
1035                 list.sort();
1036                 for ( var i = 0; i < list.length; i++ ) {
1037                         sorted[ list[ i ] ] = o[ list[ i ] ];
1038                 }
1039                 return sorted;
1040         }
1042         /**
1043          * Converts a module map of the form `{ foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }`
1044          * to a query string of the form `foo.bar,baz|bar.baz,quux`.
1045          *
1046          * See `ResourceLoader::makePackedModulesString()` in PHP, of which this is a port.
1047          * On the server, unpacking is done by `ResourceLoader::expandModuleNames()`.
1048          *
1049          * Note: This is only half of the logic, the other half has to be in #batchRequest(),
1050          * because its implementation needs to keep track of potential string size in order
1051          * to decide when to split the requests due to url size.
1052          *
1053          * @typedef {Object} ModuleString
1054          * @property {string} str Module query string
1055          * @property {Array} list List of module names in matching order
1056          *
1057          * @private
1058          * @param {Object} moduleMap Module map
1059          * @return {ModuleString}
1060          */
1061         function buildModulesString( moduleMap ) {
1062                 var str = [];
1063                 var list = [];
1064                 var p;
1066                 function restore( suffix ) {
1067                         return p + suffix;
1068                 }
1070                 for ( var prefix in moduleMap ) {
1071                         p = prefix === '' ? '' : prefix + '.';
1072                         str.push( p + moduleMap[ prefix ].join( ',' ) );
1073                         list.push.apply( list, moduleMap[ prefix ].map( restore ) );
1074                 }
1075                 return {
1076                         str: str.join( '|' ),
1077                         list: list
1078                 };
1079         }
1081         /**
1082          * @private
1083          * @param {Object} params Map of parameter names to values
1084          * @return {string}
1085          */
1086         function makeQueryString( params ) {
1087                 // Optimisation: This is a fairly hot code path with batchRequest() loops.
1088                 // Avoid overhead from Object.keys and Array.forEach.
1089                 // String concatenation is faster than array pushing and joining, see
1090                 // https://phabricator.wikimedia.org/P19931
1091                 var str = '';
1092                 for ( var key in params ) {
1093                         // Parameters are separated by &, added before all parameters other than
1094                         // the first
1095                         str += ( str ? '&' : '' ) + encodeURIComponent( key ) + '=' +
1096                                 encodeURIComponent( params[ key ] );
1097                 }
1098                 return str;
1099         }
1101         /**
1102          * Create network requests for a batch of modules.
1103          *
1104          * This is an internal method for #work(). This must not be called directly
1105          * unless the modules are already registered, and no request is in progress,
1106          * and the module state has already been set to `loading`.
1107          *
1108          * @private
1109          * @param {string[]} batch
1110          */
1111         function batchRequest( batch ) {
1112                 if ( !batch.length ) {
1113                         return;
1114                 }
1116                 var sourceLoadScript, currReqBase, moduleMap;
1118                 /**
1119                  * Start the currently drafted request to the server.
1120                  *
1121                  * @ignore
1122                  */
1123                 function doRequest() {
1124                         // Optimisation: Inherit (Object.create), not copy ($.extend)
1125                         var query = Object.create( currReqBase ),
1126                                 packed = buildModulesString( moduleMap );
1127                         query.modules = packed.str;
1128                         // The packing logic can change the effective order, even if the input was
1129                         // sorted. As such, the call to getCombinedVersion() must use this
1130                         // effective order to ensure that the combined version will match the hash
1131                         // expected by the server based on combining versions from the module
1132                         // query string in-order. (T188076)
1133                         query.version = getCombinedVersion( packed.list );
1134                         query = sortQuery( query );
1135                         addScript( sourceLoadScript + '?' + makeQueryString( query ), null, packed.list );
1136                 }
1138                 // Always order modules alphabetically to help reduce cache
1139                 // misses for otherwise identical content.
1140                 batch.sort();
1142                 // Query parameters common to all requests
1143                 var reqBase = $VARS.reqBase;
1145                 // Split module list by source and by group.
1146                 var splits = Object.create( null );
1147                 for ( var b = 0; b < batch.length; b++ ) {
1148                         var bSource = registry[ batch[ b ] ].source;
1149                         var bGroup = registry[ batch[ b ] ].group;
1150                         if ( !splits[ bSource ] ) {
1151                                 splits[ bSource ] = Object.create( null );
1152                         }
1153                         if ( !splits[ bSource ][ bGroup ] ) {
1154                                 splits[ bSource ][ bGroup ] = [];
1155                         }
1156                         splits[ bSource ][ bGroup ].push( batch[ b ] );
1157                 }
1159                 for ( var source in splits ) {
1160                         sourceLoadScript = sources[ source ];
1162                         for ( var group in splits[ source ] ) {
1164                                 // Cache access to currently selected list of
1165                                 // modules for this group from this source.
1166                                 var modules = splits[ source ][ group ];
1168                                 // Query parameters common to requests for this module group
1169                                 // Optimisation: Inherit (Object.create), not copy ($.extend)
1170                                 currReqBase = Object.create( reqBase );
1171                                 // User modules require a user name in the query string.
1172                                 if ( group === $VARS.groupUser && mw.config.get( 'wgUserName' ) !== null ) {
1173                                         currReqBase.user = mw.config.get( 'wgUserName' );
1174                                 }
1176                                 // In addition to currReqBase, doRequest() will also add 'modules' and 'version'.
1177                                 // > '&modules='.length === 9
1178                                 // > '&version=12345'.length === 14
1179                                 // > 9 + 14 = 23
1180                                 var currReqBaseLength = makeQueryString( currReqBase ).length + 23;
1182                                 // We may need to split up the request to honor the query string length limit,
1183                                 // so build it piece by piece. `length` does not include the characters from
1184                                 // the request base, see below
1185                                 var length = 0;
1186                                 moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
1188                                 for ( var i = 0; i < modules.length; i++ ) {
1189                                         // Determine how many bytes this module would add to the query string
1190                                         var lastDotIndex = modules[ i ].lastIndexOf( '.' ),
1191                                                 prefix = modules[ i ].slice( 0, Math.max( 0, lastDotIndex ) ),
1192                                                 suffix = modules[ i ].slice( lastDotIndex + 1 ),
1193                                                 bytesAdded = moduleMap[ prefix ] ?
1194                                                         suffix.length + 3 : // '%2C'.length == 3
1195                                                         modules[ i ].length + 3; // '%7C'.length == 3
1197                                         // If the url would become too long, create a new one, but don't create empty requests.
1198                                         // The value of `length` only reflects the request-specific bytes relating to the
1199                                         // accumulated entries in moduleMap so far. It does not include the base length,
1200                                         // which we account for separately with `currReqBaseLength` so that length is 0
1201                                         // when moduleMap is empty.
1202                                         if ( length && length + currReqBaseLength + bytesAdded > mw.loader.maxQueryLength ) {
1203                                                 // Dispatch what we've got...
1204                                                 doRequest();
1205                                                 // .. and start preparing a new request.
1206                                                 length = 0;
1207                                                 moduleMap = Object.create( null );
1208                                         }
1209                                         if ( !moduleMap[ prefix ] ) {
1210                                                 moduleMap[ prefix ] = [];
1211                                         }
1212                                         length += bytesAdded;
1213                                         moduleMap[ prefix ].push( suffix );
1214                                 }
1215                                 // Optimization: Skip `length` check.
1216                                 // moduleMap will contain at least one module here. The loop above leaves the last module
1217                                 // undispatched (and maybe some before it), so for moduleMap to be empty here, there must
1218                                 // have been no modules to iterate in the current group to start with, but we only create
1219                                 // a group in `splits` when the first module in the group is seen, so there are always
1220                                 // modules in the group when this code is reached.
1221                                 doRequest();
1222                         }
1223                 }
1224         }
1226         /**
1227          * @private
1228          * @param {string[]} implementations Array containing pieces of JavaScript code in the
1229          *  form of calls to mw.loader#impl().
1230          * @param {Function} cb Callback in case of failure
1231          * @param {Error} cb.err
1232          * @param {number} [offset] Integer offset into implementations to start at
1233          */
1234         function asyncEval( implementations, cb, offset ) {
1235                 if ( !implementations.length ) {
1236                         return;
1237                 }
1238                 offset = offset || 0;
1239                 mw.requestIdleCallback( function ( deadline ) {
1240                         asyncEvalTask( deadline, implementations, cb, offset );
1241                 } );
1242         }
1244         /**
1245          * Idle callback for asyncEval
1246          *
1247          * @private
1248          * @param {IdleDeadline} deadline
1249          * @param {string[]} implementations
1250          * @param {Function} cb
1251          * @param {Error} cb.err
1252          * @param {number} offset
1253          */
1254         function asyncEvalTask( deadline, implementations, cb, offset ) {
1255                 for ( var i = offset; i < implementations.length; i++ ) {
1256                         if ( deadline.timeRemaining() <= 0 ) {
1257                                 asyncEval( implementations, cb, i );
1258                                 return;
1259                         }
1260                         try {
1261                                 indirectEval( implementations[ i ] );
1262                         } catch ( err ) {
1263                                 cb( err );
1264                         }
1265                 }
1266         }
1268         /**
1269          * Make a versioned key for a specific module.
1270          *
1271          * @private
1272          * @param {string} module Module name
1273          * @return {string|null} Module key in format '`[name]@[version]`',
1274          *  or null if the module does not exist
1275          */
1276         function getModuleKey( module ) {
1277                 return module in registry ? ( module + '@' + registry[ module ].version ) : null;
1278         }
1280         /**
1281          * @private
1282          * @param {string} key Module name or '`[name]@[version]`'
1283          * @return {Object}
1284          */
1285         function splitModuleKey( key ) {
1286                 // Module names may contain '@' but version strings may not, so the last '@' is the delimiter
1287                 var index = key.lastIndexOf( '@' );
1288                 // If the key doesn't contain '@' or starts with it, the whole thing is the module name
1289                 if ( index === -1 || index === 0 ) {
1290                         return {
1291                                 name: key,
1292                                 version: ''
1293                         };
1294                 }
1295                 return {
1296                         name: key.slice( 0, index ),
1297                         version: key.slice( index + 1 )
1298                 };
1299         }
1301         /**
1302          * @private
1303          * @param {string} module
1304          * @param {string} [version]
1305          * @param {string[]} [dependencies]
1306          * @param {string} [group]
1307          * @param {string} [source]
1308          * @param {string} [skip]
1309          */
1310         function registerOne( module, version, dependencies, group, source, skip ) {
1311                 if ( module in registry ) {
1312                         throw new Error( 'module already registered: ' + module );
1313                 }
1315                 registry[ module ] = {
1316                         // Exposed to execute() for mw.loader.impl() closures.
1317                         // Import happens via require().
1318                         module: {
1319                                 exports: {}
1320                         },
1321                         // module.export objects for each package file inside this module
1322                         packageExports: {},
1323                         version: version || '',
1324                         dependencies: dependencies || [],
1325                         group: typeof group === 'undefined' ? null : group,
1326                         source: typeof source === 'string' ? source : 'local',
1327                         state: 'registered',
1328                         skip: typeof skip === 'string' ? skip : null
1329                 };
1330         }
1332         /* Public Members */
1334         mw.loader = {
1335                 /**
1336                  * The module registry is exposed as an aid for debugging and inspecting page
1337                  * state; it is not a public interface for modifying the registry.
1338                  *
1339                  * @see #registry
1340                  * @property {Object}
1341                  * @private
1342                  */
1343                 moduleRegistry: registry,
1345                 /**
1346                  * Exposed for testing and debugging only.
1347                  *
1348                  * @see #batchRequest
1349                  * @property {number}
1350                  * @private
1351                  */
1352                 maxQueryLength: $VARS.maxQueryLength,
1354                 addStyleTag: newStyleTag,
1356                 // Exposed for internal use only. Documented as @private.
1357                 addScriptTag: addScript,
1358                 // Exposed for internal use only. Documented as @private.
1359                 addLinkTag: addLink,
1361                 // Exposed for internal use only. Documented as @private.
1362                 enqueue: enqueue,
1364                 // Exposed for internal use only. Documented as @private.
1365                 resolve: resolve,
1367                 /**
1368                  * Start loading of all queued module dependencies.
1369                  *
1370                  * @private
1371                  */
1372                 work: function () {
1373                         store.init();
1375                         var q = queue.length,
1376                                 storedImplementations = [],
1377                                 storedNames = [],
1378                                 requestNames = [],
1379                                 batch = new Set();
1381                         // Iterate the list of requested modules, and do one of three things:
1382                         // - 1) Nothing (if already loaded or being loaded).
1383                         // - 2) Eval the cached implementation from the module store.
1384                         // - 3) Request from network.
1385                         while ( q-- ) {
1386                                 var module = queue[ q ];
1387                                 // Only consider modules which are the initial 'registered' state,
1388                                 // and ignore duplicates
1389                                 if ( mw.loader.getState( module ) === 'registered' &&
1390                                         !batch.has( module )
1391                                 ) {
1392                                         // Progress the state machine
1393                                         registry[ module ].state = 'loading';
1394                                         batch.add( module );
1396                                         var implementation = store.get( module );
1397                                         if ( implementation ) {
1398                                                 // Module store enabled and contains this module/version
1399                                                 storedImplementations.push( implementation );
1400                                                 storedNames.push( module );
1401                                         } else {
1402                                                 // Module store disabled or doesn't have this module/version
1403                                                 requestNames.push( module );
1404                                         }
1405                                 }
1406                         }
1408                         // Now that the queue has been processed into a batch, clear the queue.
1409                         // This MUST happen before we initiate any eval or network request. Otherwise,
1410                         // it is possible for a cached script to instantly trigger the same work queue
1411                         // again; all before we've cleared it causing each request to include modules
1412                         // which are already loaded.
1413                         queue = [];
1415                         asyncEval( storedImplementations, function ( err ) {
1416                                 // Not good, the cached mw.loader.impl calls failed! This should
1417                                 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1418                                 // Depending on how corrupt the string is, it is likely that some
1419                                 // modules' impl() succeeded while the ones after the error will
1420                                 // never run and leave their modules in the 'loading' state forever.
1421                                 store.stats.failed++;
1423                                 // Since this is an error not caused by an individual module but by
1424                                 // something that infected the implement call itself, don't take any
1425                                 // risks and clear everything in this cache.
1426                                 store.clear();
1428                                 mw.trackError( {
1429                                         exception: err,
1430                                         source: 'store-eval'
1431                                 } );
1432                                 // For any failed ones, fallback to requesting from network
1433                                 var failed = storedNames.filter( function ( name ) {
1434                                         return registry[ name ].state === 'loading';
1435                                 } );
1436                                 batchRequest( failed );
1437                         } );
1439                         batchRequest( requestNames );
1440                 },
1442                 /**
1443                  * Register a source.
1444                  *
1445                  * The #work() method will use this information to split up requests by source.
1446                  *
1447                  * @example
1448                  * mw.loader.addSource( { mediawikiwiki: 'https://www.mediawiki.org/w/load.php' } );
1449                  *
1450                  * @private
1451                  * @param {Object} ids An object mapping ids to load.php end point urls
1452                  * @throws {Error} If source id is already registered
1453                  */
1454                 addSource: function ( ids ) {
1455                         for ( var id in ids ) {
1456                                 if ( id in sources ) {
1457                                         throw new Error( 'source already registered: ' + id );
1458                                 }
1459                                 sources[ id ] = ids[ id ];
1460                         }
1461                 },
1463                 /**
1464                  * Register a module, letting the system know about it and its properties.
1465                  *
1466                  * The startup module calls this method.
1467                  *
1468                  * When using multiple module registration by passing an array, dependencies that
1469                  * are specified as references to modules within the array will be resolved before
1470                  * the modules are registered.
1471                  *
1472                  * @param {string|Array} modules Module name or array of arrays, each containing
1473                  *  a list of arguments compatible with this method
1474                  * @param {string} [version] Module version hash (falls backs to empty string)
1475                  * @param {string[]} [dependencies] Array of module names on which this module depends.
1476                  * @param {string} [group=null] Group which the module is in
1477                  * @param {string} [source='local'] Name of the source
1478                  * @param {string} [skip=null] Script body of the skip function
1479                  * @private
1480                  */
1481                 register: function ( modules ) {
1482                         if ( typeof modules !== 'object' ) {
1483                                 registerOne.apply( null, arguments );
1484                                 return;
1485                         }
1486                         // Need to resolve indexed dependencies:
1487                         // ResourceLoader uses an optimisation to save space which replaces module
1488                         // names in dependency lists with the index of that module within the
1489                         // array of module registration data if it exists. The benefit is a significant
1490                         // reduction in the data size of the startup module. This loop changes
1491                         // those dependency lists back to arrays of strings.
1492                         function resolveIndex( dep ) {
1493                                 return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1494                         }
1496                         for ( var i = 0; i < modules.length; i++ ) {
1497                                 var deps = modules[ i ][ 2 ];
1498                                 if ( deps ) {
1499                                         for ( var j = 0; j < deps.length; j++ ) {
1500                                                 deps[ j ] = resolveIndex( deps[ j ] );
1501                                         }
1502                                 }
1503                                 // Optimisation: Up to 55% faster.
1504                                 // Typically register() is called exactly once on a page, and with a batch.
1505                                 // See <https://gist.github.com/Krinkle/f06fdb3de62824c6c16f02a0e6ce0e66>
1506                                 // Benchmarks taught us that the code for adding an object to `registry`
1507                                 // should be in a function that has only one signature and does no arguments
1508                                 // manipulation.
1509                                 // JS semantics make it hard to optimise recursion to a different
1510                                 // signature of itself, hence we moved this out.
1511                                 registerOne.apply( null, modules[ i ] );
1512                         }
1513                 },
1515                 /**
1516                  * Implement a module given the components of the module.
1517                  *
1518                  * See #impl for a full description of the parameters.
1519                  *
1520                  * Prior to MW 1.41, this was used internally, but now it is only kept
1521                  * for backwards compatibility.
1522                  *
1523                  * Does not support mw.loader.store caching.
1524                  *
1525                  * @param {string} module
1526                  * @param {Function|Array|string|Object} [script]
1527                  * @param {Object} [style]
1528                  * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1529                  * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1530                  * @param {string|null} [deprecationWarning] Deprecation warning if any
1531                  * @private
1532                  */
1533                 implement: function ( module, script, style, messages, templates, deprecationWarning ) {
1534                         var split = splitModuleKey( module ),
1535                                 name = split.name,
1536                                 version = split.version;
1538                         // Automatically register module
1539                         if ( !( name in registry ) ) {
1540                                 mw.loader.register( name );
1541                         }
1542                         // Check for duplicate implementation
1543                         if ( registry[ name ].script !== undefined ) {
1544                                 throw new Error( 'module already implemented: ' + name );
1545                         }
1546                         registry[ name ].version = version;
1547                         registry[ name ].declarator = null; // not supported
1548                         registry[ name ].script = script;
1549                         registry[ name ].style = style;
1550                         registry[ name ].messages = messages;
1551                         registry[ name ].templates = templates;
1552                         registry[ name ].deprecationWarning = deprecationWarning;
1553                         // The module may already have been marked as erroneous
1554                         if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
1555                                 setAndPropagate( name, 'loaded' );
1556                         }
1557                 },
1559                 /**
1560                  * Implement a module given a function which returns the components of the module
1561                  *
1562                  * @param {Function} declarator
1563                  *
1564                  * The declarator should return an array with the following keys:
1565                  *
1566                  *  - 0. {string} module Name of module and current module version. Formatted
1567                  *    as '`[name]@[version]`". This version should match the requested version
1568                  *    (from #batchRequest and #registry). This avoids race conditions (T117587).
1569                  *
1570                  *  - 1. {Function|Array|string|Object} [script] Module code. This can be a function,
1571                  *    a list of URLs to load via `<script src>`, a string for `globalEval()`, or an
1572                  *    object like {"files": {"foo.js":function, "bar.js": function, ...}, "main": "foo.js"}.
1573                  *    If an object is provided, the main file will be executed immediately, and the other
1574                  *    files will only be executed if loaded via require(). If a function or string is
1575                  *    provided, it will be executed/evaluated immediately. If an array is provided, all
1576                  *    URLs in the array will be loaded immediately, and executed as soon as they arrive.
1577                  *
1578                  *  - 2. {Object} [style] Should follow one of the following patterns:
1579                  *
1580                  *     { "css": [css, ..] }
1581                  *     { "url": { (media): [url, ..] } }
1582                  *
1583                  *    The reason css strings are not concatenated anymore is T33676. We now check
1584                  *    whether it's safe to extend the stylesheet.
1585                  *
1586                  *  - 3. {Object} [messages] List of key/value pairs to be added to mw#messages.
1587                  *  - 4. {Object} [templates] List of key/value pairs to be added to mw#templates.
1588                  *  - 5. {String|null} [deprecationWarning] Deprecation warning if any
1589                  *
1590                  * The declarator must not use any scope variables, since it will be serialized with
1591                  * Function.prototype.toString() and later restored and executed in the global scope.
1592                  *
1593                  * The elements are all optional except the name.
1594                  * @private
1595                  */
1596                 impl: function ( declarator ) {
1597                         var data = declarator(),
1598                                 module = data[ 0 ],
1599                                 script = data[ 1 ] || null,
1600                                 style = data[ 2 ] || null,
1601                                 messages = data[ 3 ] || null,
1602                                 templates = data[ 4 ] || null,
1603                                 deprecationWarning = data[ 5 ] || null,
1604                                 split = splitModuleKey( module ),
1605                                 name = split.name,
1606                                 version = split.version;
1608                         // Automatically register module
1609                         if ( !( name in registry ) ) {
1610                                 mw.loader.register( name );
1611                         }
1612                         // Check for duplicate implementation
1613                         if ( registry[ name ].script !== undefined ) {
1614                                 throw new Error( 'module already implemented: ' + name );
1615                         }
1616                         // Without this reset, if there is a version mismatch between the
1617                         // requested and received module version, then mw.loader.store would
1618                         // cache the response under the requested key. Thus poisoning the cache
1619                         // indefinitely with a stale value. (T117587)
1620                         registry[ name ].version = version;
1621                         // Attach components
1622                         registry[ name ].declarator = declarator;
1623                         registry[ name ].script = script;
1624                         registry[ name ].style = style;
1625                         registry[ name ].messages = messages;
1626                         registry[ name ].templates = templates;
1627                         registry[ name ].deprecationWarning = deprecationWarning;
1628                         // The module may already have been marked as erroneous
1629                         if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
1630                                 setAndPropagate( name, 'loaded' );
1631                         }
1632                 },
1634                 /**
1635                  * Load an external script or one or more modules.
1636                  *
1637                  * This method takes a list of unrelated modules. Use cases:
1638                  *
1639                  * - A web page will be composed of many different widgets. These widgets independently
1640                  *   queue their ResourceLoader modules (`OutputPage::addModules()`). If any of them
1641                  *   have problems, or are no longer known (e.g. cached HTML), the other modules
1642                  *   should still be loaded.
1643                  * - This method is used for preloading, which must not throw. Later code that
1644                  *   calls #using() will handle the error.
1645                  *
1646                  * @param {string|Array} modules Either the name of a module, array of modules,
1647                  *  or a URL of an external script or style
1648                  * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1649                  *  external script or style; acceptable values are "text/css" and
1650                  *  "text/javascript"; if no type is provided, text/javascript is assumed.
1651                  * @throws {Error} If type is invalid
1652                  */
1653                 load: function ( modules, type ) {
1655                         if ( typeof modules === 'string' && /^(https?:)?\/?\//.test( modules ) ) {
1656                                 // Called with a url like so:
1657                                 // - "https://example.org/x.js"
1658                                 // - "http://example.org/x.js"
1659                                 // - "//example.org/x.js"
1660                                 // - "/x.js"
1661                                 if ( type === 'text/css' ) {
1662                                         addLink( modules );
1663                                 } else if ( type === 'text/javascript' || type === undefined ) {
1664                                         addScript( modules );
1665                                 } else {
1666                                         // Unknown type
1667                                         throw new Error( 'Invalid type ' + type );
1668                                 }
1669                         } else {
1670                                 // One or more modules
1671                                 modules = typeof modules === 'string' ? [ modules ] : modules;
1672                                 // Resolve modules into a flat list for internal queuing.
1673                                 // This also filters out unknown modules and modules with
1674                                 // unknown dependencies, allowing the rest to continue. (T36853)
1675                                 // Omit ready and error parameters, we don't have callbacks
1676                                 enqueue( resolveStubbornly( modules ) );
1677                         }
1678                 },
1680                 /**
1681                  * Change the state of one or more modules.
1682                  *
1683                  * @param {Object} states Object of module name/state pairs
1684                  * @private
1685                  */
1686                 state: function ( states ) {
1687                         for ( var module in states ) {
1688                                 if ( !( module in registry ) ) {
1689                                         mw.loader.register( module );
1690                                 }
1691                                 setAndPropagate( module, states[ module ] );
1692                         }
1693                 },
1695                 /**
1696                  * Get the state of a module.
1697                  *
1698                  * Possible states for the public API:
1699                  *
1700                  * - `registered`: The module is available for loading but not yet requested.
1701                  * - `loading`, `loaded`, or `executing`: The module is currently being loaded.
1702                  * - `ready`: The module was successfully and fully loaded.
1703                  * - `error`: The module or one its dependencies has failed to load, e.g. due to
1704                  *    uncaught error from the module's script files.
1705                  * - `missing`: The module was requested but is not defined according to the server.
1706                  *
1707                  * Internal mw.loader state machine:
1708                  *
1709                  * - `registered`:
1710                  *    The module is known to the system but not yet required.
1711                  *    Meta data is stored by `register()`.
1712                  *    Calls to that method are generated server-side by StartupModule.
1713                  * - `loading`:
1714                  *    The module was required through mw.loader (either directly or as dependency of
1715                  *    another module). The client will fetch module contents from mw.loader.store
1716                  *    or from the server. The contents should later be received by `implement()`.
1717                  * - `loaded`:
1718                  *    The module has been received by `implement()`.
1719                  *    Once the module has no more dependencies in-flight, the module will be executed,
1720                  *    controlled via `setAndPropagate()` and `doPropagation()`.
1721                  * - `executing`:
1722                  *    The module is being executed (apply messages and stylesheets, execute scripts)
1723                  *    by `execute()`.
1724                  * - `ready`:
1725                  *    The module has been successfully executed.
1726                  * - `error`:
1727                  *    The module (or one of its dependencies) produced an uncaught error during execution.
1728                  * - `missing`:
1729                  *    The module was registered client-side and requested, but the server denied knowledge
1730                  *    of the module's existence.
1731                  *
1732                  * @param {string} module Name of module
1733                  * @return {string|null} The state, or null if the module (or its state) is not
1734                  *  in the registry.
1735                  */
1736                 getState: function ( module ) {
1737                         return module in registry ? registry[ module ].state : null;
1738                 },
1740                 /**
1741                  * Get the exported value of a module.
1742                  *
1743                  * This static method is publicly exposed for debugging purposes
1744                  * only and must not be used in production code. In production code,
1745                  * please use the dynamically provided `require()` function instead.
1746                  *
1747                  * In case of lazy-loaded modules via mw.loader#using(), the returned
1748                  * Promise provides the function, see #using() for examples.
1749                  *
1750                  * @private
1751                  * @since 1.27
1752                  * @param {string} moduleName Module name
1753                  * @return {any} Exported value
1754                  */
1755                 require: function ( moduleName ) {
1756                         var path;
1757                         if ( window.QUnit ) {
1758                                 // Comply with Node specification
1759                                 // https://nodejs.org/docs/v20.1.0/api/modules.html#all-together
1760                                 //
1761                                 // > Interpret X as a combination of NAME and SUBPATH, where the NAME
1762                                 // > may have a "@scope/" prefix and the subpath begins with a slash (`/`).
1763                                 //
1764                                 // Regex inspired by Node [1], but simplified to suite our purposes
1765                                 // and split in two in order to keep the Regex Star Height under 2,
1766                                 // as per ESLint security/detect-unsafe-regex.
1767                                 //
1768                                 // These patterns match "@scope/module/dir/file.js" and "module/dir/file.js"
1769                                 // respectively. They must not match "module.name" or "@scope/module.name".
1770                                 //
1771                                 // [1] https://github.com/nodejs/node/blob/v20.1.0/lib/internal/modules/cjs/loader.js#L554-L560
1772                                 var paths = moduleName.startsWith( '@' ) ?
1773                                         /^(@[^/]+\/[^/]+)\/(.*)$/.exec( moduleName ) :
1774                                         // eslint-disable-next-line no-mixed-spaces-and-tabs
1775                                                 /^([^/]+)\/(.*)$/.exec( moduleName );
1776                                 if ( paths ) {
1777                                         moduleName = paths[ 1 ];
1778                                         path = paths[ 2 ];
1779                                 }
1780                         }
1782                         // Only ready modules can be required
1783                         if ( mw.loader.getState( moduleName ) !== 'ready' ) {
1784                                 // Module may've forgotten to declare a dependency
1785                                 throw new Error( 'Module "' + moduleName + '" is not loaded' );
1786                         }
1788                         return path ?
1789                                 makeRequireFunction( registry[ moduleName ], '' )( './' + path ) :
1790                                 registry[ moduleName ].module.exports;
1791                 }
1792         };
1794         var hasPendingFlush = false,
1795                 hasPendingWrites = false;
1797         /**
1798          * Actually update the store
1799          *
1800          * @see #requestUpdate
1801          * @private
1802          */
1803         function flushWrites() {
1804                 // Process queued module names, serialise their contents to the in-memory store.
1805                 while ( store.queue.length ) {
1806                         store.set( store.queue.shift() );
1807                 }
1809                 // Optimization: Don't reserialize the entire store and rewrite localStorage,
1810                 // if no module was added or changed.
1811                 if ( hasPendingWrites ) {
1812                         // Remove anything from the in-memory store that came from previous page
1813                         // loads that no longer corresponds with current module names and versions.
1814                         store.prune();
1816                         try {
1817                                 // Replacing the content of the module store might fail if the new
1818                                 // contents would exceed the browser's localStorage size limit. To
1819                                 // avoid clogging the browser with stale data, always remove the old
1820                                 // value before attempting to store a new one.
1821                                 localStorage.removeItem( store.key );
1822                                 localStorage.setItem( store.key, JSON.stringify( {
1823                                         items: store.items,
1824                                         vary: store.vary,
1825                                         // Store with 1e7 ms accuracy (1e4 seconds, or ~ 2.7 hours),
1826                                         // which is enough for the purpose of expiring after ~ 30 days.
1827                                         asOf: Math.ceil( Date.now() / 1e7 )
1828                                 } ) );
1829                         } catch ( e ) {
1830                                 mw.trackError( {
1831                                         exception: e,
1832                                         source: 'store-localstorage-update'
1833                                 } );
1834                         }
1835                 }
1837                 // Let the next call to requestUpdate() create a new timer.
1838                 hasPendingFlush = hasPendingWrites = false;
1839         }
1841         // We use a local variable `store` so that its easier to access, but also need to set
1842         // this in mw.loader so its exported - combine the two
1844         /**
1845          * On browsers that implement the localStorage API, the module store serves as a
1846          * smart complement to the browser cache. Unlike the browser cache, the module store
1847          * can slice a concatenated response from ResourceLoader into its constituent
1848          * modules and cache each of them separately, using each module's versioning scheme
1849          * to determine when the cache should be invalidated.
1850          *
1851          * @private
1852          * @singleton
1853          * @class mw.loader.store
1854          * @ignore
1855          */
1856         mw.loader.store = store = {
1857                 // Whether the store is in use on this page.
1858                 enabled: null,
1860                 // The contents of the store, mapping '[name]@[version]' keys
1861                 // to module implementations.
1862                 items: {},
1864                 // Names of modules to be stored during the next update.
1865                 // See add() and update().
1866                 queue: [],
1868                 // Cache hit stats
1869                 stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
1871                 /**
1872                  * The localStorage key for the entire module store. The key references
1873                  * $wgDBname to prevent clashes between wikis which share a common host.
1874                  *
1875                  * @property {string}
1876                  */
1877                 key: $VARS.storeKey,
1879                 /**
1880                  * A string containing various factors by which the module cache should vary.
1881                  *
1882                  * Defined by ResourceLoader\StartupModule::getStoreVary() in PHP.
1883                  *
1884                  * @property {string}
1885                  */
1886                 vary: $VARS.storeVary,
1888                 /**
1889                  * Initialize the store.
1890                  *
1891                  * Retrieves store from localStorage and (if successfully retrieved) decoding
1892                  * the stored JSON value to a plain object.
1893                  */
1894                 init: function () {
1895                         // Init only once per page
1896                         if ( this.enabled === null ) {
1897                                 this.enabled = false;
1898                                 if ( $VARS.storeEnabled ) {
1899                                         this.load();
1900                                 } else {
1901                                         // Clear any previous store to free up space. (T66721)
1902                                         this.clear();
1903                                 }
1905                         }
1906                 },
1908                 /**
1909                  * Internal helper for init(). Separated for ease of testing.
1910                  */
1911                 load: function () {
1912                         // These are the scenarios to think about:
1913                         //
1914                         // 1. localStorage is disallowed by the browser.
1915                         //    This means `localStorage.getItem` throws.
1916                         //    The store stays disabled.
1917                         //
1918                         // 2. localStorage did not contain our store key.
1919                         //    This usually means the browser has a cold cache for this site,
1920                         //    and thus localStorage.getItem returns null.
1921                         //    The store will be enabled, and `items` starts fresh.
1922                         //
1923                         // 3. localStorage contains parseable data, but it's not usable.
1924                         //    This means the data is too old, or is not valid for mw.loader.store.vary
1925                         //    (e.g. user switched skin or language).
1926                         //    The store will be enabled, and `items` starts fresh.
1927                         //
1928                         // 4. localStorage contains invalid JSON data.
1929                         //    This means the data was corrupted, and `JSON.parse` throws.
1930                         //    The store will be enabled, and `items` starts fresh.
1931                         //
1932                         // 5. localStorage contains valid and usable JSON.
1933                         //    This means we have a warm cache from a previous visit.
1934                         //    The store will be enabled, and `items` starts with the stored data.
1936                         try {
1937                                 var raw = localStorage.getItem( this.key );
1939                                 // If we make it here, localStorage is enabled and available.
1940                                 // The rest of the function may fail, but that only affects what we load from
1941                                 // the cache. We'll still enable the store to allow storing new modules.
1942                                 this.enabled = true;
1944                                 // If getItem returns null, JSON.parse() will cast to string and re-parse, still null.
1945                                 var data = JSON.parse( raw );
1946                                 if ( data &&
1947                                         data.vary === this.vary &&
1948                                         data.items &&
1949                                         // Only use if it's been less than 30 days since the data was written
1950                                         // 30 days = 2,592,000 s = 2,592,000,000 ms = ± 259e7 ms
1951                                         Date.now() < ( data.asOf * 1e7 ) + 259e7
1952                                 ) {
1953                                         // The data is not corrupt, matches our vary context, and has not expired.
1954                                         this.items = data.items;
1955                                 }
1956                         } catch ( e ) {
1957                                 // Ignore error from localStorage or JSON.parse.
1958                                 // Don't print any warning (T195647).
1959                         }
1960                 },
1962                 /**
1963                  * Retrieve a module from the store and update cache hit stats.
1964                  *
1965                  * @param {string} module Module name
1966                  * @return {string|boolean} Module implementation or false if unavailable
1967                  */
1968                 get: function ( module ) {
1969                         if ( this.enabled ) {
1970                                 var key = getModuleKey( module );
1971                                 if ( key in this.items ) {
1972                                         this.stats.hits++;
1973                                         return this.items[ key ];
1974                                 }
1976                                 this.stats.misses++;
1977                         }
1979                         return false;
1980                 },
1982                 /**
1983                  * Queue the name of a module that the next update should consider storing.
1984                  *
1985                  * @since 1.32
1986                  * @param {string} module Module name
1987                  */
1988                 add: function ( module ) {
1989                         if ( this.enabled ) {
1990                                 this.queue.push( module );
1991                                 this.requestUpdate();
1992                         }
1993                 },
1995                 /**
1996                  * Add the contents of the named module to the in-memory store.
1997                  *
1998                  * This method does not guarantee that the module will be stored.
1999                  * Inspection of the module's meta data and size will ultimately decide that.
2000                  *
2001                  * This method is considered internal to mw.loader.store and must only
2002                  * be called if the store is enabled.
2003                  *
2004                  * @private
2005                  * @param {string} module Module name
2006                  */
2007                 set: function ( module ) {
2008                         var descriptor = registry[ module ],
2009                                 key = getModuleKey( module );
2011                         if (
2012                                 // Already stored a copy of this exact version
2013                                 key in this.items ||
2014                                 // Module failed to load
2015                                 !descriptor ||
2016                                 descriptor.state !== 'ready' ||
2017                                 // Unversioned, private, or site-/user-specific
2018                                 !descriptor.version ||
2019                                 descriptor.group === $VARS.groupPrivate ||
2020                                 descriptor.group === $VARS.groupUser ||
2021                                 // Legacy descriptor, registered with mw.loader.implement
2022                                 !descriptor.declarator
2023                         ) {
2024                                 // Decline to store
2025                                 return;
2026                         }
2028                         var script = String( descriptor.declarator );
2029                         // Modules whose serialised form exceeds 100 kB won't be stored (T66721).
2030                         if ( script.length > 1e5 ) {
2031                                 return;
2032                         }
2034                         var srcParts = [
2035                                 'mw.loader.impl(',
2036                                 script,
2037                                 ');\n'
2038                         ];
2039                         if ( $VARS.sourceMapLinks ) {
2040                                 srcParts.push( '// Saved in localStorage at ', ( new Date() ).toISOString(), '\n' );
2041                                 var sourceLoadScript = sources[ descriptor.source ];
2042                                 var query = Object.create( $VARS.reqBase );
2043                                 query.modules = module;
2044                                 query.version = getCombinedVersion( [ module ] );
2045                                 query = sortQuery( query );
2046                                 srcParts.push(
2047                                         '//# sourceURL=',
2048                                         // Use absolute URL so that Firefox console stack trace links will work
2049                                         ( new URL( sourceLoadScript, location ) ).href,
2050                                         '?',
2051                                         makeQueryString( query ),
2052                                         '\n'
2053                                 );
2055                                 query.sourcemap = '1';
2056                                 query = sortQuery( query );
2057                                 srcParts.push(
2058                                         '//# sourceMappingURL=',
2059                                         sourceLoadScript,
2060                                         '?',
2061                                         makeQueryString( query )
2062                                 );
2063                         }
2064                         this.items[ key ] = srcParts.join( '' );
2065                         hasPendingWrites = true;
2066                 },
2068                 /**
2069                  * Iterate through the module store, removing any item that does not correspond
2070                  * (in name and version) to an item in the module registry.
2071                  */
2072                 prune: function () {
2073                         for ( var key in this.items ) {
2074                                 // key is in the form [name]@[version], slice to get just the name
2075                                 // to provide to getModuleKey, which will return a key in the same
2076                                 // form but with the latest version
2077                                 if ( getModuleKey( splitModuleKey( key ).name ) !== key ) {
2078                                         this.stats.expired++;
2079                                         delete this.items[ key ];
2080                                 }
2081                         }
2082                 },
2084                 /**
2085                  * Clear the entire module store right now.
2086                  */
2087                 clear: function () {
2088                         this.items = {};
2089                         try {
2090                                 localStorage.removeItem( this.key );
2091                         } catch ( e ) {}
2092                 },
2094                 /**
2095                  * Request a sync of the in-memory store back to persisted localStorage.
2096                  *
2097                  * This function debounces updates. The debouncing logic should account
2098                  * for the following factors:
2099                  *
2100                  * - Writing to localStorage is an expensive operation that must not happen
2101                  *   during the critical path of initialising and executing module code.
2102                  *   Instead, it should happen at a later time after modules have been given
2103                  *   time and priority to do their thing first.
2104                  *
2105                  * - This method is called from mw.loader.store.add(), which will be called
2106                  *   hundreds of times on a typical page, including within the same call-stack
2107                  *   and eventloop-tick. This is because responses from load.php happen in
2108                  *   batches. As such, we want to allow all modules from the same load.php
2109                  *   response to be written to disk with a single flush, not many.
2110                  *
2111                  * - Repeatedly deleting and creating timers is non-trivial.
2112                  *
2113                  * - localStorage is shared by all pages from the same origin, if multiple
2114                  *   pages are loaded with different module sets, the possibility exists that
2115                  *   modules saved by one page will be clobbered by another. The impact of
2116                  *   this is minor, it merely causes a less efficient cache use, and the
2117                  *   problem would be corrected by subsequent page views.
2118                  *
2119                  * This method is considered internal to mw.loader.store and must only
2120                  * be called if the store is enabled.
2121                  *
2122                  * @private
2123                  * @method
2124                  */
2125                 requestUpdate: function () {
2126                         // On the first call to requestUpdate(), create a timer that
2127                         // waits at least two seconds, then calls onTimeout.
2128                         // The main purpose is to allow the current batch of load.php
2129                         // responses to complete before we do anything. This batch can
2130                         // trigger many hundreds of calls to requestUpdate().
2131                         if ( !hasPendingFlush ) {
2132                                 hasPendingFlush = setTimeout(
2133                                         // Defer the actual write via requestIdleCallback
2134                                         function () {
2135                                                 mw.requestIdleCallback( flushWrites );
2136                                         },
2137                                         2000
2138                                 );
2139                         }
2140                 }
2141         };
2142 }() );