From 987d718766a11cd1d95a9a00e2028ac38d8c4705 Mon Sep 17 00:00:00 2001 From: Duo Date: Wed, 21 Oct 2026 18:28:00 +0800 Subject: [PATCH] Drain the async generator queue after it completes Once an async generator has completed, requests queued behind the head request could stay pending forever: const it = (async function* () {})(); await it.next(); it.return(1); it.next(); // never settled Two places stopped processing the queue early: - The fulfilled and rejected closures of AsyncGeneratorAwaitReturn must run AsyncGeneratorDrainQueue after settling the return() request. js_async_generator_resolve_function() settled the request and returned without resuming the queue. - AsyncGeneratorDrainQueue keeps settling next() and throw() requests in the completed state until it reaches a return() or empties the queue. js_async_generator_resume_next() left the loop after the first one, which also stranded next() calls that were queued while the body was still running. --- quickjs.c | 4 +- tests/async-generator-drain-queue.js | 62 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/async-generator-drain-queue.js diff --git a/quickjs.c b/quickjs.c index 8f118a9b2..ee24cc1c0 100644 --- a/quickjs.c +++ b/quickjs.c @@ -21903,10 +21903,11 @@ static void js_async_generator_resume_next(JSContext *ctx, } else if (next->completion_type == GEN_MAGIC_RETURN) { s->state = JS_ASYNC_GENERATOR_STATE_AWAITING_RETURN; js_async_generator_completed_return(ctx, s, next->result); + goto done; } else { js_async_generator_reject(ctx, s, next->result); } - goto done; + break; case JS_ASYNC_GENERATOR_STATE_SUSPENDED_YIELD: case JS_ASYNC_GENERATOR_STATE_SUSPENDED_YIELD_STAR: value = js_dup(next->result); @@ -21997,6 +21998,7 @@ static JSValue js_async_generator_resolve_function(JSContext *ctx, } else { js_async_generator_resolve(ctx, s, arg, true); } + js_async_generator_resume_next(ctx, s); } else if (s->state == JS_ASYNC_GENERATOR_STATE_EXECUTING) { /* restart function execution after await() */ s->func_state.throw_flag = is_reject; diff --git a/tests/async-generator-drain-queue.js b/tests/async-generator-drain-queue.js new file mode 100644 index 000000000..33d0c6f16 --- /dev/null +++ b/tests/async-generator-drain-queue.js @@ -0,0 +1,62 @@ +import { assert } from "./assert.js"; + +/* AsyncGeneratorAwaitReturn and AsyncGeneratorDrainQueue: once the generator + has completed, every request queued behind a return() must still settle, + in order. A request that never settles leaves the module promise pending. */ + +async function completed() { + const it = (async function* () {})(); + assert((await it.next()).done, true); + return it; +} + +function track(log, label, promise) { + return promise.then( + v => { log.push(label + ":" + v.value + ":" + v.done); }, + e => { log.push(label + ":throw:" + e); }); +} + +async function run(requests) { + const it = await completed(); + const log = []; + await Promise.all(requests(it).map(([label, p]) => track(log, label, p))); + return log.join(","); +} + +/* a single request behind the awaited return value */ +assert(await run(it => [["return", it.return(1)], ["next", it.next()]]), + "return:1:true,next:undefined:true"); +assert(await run(it => [["return", it.return(1)], ["return", it.return(2)]]), + "return:1:true,return:2:true"); +assert(await run(it => [["return", it.return(1)], ["throw", it.throw("e")]]), + "return:1:true,throw:throw:e"); +assert(await run(it => [["return", it.return(Promise.reject("r"))], + ["return", it.return(2)]]), + "return:throw:r,return:2:true"); + +/* several requests: the queue is drained past next() and throw() */ +assert(await run(it => [["return", it.return(1)], ["next", it.next()], + ["next", it.next()]]), + "return:1:true,next:undefined:true,next:undefined:true"); +assert(await run(it => [["return", it.return(1)], ["throw", it.throw("e")], + ["next", it.next()]]), + "return:1:true,throw:throw:e,next:undefined:true"); +assert(await run(it => [["return", it.return(Promise.reject("r"))], + ["next", it.next()], ["return", it.return(2)]]), + "return:throw:r,next:undefined:true,return:2:true"); +assert(await run(it => [["return", it.return(1)], ["next", it.next()], + ["return", it.return(2)], ["next", it.next()]]), + "return:1:true,next:undefined:true,return:2:true,next:undefined:true"); + +/* requests queued while the body is still running reach a completed + generator without any return() call */ +{ + const gate = Promise.withResolvers(); + const it = (async function* () { await gate.promise; })(); + const log = []; + const requests = [1, 2, 3].map(i => track(log, "next" + i, it.next())); + gate.resolve(); + await Promise.all(requests); + assert(log.join(","), + "next1:undefined:true,next2:undefined:true,next3:undefined:true"); +}