r108105 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r108104‎ | r108105 | r108106 >
Date:01:30, 5 January 2012
Author:krinkle
Status:ok (Comments)
Tags:
Comment:
[mediawiki.loader] fix numerous bugs and edge cases discovered with Roan on Etherpad/Skype

* 'undefined' state removed:
State 'undefined' has been removed. This was half-supported, probably with the intention to support loading of modules that are registered server side after the startup module was loaded (either because the server had a deployment while the user was browsing the page, or because loader.load calls in HTML document or loader.register where cached and out of sync. That's not unlikely to happen after deployment, after which there is a 5 minute window of cached startup modules in peoples browser cache but fresh HTML documents.

Instead of filling in the missing registry with no information, simply don't support this. Shouldn't break anything as it wasn't supported all the way.

* Document state 'missing'. Used by the load.php when a requested module doesn't exist according to the server.

* Fix recurse(). Previously it failed to recognize circular dependencies, because "unresolved" started as an empty array (given by resolve(). See also test cases at bottom of commitmsg.
* Fix recurse() even more. It was using inArray as a parameter to [].splice(), which is bad as inArray can be -1, in which case splice(-1,1) will remove the last item in the array, instead of the one at the index. Fortunately this was never exploited as 'unresolved=[]' is starting point, so nothing to delete.

* Fix resolve(). Don't return an empty array, which is contrary to it's (now documented) behavior of including the requested module in the return array. Instead letting recurse() handle it.

* Add magic filter to filter() named "unregistered" which will reduce the set to only module names that are not in the registry.

* Removing call to request() from the bottom bottom of mw.loader.implement. This can't be needed as before load.php is called the toes implement, all dependencies are put in queue and set to "loading" state, this queue is build first and then the load requests happen. At point implement is called, it's dependencies must already be in the queue. If any unloaded dependencies at moment of implementation (which is very common), the next implement will call execute which does handlePending, which will execute previously implemented modules that were waiting for dependencies to arrive.

* Make load() and using() no longer return meaningless booleans, they were undocumented and meaningless as they are either used as voids or with callbacks.

* Bring consistency in handling input. If direct input to a loader function is invalid or contains inexistent modules, throw an exception. Otherwise use error callback.
-- with one exception, that is the raw module list passed to mw.loader.load, those are not related to each other and should continue even if there is an inexistent module in there.
-- For example if a deployment occurs, adding an extension with modules loaded on a particular page. The user will have a startup module in cache up to 5 minutes that doesn't have this module's registry yet. In that case the new modules should simply be ignored. Before this commit they were also silently ignored, but not by skipping them. previously inexistent/undefined modules would give ok-callback of using() instantly, because filter(['ready'], modules) is the same as modules when it contains nothing.
Modified paths:
  • /trunk/phase3/resources/mediawiki/mediawiki.js (modified) (history)

Diff [purge]

Index: trunk/phase3/resources/mediawiki/mediawiki.js
@@ -322,7 +322,7 @@
323323 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function() {}
324324 * 'group': 'somegroup', (or) null,
325325 * 'source': 'local', 'someforeignwiki', (or) null
326 - * 'state': 'registered', 'loading', 'loaded', 'ready', or 'error'
 326+ * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
327327 * 'script': ...,
328328 * 'style': ...,
329329 * 'messages': { 'key': 'value' },
@@ -435,18 +435,22 @@
436436 ' -> ' + deps[n]
437437 );
438438 }
 439+
 440+ // Add to unresolved
 441+ unresolved[unresolved.length] = module;
439442 recurse( deps[n], resolved, unresolved );
 443+ // module is at the end of unresolved
 444+ unresolved.pop();
