Skip to content
This repository has been archived by the owner on Aug 29, 2021. It is now read-only.

Possible to avoid lock-in to async-only implementations #29

Closed
jdalton opened this issue Jul 3, 2018 · 23 comments
Closed

Possible to avoid lock-in to async-only implementations #29

jdalton opened this issue Jul 3, 2018 · 23 comments
Labels

Comments

@jdalton
Copy link
Member

jdalton commented Jul 3, 2018

At the moment the specification allows implementations to create, instantiate, and evaluate modules in an async or sync fashion. My hunch is that, as is, this proposal will remove the sync option from the table and enforce async all-up. (correct?)

Is there a possible flavor, that the champions of TLA could see, where sync implementations could still be doable? I'm assuming it would mean parent modules don't block for children much like a child module using an async IIFE today.

@ljharb
Copy link
Member

ljharb commented Jul 3, 2018

Could this be addressed with the restriction that TLA can not appear in a module that has export?

@jdalton
Copy link
Member Author

jdalton commented Jul 3, 2018

Could this be addressed with the restriction that TLA can not appear in a module that has export?

I was wondering that too (see: optional-constraint), but got it in my head that the parent would still block. Some clarification on that would be great!

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

My assumption is that without the collision between await and export to worry about, it would be desirable for the parent to not block - ie, for TLA to be sugar for a "yield til a future tick", just like await already works in async function.

@benjamn
Copy link
Member

benjamn commented Jul 4, 2018

The child module may not have any exports to provide to the parent, but it very well might have side-effects that it expects to complete while the parent waits. Whether or not the child has export declarations seems irrelevant to whether the parent should wait for the child to finish evaluating.

My stance: in order for TLA to be useful, parents must wait for children. Otherwise, if we settle for restricting TLA to non-exporting modules, and parent modules do not have to wait for children to finish, then TLA will be nothing more than sugar for an immediately-invoked async function expression in the entry point module, which is already possible today.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

If the only way TLA is useful is by introducing a new kind of blocking to the language - effectively sleep() - and by making top-level await behave wildly differently than normal await - then TLA might not be a good fit for the language.

Personally, I think there's value in it purely as sugar for an AIIFE.

@domenic
Copy link
Member

domenic commented Jul 4, 2018

I think if hosts want to have extra restrictions, they should be able to do so, e.g. disallowing modules that use top level await. But that's host-specific, and should be out of the scope of the proposal.

@jdalton
Copy link
Member Author

jdalton commented Jul 4, 2018

@benjamn

The child module may not have any exports to provide to the parent, but it very well might have global side-effects that it expects to complete while the parent waits.

I think variant A (which did not make it to stage 2) may be more in line with the order-is-import-scenario, but I'm not positive.

My stance: in order for TLA to be useful, parents must wait for children.

FWIW I find TLA useful today as just sugar around an async IIFE.

TLA will be nothing more than sugar for an immediately-invoked async function expression in the entry point module

I don't believe the optional-constraint clause restricts to entry scripts.


Would TLA champions be ok with something that's just syntax sugar around async-IIFEs?

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

But that's host-specific

I don't think that changing module evaluation order (which blocking would do) is something hosts should be permitted to do.

@domenic
Copy link
Member

domenic commented Jul 4, 2018

No, they would just throw if they see an await.

@benjamn
Copy link
Member

benjamn commented Jul 4, 2018

If TLA is sugar for an IIAFE, but dynamic import() waits for top-level awaits to finish, then that's a pretty big difference in semantics between static import and dynamic import(). One of them allows child modules to conceal asynchronous work as an implementation detail, and the other does not.

Are we imagining that dynamic import() would also resolve the namespace object as soon as module evaluation "returns," before all the awaits in the child module have finished?

If TLA ends up being "just sugar around an async IIFE," it seems important to make sure dynamic import() also essentially disregards top-level await, for consistency with static import.

@zenparsing
Copy link
Member

I'd like to add my perspective (since I've been active in the node modules discussions).

For ESM/CJS interoperability we will likely be depending heavily on either dual publishing both CJS and ESM, or on runtime ESM-to-CJS translation (e.g. the "esm" package). Users that require a package, either because they are on an older version of node, or they are transpiling with Babel, or any other reason, will get the CJS version of the package.

