I'll try to give a concrete example, which is our daily code to deal with.
There is a definition per each interpreter, where an interpreter is provided via a module, which in turns can create different "sandboxes" ... they all do something like this:
// import the interpreter and returns a normalized namespace
// able to run code sync or async
function getInterpreter(url) {
return {
module: import(url),
run: (sandbox, code) => sandbox.evalute(code),
runAsync: (sandbox, code) => sandbox.evaluate(code, {async: true})
};
}
// how things are done now ...
async function createSandbox(moduleURL) {
const interpreter = getInterpreter(moduleURL);
const {run, runAsync} = interpreter;
const module = await interpreter.module;
const sandbox = await module.createSandBox();
return {
run: run.bind(null, sandbox),
runAsync: runAsync.bind(null, sandbox)
};
}
// usage
const pyodide = await createSandbox('./pyodide.mjs');
pyodide.run(pythonCode);
There other cases where both loading the module / sandbox requires fetching config files to bootstrap and so on ... we're manually orchestrating Promise.all([...])
here and there to boost bootstrap and use parallelism as much as we can ... now, with my proposal:
async function createSandbox(moduleURL) {
const {
run, runAsync,
await module: { createSandBox }
} = await getInterpreter(moduleURL);
const sandbox = await createSandBox();
return {
run: run.bind(null, sandbox),
runAsync: runAsync.bind(null, sandbox)
};
}
Now, when it comes to namespaces carrying lazy imports things become more interesting:
// lazy importMaps in JS (one that works in Workers too)
const urls = {
'utils': './path/utils.js',
'lib': 'https://esm.sh/lib',
'poly': 'https://esm.sh/poly'
};
// handy proxy for lazy imports
const modules = new Proxy(Object.prototype, {
get: (_, module) => import(urls[module])
});
// file X
const {
await utils: { path, fs },
await poly
} = modules;
// file Y
const { await lib } = modules;
So now we have a thin layer able to hot-swap loaded modules (change urls
fields when/if needed) or lazy load all dependencies with ease, allowing modules to be destructured like an import could, or better, exactly like a dynamic const { stuff } = await import(url)
would.
Is any of these use cases interesting? I find the latter one specially compelling for both prod and dev cases.
edit P.S. this latter example explicitly shows the intent of the proposal:
- it is not desired to await all properties of an object ... only destructured properties with explicit
await
should be awaited
- it is very desired to have all destructured awaited properties to be grouped as single
Promise.all([...])
operation
- it is still possible to attach or return, via the namespace, properties that are not promises, or promises themselves that should not be awaited during destructuring
Any deviation from any of these points will likely make the effort futile and the feature less interesting, imho.
edit2 the only "complexity" I see for this proposal is the case:
const {
ββββββββββββββββββββββββββ
await utils: { path, fs, await nested },
await poly
} = modules;
The "obvious" solution I see is that utils
should be group-awaited with poly
and then its nested await
should be grouped / awaited once utils
is resolved ... in this case the Promise.all
might not branch all levels equally, if difficult, but ergonomics will still be pretty awesome and easy on the eyes with way less boilerplate needed in general.