440445 }
441446 }
442447 resolved[resolved.length] = module;
443 - unresolved.splice( $.inArray( module, unresolved ), 1 );
444448 }
445449
446450 /**
447451 * Gets a list of module names that a module depends on in their proper dependency order
448452 *
449453 * @param module string module name or array of string module names
450 - * @return list of dependencies
 454+ * @return list of dependencies, including 'module'.
451455 * @throws Error if circular reference is detected
452456 */
453457 function resolve( module ) {
@@ -463,10 +467,6 @@
464468 }
465469 return modules;
466470 } else if ( typeof module === 'string' ) {
467 - // Undefined modules have no dependencies
468 - if ( registry[module] === undefined ) {
469 - return [];
470 - }
471471 resolved = [];
472472 recurse( module, resolved, [] );
473473 return resolved;
@@ -476,12 +476,13 @@
477477
478478 /**
479479 * Narrows a list of module names down to those matching a specific
480 - * state. Possible states are 'undefined', 'registered', 'loading',
481 - * 'loaded', or 'ready'
 480+ * state (see comment on top of this scope for a list of valid states).
 481+ * One can also filter for 'unregistered', which will return the
 482+ * modules names that don't have a registry entry.
482483 *
483484 * @param states string or array of strings of module states to filter by
484 - * @param modules array list of module names to filter (optional, all modules
485 - * will be used by default)
 485+ * @param modules array list of module names to filter (optional, by default the entire
 486+ * registry is used)
486487 * @return array list of filtered module names
487488 */
488489 function filter( states, modules ) {
@@ -504,7 +505,7 @@
505506 for ( m = 0; m < modules.length; m += 1 ) {
506507 if ( registry[modules[m]] === undefined ) {
507508 // Module does not exist
508 - if ( states[s] === 'undefined' ) {
 509+ if ( states[s] === 'unregistered' ) {
509510 // OK, undefined
510511 list[list.length] = modules[m];
511512 }
@@ -736,15 +737,15 @@
737738 if ( arguments.length > 1 ) {
738739 jobs[jobs.length] = {
739740 'dependencies': filter(
740 - ['undefined', 'registered', 'loading', 'loaded'],
 741+ ['registered', 'loading', 'loaded'],
741742 dependencies
742743 ),
743744 'ready': ready,
744745 'error': error
745746 };
746747 }
747 - // Queue up any dependencies that are undefined or registered
748 - dependencies = filter( ['undefined', 'registered'], dependencies );
 748+ // Queue up any dependencies that are registered
 749+ dependencies = filter( ['registered'], dependencies );
749750 for ( n = 0; n < dependencies.length; n += 1 ) {
750751 if ( $.inArray( dependencies[n], queue ) === -1 ) {
751752 queue[queue.length] = dependencies[n];
@@ -822,15 +823,13 @@
823824
824825 // Appends a list of modules from the queue to the batch
825826 for ( q = 0; q < queue.length; q += 1 ) {
826 - // Only request modules which are undefined or registered
827 - if ( registry[queue[q]] === undefined || registry[queue[q]].state === 'registered' ) {
 827+ // Only request modules which are registered
 828+ if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
828829 // Prevent duplicate entries
829830 if ( $.inArray( queue[q], batch ) === -1 ) {
830831 batch[batch.length] = queue[q];
831832 // Mark registered modules as loading
832 - if ( registry[queue[q]] !== undefined ) {
833 - registry[queue[q]].state = 'loading';
834 - }
 833+ registry[queue[q]].state = 'loading';
835834 }
836835 }
837836 }
@@ -985,7 +984,7 @@
986985 throw new Error( 'module must be a string, not a ' + typeof module );
987986 }
988987 if ( registry[module] !== undefined ) {
989 - throw new Error( 'module already implemented: ' + module );
 988+ throw new Error( 'module already registered: ' + module );
990989 }
991990 // List the module as registered
992991 registry[module] = {
@@ -1053,8 +1052,6 @@
10541053 registry[module].dependencies ) )
10551054 {
10561055 execute( module );
1057 - } else {
1058 - request( module );
10591056 }
10601057 },
10611058
@@ -1107,45 +1104,59 @@
11081105 * "text/javascript"; if no type is provided, text/javascript is assumed.
11091106 */
11101107 load: function ( modules, type ) {
 1108+ var filtered, m;
 1109+
11111110 // Validate input
11121111 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
11131112 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
11141113 }
1115 - // Allow calling with an external script or single dependency as a string
 1114+ // Allow calling with an external url or single dependency as a string
11161115 if ( typeof modules === 'string' ) {
11171116 // Support adding arbitrary external scripts
11181117 if ( /^(https?:)?\/\//.test( modules ) ) {
11191118 if ( type === 'text/css' ) {
1120 - $( 'head' ).append( $( '<link/>', {
 1119+ $( 'head' ).append( $( '<link>', {
11211120 rel: 'stylesheet',
11221121 type: 'text/css',
11231122 href: modules
11241123 } ) );
1125 - return true;
 1124+ return;
11261125 } else if ( type === 'text/javascript' || type === undefined ) {
11271126 addScript( modules );
1128 - return true;
 1127+ return;
11291128 }
11301129 // Unknown type
1131 - return false;
 1130+ throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
11321131 }
11331132 // Called with single module
11341133 modules = [modules];
11351134 }
 1135+
 1136+ // Filter out undefined modules, otherwise resolve() will throw
 1137+ // an exception for trying to load an undefined module.
 1138+ // Undefined modules are acceptable here in load(), because load() takes
 1139+ // an array of unrelated modules, whereas the modules passed to
 1140+ // using() are related and must all be loaded.
 1141+ for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
 1142+ if ( registry[modules[m]] !== undefined ) {
 1143+ filtered[filtered.length] = modules[m];
 1144+ }
 1145+ }
 1146+
11361147 // Resolve entire dependency map
1137 - modules = resolve( modules );
 1148+ filtered = resolve( filtered );
11381149 // If all modules are ready, nothing dependency be done
1139 - if ( compare( filter( ['ready'], modules ), modules ) ) {
1140 - return true;
 1150+ if ( compare( filter( ['ready'], filtered ), filtered ) ) {
 1151+ return;
11411152 }
1142 - // If any modules have errors return false
1143 - else if ( filter( ['error'], modules ).length ) {
1144 - return false;
 1153+ // If any modules have errors
 1154+ else if ( filter( ['error'], filtered ).length ) {
 1155+ return;
11451156 }
11461157 // Since some modules are not yet ready, queue up a request
11471158 else {
1148 - request( modules );
1149 - return true;
 1159+ request( filtered );
 1160+ return;
11501161 }
11511162 },
11521163

Sign-offs

UserFlagDate
Nikerabbitinspected07:45, 5 January 2012

Comments

#Comment by Hashar (talk | contribs)   14:39, 10 January 2012

Krinkle, Roan > can we assume this was peer reviewed between you two and thus mark it ok? :-b

#Comment by Catrope (talk | contribs)   15:26, 10 January 2012

I'm gonna review this today.

#Comment by Hashar (talk | contribs)   22:13, 10 January 2012

Awesome. Thanks Roan!

Status & tagging log