Currently, this works very well because ESM can be transformed down into CJS without affecting the resulting API very much.

But any ESM graph that includes a top-level await cannot be transpiled down into CJS without converting the results into a Promise.

From this point of view, top-level await complicates the transition story for Node (especially since top-level await is attractive to users). I think it would be risky to move forward with top-level await until we've had more time for the Node ecosystem to adjust to native modules.

Would other options give us the same advantages without the risks? For example, the sugar over AIIFE that's been proposed elsewhere:

// An async "statement"
async {
  await fileSystem.write(path, 'aksdjfas');
}

@benjamn
Copy link
Member

benjamn commented Jul 4, 2018

Would other options give us the same advantages without the risks?

That's a great question. If you're someone who would be happy with TLA-as-sugar-for-AIIFE, then you might be equally happy with a language feature (such as async blocks) that explicitly does not affect module graph loading semantics. On the other hand, if you're someone who is reluctant to give up synchronous module evaluation and the ability to compile to CJS, such a proposal would presumably be much easier to accept.

@bergus
Copy link

bergus commented Jul 4, 2018

But any ESM graph that includes a top-level await cannot be transpiled down into CJS without converting the results into a Promise.

Would this be that bad actually? I would expect to get a promise for a module when having TLA in the module or its dependencies. Sure, it could be a bit surprising since nothing in the code explicitly marks up asynchronous dependencies, but I doubt it will be a large problem in practice - you don't make your module asynchronous out of nothing, and without documentation about the implications for using it with CJS.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

@bergus it would be that bad if a transitive dependency, deep in your graph, suddenly forced its entire ancestry to be async unbeknownst to you by adding a TLA.

@zenorbi
Copy link

zenorbi commented Jul 4, 2018

@ljharb AFAIK commonjs require cannot and possibly won't be able to load an esm package. Some discussions are happening at nodejs/modules#139 weather require should return a promise of the requested module, but sync loading is not possible.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

@zenorbi yes, I’m quite aware of those discussions, and i don’t agree that sync-looking loading is not possible.

Regardless tho, I’m mostly concerned with not adding sleep() to JS, and with making sure that every level of await behaves consistently.

@bergus
Copy link

bergus commented Jul 4, 2018

@ljharb Sure that's bad, but not really any different than how it currently is - even today any transitive dependency can break your code by suddenly exporting a promise instead of a plain object. The only difference with transpiled ES6 modules would be that the root cause will be harder to find, as all modules implicitly become async and the surprising promise would surface far away from the problem.

But that problem is the same with plain ES6 modules (without transpilation), when you have an asynchronous dependency without expecting it and code breaking because of that. The only way to fix that would be to make such imports explicit, e.g. async import … from …;, and causing early errors if an imported module without that async annotation uses TLA or async imports.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

@bergus it’s a very different sort of problem when a module ships a breaking change, versus when a module can virally infect every other thing that transitively depends on it.

@zenparsing
Copy link
Member

zenparsing commented Jul 4, 2018

It is true that, in general, ESM graphs cannot be transpiled down into sync CJS graphs. But in practice, users aren't using any features that would prevent such transformation.

Basically, TLA is going to play havoc with the dual-publishing transition story, where package authors provide both CJS and ESM versions of their modules (either by using Babel or a runtime solution like "esm").

@Jamesernator
Copy link

"In theory" you could require modules even if they have asynchronous work by using Atomics.wait and coordinating two agents so that things still work normally otherwise.

I do say in theory because there's a large amount of stuff that would be need to be done to make things like intrinsics work, cross-agent symbol passing, ensuring the event loop behaves identically outside of require amongst other things.

@littledan
Copy link
Member

The current specification preserves the semantics of sync modules with all sync dependencies. That is, if you don't use TLA, and no dependencies do, then there's no lock-in. Does this address the concern? I understand that the rest of the ecosystem could cause you to be using this feature, but that seems like a property of any change to the language.

@codehag
Copy link
Collaborator

codehag commented Apr 6, 2021

Pinging here again to see the status of this concern.

@codehag
Copy link
Collaborator

codehag commented May 12, 2021

Given @littledan's answer and lack of engagement further on this issue, I believe that this concern is addressed. If it is not, please re-open this issue.

@codehag codehag closed this as completed May 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

10 participants