Page MenuHomePhabricator

Allow subscribing to the loading of a ResourceLoader module
Open, MediumPublic

Description

mw.loader.using triggers loading of a module and returns a promise for when it will load, but sometimes it would be useful to obtain such a promise without triggering loading. Here is an example of current code trying to achieve that in hacky ways. There should be a proper way of doing it, something like mw.loader.whenUsing,

Event Timeline

Restricted Application added a subscriber: Aklapper. · View Herald Transcript

I'm not sure if this is a good idea – I don't want to worry about side-effects when loading a module…

If the module already has side-effects, and you need to wait for someone that the module does, then the module should provide some appropriate hook.

Here is an example of current code trying to achieve that in hacky ways.

if ( veState === 'loading' || veState === 'loaded' || veState === 'ready' ) {
	mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init' ).done( function () {
		mw.libs.ve.addPlugin( 'ext.growthExperiments.HelpPanel' );
	} );
}

VisualEditor already provides a way to do this without depending on ResourceLoader implementation details. Here's an example of that. In this case, you could do this instead:

mw.loader.using( 'ext.visualEditor.targetLoader' ).done( function () {
	mw.libs.ve.targetLoader.addPlugin( 'ext.growthExperiments.HelpPanel' );
} );

(this also works on the mobile version – which might resolve another hack in the GrowthExperiments code, not sure)

On mobile ext.visualEditor.targetLoader only gets loaded when the VE editor is open, so it doesn't seem nice to trigger loading it when the current view might not be related to editing in any way. But more generally, expecting all JS widgets to emit events when they start up isn't really a scalable coordination mechanism, especially for gadget authors most of whom don't really have any ability to effect changes in deployed code.

The use case of "introducing modules", where we let two modules discover each other, without dependencies and without one module taking responsiblity for loading the other module, is one that comes up from time to time.

This use case has a solution in the platform today, it is mw.hook(). For example, https://www.mediawiki.org/wiki/VisualEditor/Gadgets#Code_snippets documents:

mw.hook( 've.activationComplete' ).add( function () {
	// Some code to run when edit surface is ready
	var surface = ve.init.target.getSurface();
} );

This avoids coupling to specific module names, and allows for greater flexibilility both in that you can freely refactor and move the code that emits this, as well as that it isn't limited to firing immediatley upon a module loading, but may be verified and limited to after certain checks or computations have taken place.

It also allows you to document what you do and don't guarantee. For example, in the case of VE, it is allowed to unconditionally access the ve.init.target singleton from here, as VE ensures the hook will only fire "if" VE is used and only "when" the target has finished being loaded and set up.

I think of particular importance here is the idea of consent. It doesn't allow you to intercept arbitrary module loads. It is fairly cheap to add one to an arbitrary module, though, just append mw.hook().fire(); to the end of any module script.

I agree hooks are a superior solution in cases when the interaction between the two modules is coordinated (ie. the authors of both are aware of the need). "consent" to me implies that it would be somehow unethical to add callbacks to a module load without the module owner agreeing to it, which I don't think is the case, but I can certainly imagine situations where relying on the module load time has subtle risks and the author is well-placed to point those out.

On the other hand, I think gadget authors usually lack the capacity and/or know-how to coordinate their needs with the author other modules (whether gadgets or extension/core modules).

Conceptually this is somewhat similar to ExtensionRegistry::isLoaded() which also could be replaced with hooks but that would also result with unwanted overhead.

The analogue to ExtensionRegistry::isLoaded() is mw.loader.getState() which can be used to distinguish between:

  • null, an extension not being installed,
  • "registered", installed but not involved on this page,
  • else: it was requested (it may be in-flight, or failed, or succeededed; just like how isLoaded doesn't make guruantees about current state)

This is used in a couple of places even in production code. I hestitate to offer more ergonomic support here as per the previous comment, this imho leads to brittle and hard-to-support code.

I agree that it is plausible that requesting new hooks may be a burden, but do we have evidence that it is a burden? In two decades of maintaining gadgets, I've not seen this come up in the community. In my experience, the need for specific timing tends to mostly come up in fairly complex extensions that would likely want such hook regardless. The hooks I'm thinking of weren't added soley for Gadgets usage.

The example linked in the task description is about an interaction between two WMF-maintained extensions (GrowthExperiments and VisualEditor), and seems to be an example where VisualEditor hook that I used as example, would likely be appropiate. If not, I'd ask Editing team about how they would prefer to support that use case, so as to avoid surprise breakages in this production code.

Krinkle triaged this task as Medium priority.Mon, Apr 22, 6:21 PM
Krinkle moved this task from Inbox to Backlog on the MediaWiki-ResourceLoader board.

The immediate reason for filing the task was someone complaining on #wmhack about the difficulty of having a gadget do something when a client-side component loads (not VE, I think, but I forgot the specific use case). I agree for production code <-> production code interactions, developers should probably find better ways. I think most of the time, gadget authors have neither the leverage nor the know-how to make that happen.