From d3b63835ed4716a690d322a104a09af3273a0e9e Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 21 Jun 2026 14:04:09 +0200 Subject: [PATCH 1/5] quic: fix wake up blob The quic implementation calls setWakeUp with the assumption, that it is only executed once per event loop cycle. This assumption is wrong. Only setImmediate will guarantee, that the execution is delayed to later in the event loop and happening once in the event loop. Fixes: https://github.com/nodejs/node/issues/64035 Signed-off-by: Marten Richter --- lib/internal/blob.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 68071a5f5645e7..acaf8b4ac9b081 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -475,11 +475,17 @@ function createBlobReaderStream(reader) { this.pendingPulls = []; // Lazily register a wakeup callback that the C++ side can invoke // when new data is available after a STATUS_BLOCK. - this.wakeup = () => { - if (this.pendingPulls.length > 0) { - this.readNext(c); + let immediate; + reader.setWakeup(() => { + if (this.pendingPulls.length > 0 && + typeof immediate === 'undefined') { + // Postpone the execution to the next steps of the event loop + immediate = setImmediate(() => { + immediate = undefined; + this.readNext(c); + }); } - }; + }); }, pull(c) { const { promise, resolve, reject } = PromiseWithResolvers(); @@ -577,7 +583,16 @@ const kMaxBatchChunks = 16; async function* createBlobReaderIterable(reader, options = kEmptyObject) { const { getReadError } = options; let wakeup = PromiseWithResolvers(); - reader.setWakeup(wakeup.resolve); + let immediate; + reader.setWakeup(() => { + if (typeof immediate === 'undefined') { + // Postpone the execution to the next steps of the event loop + immediate = setImmediate(() => { + immediate = undefined; + wakeup.resolve?.(); + }); + } + }); try { while (true) { @@ -624,7 +639,6 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { if (blocked) { const fin = await wakeup.promise; wakeup = PromiseWithResolvers(); - reader.setWakeup(wakeup.resolve); // If the wakeup was triggered by FIN (EndReadable), the DataQueue // is capped. Continue the loop to pull again -- the next pull will // return EOS. Without this, a race between the data notification From e0521f54495ac83b64ea76e964a98ebbed0408d8 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 18:59:22 +0200 Subject: [PATCH 2/5] quic: Update lib/internal/blob.js more elegantly Co-authored-by: James M Snell --- lib/internal/blob.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index acaf8b4ac9b081..cfef53ee95bc32 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -477,13 +477,12 @@ function createBlobReaderStream(reader) { // when new data is available after a STATUS_BLOCK. let immediate; reader.setWakeup(() => { - if (this.pendingPulls.length > 0 && - typeof immediate === 'undefined') { - // Postpone the execution to the next steps of the event loop - immediate = setImmediate(() => { + if (this.pendingPulls.length > 0) { + immediate ??= setImmediate(() => { immediate = undefined; this.readNext(c); - }); + }) + } } }); }, From 5f2ea3333c0e52ee3e365bd2686003c54e173ae4 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 19:36:59 +0200 Subject: [PATCH 3/5] quic: fix lib/internal/blob.js --- lib/internal/blob.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index cfef53ee95bc32..dff1810ce82983 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -481,8 +481,7 @@ function createBlobReaderStream(reader) { immediate ??= setImmediate(() => { immediate = undefined; this.readNext(c); - }) - } + }); } }); }, From 0990b48167c7ef6fd4986f7b06892fc30e54a703 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 6 Jul 2026 07:22:27 +0200 Subject: [PATCH 4/5] quic: Apply suggestion from @jasnell for changing setImmediate Co-authored-by: James M Snell --- lib/internal/blob.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index dff1810ce82983..3324b0e323a6cb 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -583,13 +583,10 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { let wakeup = PromiseWithResolvers(); let immediate; reader.setWakeup(() => { - if (typeof immediate === 'undefined') { - // Postpone the execution to the next steps of the event loop - immediate = setImmediate(() => { - immediate = undefined; - wakeup.resolve?.(); - }); - } + immediate ??= setImmediate(() => { + immediate = undefined; + wakeup.resolve?.(); + }); }); try { From 2b28486fdd26ad2d2b07fc3f7e4b35a24f8a59c1 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 18 Jul 2026 11:36:33 +0200 Subject: [PATCH 5/5] quic: fix weakly weakup assignment --- lib/internal/blob.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 3324b0e323a6cb..e91542681970c1 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -476,14 +476,14 @@ function createBlobReaderStream(reader) { // Lazily register a wakeup callback that the C++ side can invoke // when new data is available after a STATUS_BLOCK. let immediate; - reader.setWakeup(() => { + this.wakeup = () => { if (this.pendingPulls.length > 0) { immediate ??= setImmediate(() => { immediate = undefined; this.readNext(c); }); } - }); + }; }, pull(c) { const { promise, resolve, reject } = PromiseWithResolvers();