Awesome
Intent
The intent of adding top level await is to add a means to allow Modules to asynchronously initialize their namespace prior to notifying the consumer that the Module is finished evaluating.
Uses
Dynamic dependency pathing
const strings = await import(`/i18n/${navigator.language}`);
This allows for Modules to use runtime values in order to determine dependencies. This is useful for things like development/production splits, internationalization, environment splits, etc.
Resource initialization
const connection = await dbConnector();
This allows Modules to represent resources and also to produce errors in cases where the Module will never be able to be used.
Dependency fallbacks
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
Designs
The main problem space in designing top level await is to aid in detecting
and preventing forms of deadlock that can occur. All examples below will
use a cyclic import()
to a graph of 'a' -> 'b', 'b' -> 'a'
with both using
top level await to halt progress until the other finishes loading. For brevity
the examples will only show one side of the graph, the other side is a mirror.
Await during Evaluation
This design would introduce an ability to block Module loading while in the Evaluation phase of Module loading.
await import('b');
Async instantiate blocks
This design would introduce an ability to execute code within the Instantiate phase of Module loading.
// using static as bikeshed for way to define this action is during
// instantiate
static async {
await import('b');
}
Since import()
requires the target Module finish Evaluation to complete
having a cycle in Instantiate still suffers from deadlock.
Halting Progress
Existing Ways to halt progress
Infinite Loops
for (const n of primes()) {
console.log(`${n} is prime}`);
}
Infinite series or lack of base condition means static control structures are vulnerable to infinite looping.
Infinite Recursion
const fibb = n => (n ? fibb(n - 1) : 1);
fibb(Infinity);
Proper tail calls allow for recursion to never overflow the stack. This makes it vulnerable to infinite recursion.
Atomics.wait
Atomics.wait(shared_array_buffer, 0, 0);
Atomics allow blocking forward progress by waiting on an index that never changes.
export function then
// a
export function then(f, r) {}
async function start() {
const a = await import('a');
console.log(a);
}
Exporting a then
function allows blocking import()
.
Guarding against
Implementing a TDZ
// a
await import('b');
// implement a hoistable then()
export function then(f, r) {
r('not finished');
};
// remove the rejection
then = null;
// b
await import('a');
Having a then
in the TDZ is a way to prevent cycles while a module is still
evaluating. try{}catch{}
can also be used as a recovery or notification
mechanism.
// b
let a;
try {
a = await import('a');
} catch {
// do something
}
This mechanism could even be codified into import()
by making it reject if
the target module has not run to end of source text in Evaluation yet.