Files
scriptc/tests/corpus/5905-array-from-async-generator.ts
T
Chris Tate aa19f948c5 Settle async generator completions on Node's microtask turns
An async generator that fell off its end, with a return type other than
void, settled its last next() one microtask turn after Node, so for await
loops over it also ended a turn late. The appended implicit return now
marks the completion as settled, like a bare `return;`.

return() at a suspended yield ran the generator's finally blocks before
awaiting the return value. The request now awaits the value first and
resumes the body on the next turn, and the completion that follows settles
without a further turn, also when a finally block yields or awaits first.
An explicit `return x` reached during that unwind still takes its own turn
to await x.

Array.fromAsync accepts typed async generator sources: each next() result
is awaited and its value used as is, a throwing or rejecting mapping
function closes the generator, and a rejected next() rejects the result on
the turn Node rejects it.
2026-10-17 23:19:15 -05:00

62 lines
2.1 KiB
TypeScript

// Array.fromAsync over async generators: plain, mapped, async-mapped, a
// throwing or rejecting mapping function (which closes the generator), a
// generator that throws, an empty generator, and two calls at once,
// interleaved with a microtask ticker. Node.js prints the same.
const log: string[] = [];
async function ticker(n: number): Promise<void> {
for (let i = 0; i < n; i++) {
log.push("t" + i);
await null;
}
}
async function* nums(n: number): AsyncGenerator<number> {
try {
for (let i = 0; i < n; i++) {
log.push("yield " + i);
yield i;
}
} finally {
log.push("gen finally");
}
}
async function* failing(): AsyncGenerator<number> {
yield 1;
throw new Error("gen failure");
}
async function report(label: string, job: Promise<number[]>): Promise<void> {
await job.then(
(a) => log.push(label + " [" + a.join(",") + "]"),
(e: unknown) => log.push(label + " rejected " + (e as Error).message),
);
}
async function run(name: string, body: () => Promise<void>): Promise<void> {
log.length = 0;
const tick = ticker(20);
await body();
await tick;
console.log(name + ": " + log.join(" "));
}
async function main(): Promise<void> {
await run("plain", () => report("plain", Array.fromAsync(nums(3))));
await run("map", () => report("map", Array.fromAsync(nums(3), (x: number, i: number) => x * 10 + i)));
await run("async map", () => report("async map", Array.fromAsync(nums(2), async (x: number) => x + 1)));
await run("map throws", () =>
report("map throws", Array.fromAsync(nums(3), (x: number) => {
if (x === 1) throw new Error("map failure");
return x;
})),
);
await run("map rejects", () =>
report("map rejects", Array.fromAsync(nums(3), async (x: number) => {
if (x === 1) throw new Error("async map failure");
return x;
})),
);
await run("generator throws", () => report("generator throws", Array.fromAsync(failing())));
await run("empty", () => report("empty", Array.fromAsync(nums(0))));
await run("two at once", async () => {
await Promise.all([report("a", Array.fromAsync(nums(2))), report("b", Array.fromAsync(nums(1)))]);
});
}
void main();