Asynchronous Module Initialize

# Jussi Kalliokoski (11 years ago)

On the ModuleImport thread, I pretty much derailed [1] the conversation with poor argumentation which lead to the discussion drying out. But the more I think about it, the more important I feel my concern was so I figured I'd have another shot at it.

The problem I was describing is that, as with any problem space complex enough, there is a conflict of interest between different sets of use cases. As Brendan replied [2] to my post, the goals of the current design of the module system include:

  • Static vs. dynamic imports and exports. This enables read-time import resolving, better (or at least easier to build) tooling, import:export mismatches as early errors, leaves the door open for static metaprogramming patterns like macros and types and probably other benefits. Also the transitive cyclic dependencies I unwarrantedly focused on.
  • Paving the cowpaths of existing patterns, such as named exports and default exports.

Like I stated in the other thread, I'm a big fan of the static imports and the read-time import resolving that it brings to the table. However, the use case incompatibilities arise from the static exports side of things. I proposed (it was less of a proposal though, more an idea or an example to spur better ideas) that we had a single dynamic exportable per each module, and that could be an object, function, undefined for side effects or anything. But, the important part was that it could also be a Promise of what you want to export, allowing asynchronous module initialization.

The use cases addressed include:

  • Optional dependencies (required for porting large amounts of existing code to use ES6 modules).
  • Async feature detection.
  • Dependencies on things other than JS, such as stylesheets, images, templates or configuration (e.g. a default language pack).
  • Waiting on something to be ready, for example something like jQuery could wait for DOM ready so that the API consumer doesn't have to.

All of these can be done with the current design, however you cannot defer the module being ready to be imported. So if you depend on these use cases, you have to provide async APIs for things that are possibly synchronous otherwise, not only imposing a performance penalty, but also a convenience downer on the consumers of your API.

I'm quite skeptic of this being possible to retrofit to the current design without sacrificing static exports, but I'd be more than happy to see myself proven wrong and we have a lot of smart minds gathered here so mayhaps.

I really like macros (all good things in moderation of course) and sweet.js and use it occasionally even for production code. Before that I sometimes even used GCC's preprocessor to get macros in JS. I also really like types

  • in moderation as well, i.e. declaring the types of inputs and outputs of functions. This is to say, I'm definitely not just happily trying to close the metaprogramming door here. But I think things like macros and types are something that can be done and is being done with tooling, at least to some extent, whereas optional dependencies for example, not really, at least my imagination is too limited to see how.

Of course, the ES6 modules ship is already overdue, there's already tooling made for it and probably browsers are prototyping the current design as well, so maybe these use cases are something we don't want to or can't (anymore) afford to consider. Or maybe they are less important than the use cases that would be excluded if we included async module initializing. And that's all fine, as long as it's a conscious choice made with these implications considered. (Maybe not personally fine for me, but I've survived with the status quo so I can probably survive with using the existing solutions if I have a use case that's not elegantly solved by ES6 modules).

[1] esdiscuss.org/topic/moduleimport#content-181 [2] For some reason, I could not find the reply on esdiscuss.org.

# John Barton (11 years ago)

On Wed, Jul 9, 2014 at 1:57 PM, Jussi Kalliokoski < jussi.kalliokoski at gmail.com> wrote: ...

I proposed (it was less of a proposal though, more an idea or an example to spur better ideas) that we had a single dynamic exportable per each module, and that could be an object, function, undefined for side effects or anything. But, the important part was that it could also be a Promise of what you want to export, allowing asynchronous module initialization.

The use cases addressed include:

  • Optional dependencies (required for porting large amounts of existing code to use ES6 modules).
  • Async feature detection.
  • Dependencies on things other than JS, such as stylesheets, images, templates or configuration (e.g. a default language pack).
  • Waiting on something to be ready, for example something like jQuery could wait for DOM ready so that the API consumer doesn't have to.

All of these can be done with the current design, however you cannot defer the module being ready to be imported. So if you depend on these use cases, you have to provide async APIs for things that are possibly synchronous otherwise, not only imposing a performance penalty, but also a convenience downer on the consumers of your API.

...

If I understand your question here, I think the current solution as adequate support for these cases.

The current module loading solution mixes imperative and declarative specification of the JS execution order. It's a bit of a layer cake: imperative layer trigger declarative layers trigger imperative layers.

The top imperative layer (eg System.import()) loads the root of the first dependency tree and parses it, entering a declarative specification layer, the import declarations. These declarations are then processed with Loader callbacks, in effect more imperative code, that can result in parsing and more declarative analysis.

By design the declarative layers prevent all of the things you seek. This layer is synchronous, unconditional, wired to JS exclusively.

The imperative layers support all of the use cases you outline, though to be sure some of this is more by benign neglect than design.

By providing a custom Loader one can configure the module registry to contain optional, feature-detected modules or non-JS code. The Loader can also delay loading modules until some condition is fulfilled. I expect that multiple custom loaders will emerge optimized for different use cases, with their own configuration settings to make the process simpler for devs. Guy Bedford's systemjs already supports bundling for example.

This approach concentrates the configuration activity in the code preceding the load of a dependency tree (and hopefully immediately before it). This seems like a better design than say commonjs where any module at any level can manipulate the configuration.

The only unfortunate issue in this result is the decision to embed the custom loader in the global environment. This means that a tree of interdependent modules can issue Loader calls expecting a particular Loader to be in System so a custom loader will have to set/unset the global while loading the tree. Maybe we can experiment with modular loaders some time.

jjb

# Jussi Kalliokoski (11 years ago)

Interesting, thank you! I like this in the sense that the goal seems to not be the ultimate solution, but the tool for building one (or many). So, do you have any examples of how having optional dependencies would look from the API providers' perspective, versus e.g. the examples I showed earlier:

// foo.js
export System.import("optional-better-foo-implementation")
  .catch( => System.import("worse-but-always-there-foo-implementation") );

Does the provided by the custom loaders defer the responsibility of taking care of the optional dependencies to the API consumer, e.g. by dictating which module loader to use for loading the module at hand? That might not be ideal, especially if your code base is built on features of one loader and then want to employ a third party library that is built on the assumption of another loader. But maybe the future will show it to be a worthy compromise.

# caridy (11 years ago)

John is right, all the use cases you've mentioned can be solved with the current design. Here is an example of a loader extension that implements conditional loading:

gist.github.com/caridy/1a67aaf433ae03a42a8b

This particular implementation relies on the trigger configuration attached to the the loader instance, in this case System. In the example below, if XMLHttpRequest is detected, bar module will be used whenever someone try to import foo using the imperative or declarative form, assuming foo and bar have the same named exports, making the replacement completely transparent for the consumer module:

System.trigger['foo'] = {
    test: function () {
        return !!window.XMLHttpRequest;
    },
    replaceWith: 'bar'
};

As for DOM ready, etc, you can apply the same principle, and since all loader hooks are promise based, you could hold a promise until reaching the DOM ready state, and that should be all.