From 4ee564b21d4bf882888b5f43fe2ee049c8d07689 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 21 Jun 2026 14:04:09 +0200 Subject: [PATCH 1/7] 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 fde3d86b430b04b10d39d76f90f9d008ab1135ea Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 18:59:22 +0200 Subject: [PATCH 2/7] 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 4627799c6f541ff50c9f8a2f72bacb34f05bc3ca Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 5 Jul 2026 19:36:59 +0200 Subject: [PATCH 3/7] 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 6b631e35c0fea9feab07e109a818aa57a7dfb289 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Mon, 6 Jul 2026 07:22:27 +0200 Subject: [PATCH 4/7] 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 04a8c62d495bccf0b3bbd2267df3f6c28ba7a790 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sat, 18 Jul 2026 11:36:33 +0200 Subject: [PATCH 5/7] 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(); From fff623f03313768b431f20787f55901671a4d43b Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 26 Jul 2026 22:17:23 +0200 Subject: [PATCH 6/7] quic: fix fin handling, so it is broken --- lib/internal/blob.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index e91542681970c1..be1f06ee93a483 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -582,7 +582,9 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { const { getReadError } = options; let wakeup = PromiseWithResolvers(); let immediate; - reader.setWakeup(() => { + let fin = false; + reader.setWakeup((setfin) => { + fin ||= setfin; immediate ??= setImmediate(() => { immediate = undefined; wakeup.resolve?.(); @@ -632,7 +634,7 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { if (error) throw error; if (blocked) { - const fin = await wakeup.promise; + await wakeup.promise; wakeup = PromiseWithResolvers(); // If the wakeup was triggered by FIN (EndReadable), the DataQueue // is capped. Continue the loop to pull again -- the next pull will From 4657936a110c86b6c2d461dd9e630dc08ed2d864 Mon Sep 17 00:00:00 2001 From: Marten Richter Date: Sun, 26 Jul 2026 22:28:21 +0200 Subject: [PATCH 7/7] quic: Fix lint --- lib/internal/blob.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/blob.js b/lib/internal/blob.js index be1f06ee93a483..e85438f5d5704b 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -582,7 +582,7 @@ async function* createBlobReaderIterable(reader, options = kEmptyObject) { const { getReadError } = options; let wakeup = PromiseWithResolvers(); let immediate; - let fin = false; + let fin = false; reader.setWakeup((setfin) => { fin ||= setfin; immediate ??= setImmediate(() => {