Nested Wait Groups, Like a Poor-Man's Nursery
Here’s a code sample from a project I am working on.
const readAsset = (relativePath: string) =>
readFile(join(options.assetRoot, relativePath), "utf8");
const [
registry,
launcherHtml,
launcherScript,
launcherStyles,
workspaceHtml,
workspaceScript,
] = await Promise.all([
loadWorkspaceRegistry(options.registryPath),
readAsset("index.html"),
readAsset("assets/launcher.js"),
readAsset("assets/launcher.css"),
readAsset("workspace.html"),
readAsset("assets/workspace.js"),
]);AFAIK this is the bog standard approach to awaiting a bunch of things at the same time. When its two to three items this is relatively ergonomic, but once you get past 5 I think it really just looks kind of gross.
All I want is a bunch of things to run concurrently, and for the name of the thing to be right next to the thing!
Because none of my problems or ideas are original, there is certainly many many approaches to obviate this “define over under instead of side by side” friction.
Here’s a sketch of what I thought would look the best.
const loaded = await resolveProperties({
registry: loadWorkspaceRegistry(options.registryPath),
launcher: resolveProperties({
html: readAsset("index.html"),
script: readAsset("assets/launcher.js"),
styles: readAsset("assets/launcher.css"),
}),
workspace: resolveProperties({
html: readAsset("workspace.html"),
script: readAsset("assets/workspace.js"),
}),
});
loaded.registry;
loaded.launcher.html;
loaded.workspace.script;One benefit is that you can nest your calls into groups if you wanted to. Yes, I am aware this is now a materially different execution pattern, it’s just an option!
So it’s kind of like a structured concurrency nursery, except you don’t really have a way to cancel promises when siblings fail.
The helper is small:
async function resolveProperties<
const T extends Record<string, unknown>,
>(properties: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
const entries = await Promise.all(
Object.entries(properties).map(async ([key, value]) => [
key,
await value,
] as const),
);
return Object.fromEntries(entries) as {
[K in keyof T]: Awaited<T[K]>;
};
}I didn’t end up actually using this because I only had the one case of the longer-than-three promise chain in the repo. I’d rather stick to the gross conventions for a one-off, but it sure is pretty!