Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
f1fa832
fetch: yield the async-iterable pump per write when feeding an ArrayB…
robobun Jul 27, 2026
61faf95
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 27, 2026
4c9c022
fetch: hand a DirectPending body's pull() straight to the ResumableSink
robobun Jul 27, 2026
e63fb41
test: inherit stderr instead of piping unused
robobun Jul 27, 2026
67a25c5
ResumableSink: match JSDirectStreamController's end/close/error contract
robobun Jul 27, 2026
ae14d0e
ResumableSink: controller.error() with no argument aborts
robobun Jul 27, 2026
b201c14
ci: retrigger
robobun Jul 27, 2026
b95068b
Merge branch 'main' into farm/40eaa140/fetch-node-readable-body-spin
Jarred-Sumner Jul 29, 2026
5563870
fetch: drive async-iterable request bodies into the ResumableSink nat…
robobun Jul 29, 2026
6157425
async-iterable pump: recover the op from the bound pull instead of m_…
robobun Jul 29, 2026
653d7c2
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
robobun Jul 30, 2026
a0fa8a1
revert src/ to main for ResumableSink→JSSink consolidation
robobun Jul 30, 2026
0622880
Register FetchRequestBodySink JSSink in codegen + Sink.h + StartTag
robobun Jul 30, 2026
2f5078d
NetworkSink: report Writable::Backpressure + signal.ready on drain
robobun Jul 30, 2026
3bf80d4
fetch: route sink cancel/detach through cancel_request_body_sink
robobun Jul 30, 2026
a64ede4
Delete C++ ResumableSink pump (JSResumableSinkPumpOperation et al)
robobun Jul 30, 2026
9d6031b
multipart: replace ResumableSinkBackpressure with local UploadBackpre…
robobun Jul 30, 2026
c88d7b8
webcore/fetch: add FetchRequestBodySink JSSink type
robobun Jul 30, 2026
2cc910c
webcore: unregister resumable_sink module + re-exports
robobun Jul 30, 2026
ce04e51
FetchTasklet: swap sink to FetchRequestBodySink; drain/write off JSSink
robobun Jul 30, 2026
85d2f16
Register fetch/S3 request-stream .then() handlers in thenable dispatch
robobun Jul 30, 2026
485e4e0
s3: upload_stream uses NetworkSink + assign_to_stream
robobun Jul 30, 2026
8addbf6
FetchRequestBodySink: delegate write to write_request_data
robobun Jul 30, 2026
f8fa2be
Declare FetchRequestBodySink ABI in headers.h; drop ResumableSink stubs
robobun Jul 30, 2026
05e05e7
fetch request-body sink: single write_end_request via pump-promise path
robobun Jul 30, 2026
11d2c74
fetch: drop signal.clear in ignore_remaining; resolve flush on cancel
robobun Jul 30, 2026
ee22dd5
s3 upload_stream: use AnyPromise safe accessors + set_handled
robobun Jul 30, 2026
35f6c15
FetchRequestBodySink: gate backpressure on JS-side pending_bytes
robobun Jul 30, 2026
452b00f
FetchRequestBodySink: backpressure on every write, not at HWM
robobun Jul 30, 2026
b8acab7
FetchRequestBodySink: convert latin1/utf16 directly into the stream b…
robobun Jul 30, 2026
f2c8477
s3 upload_stream: capture end_promise before assign_to_stream; settle…
robobun Jul 30, 2026
e6d4f33
drop dead code: cancel_from_native, FetchRequestBodySink::{new,global…
robobun Jul 30, 2026
728441c
address review: utf16 length with replacement; s3 continue_stream bef…
robobun Jul 30, 2026
57fade4
s3: drop carried-over TODO comments in moved lines
robobun Jul 30, 2026
1b51f1a
s3: keep js_err alive across the Failure branch's JSC calls
robobun Jul 30, 2026
f7de8ae
s3: drop task.ended guard in handle_reject_stream
robobun Jul 30, 2026
e64e795
s3: trim handle_reject_stream comment
robobun Jul 30, 2026
fec25c2
s3: ref the MultiPartUpload across the single-file PUT
robobun Jul 30, 2026
3c9c618
address review nits: drop test timeout; gate NetworkSink end_promise …
robobun Jul 30, 2026
863eb83
FileSink: signal→source JsCell<SourceHandle>; to_result backpressure …
robobun Jul 30, 2026
d768829
streams: add SourceHandle<T: JsSinkAbi> tagged source enum
robobun Jul 30, 2026
0e14582
readStreamIntoSink: backpressure via m_onPull instead of flush(true)
robobun Jul 30, 2026
360d455
NetworkSink: signal→source SourceHandle, drop flush_promise; s3 callers
robobun Jul 30, 2026
90b13a3
Add SinkHandle enum; swap ByteStream pipe→sink with backpressure
robobun Jul 30, 2026
1ffd3b9
HTTPServerWritable: swap signal:Signal -> source:SourceHandle<Self>
robobun Jul 30, 2026
335efb7
JSSink::assign_to_stream: wrap bits into SourceHandle; drop jsvalue_ptr
robobun Jul 30, 2026
de7eb66
test: add native pipe backpressure/duplex/s3 tests; un-todo spawn cancel
robobun Jul 30, 2026
4bbf301
RequestContext: write_chunk/end_chunk + SinkHandle::http_response hookup
robobun Jul 30, 2026
6b46a40
FetchRequestBodySink: signal→source SourceHandle, drop flush_promise
robobun Jul 30, 2026
fb320ea
Add is_backed_up() to Posix/WindowsStreamingWriter
robobun Jul 30, 2026
b938b1b
ArrayBufferSink: swap signal field to SourceHandle<Self>
robobun Jul 30, 2026
c36aa30
Subprocess/ShellWritable use SourceHandle; drop SignalHandler impls
robobun Jul 30, 2026
b22ebd3
ValueBufferer: write_chunk->Writable + SinkHandle hookup; drop PipeHa…
robobun Jul 30, 2026
85ac139
fetch/s3: native ByteStream→sink bypass of assign_to_stream
robobun Jul 30, 2026
ab8ebff
Delete Signal/SignalVTable/SignalHandler/SinkSignal/Pipe/PipeHandler/…
robobun Jul 30, 2026
80b8e2c
NetworkSink: restore flush_promise for .writer() path
robobun Jul 30, 2026
16cd2e3
ByteStream: forward upstream Err to sink.end when back-pressured
robobun Jul 30, 2026
55e0c08
rsis: settle m_result when sink closes during backpressure wait
robobun Jul 30, 2026
76e6bce
FileSink: gate Backpressure to pump sources; resume on POSIX drain
robobun Jul 30, 2026
1e3ba61
is_backed_up(): track EAGAIN, not buffered bytes (match #33399)
robobun Jul 30, 2026
f559d76
Body.rs: fix ValueBufferer SinkHandle SAFETY comment attribution
robobun Jul 30, 2026
6336d00
subprocess: migrate FileSink signal→SourceHandle::Subprocess checks
robobun Jul 30, 2026
7294221
RequestContext: raw-ptr dispatch in on_writable_byte_stream; drop dea…
robobun Jul 30, 2026
3c5ad02
JSSink::assign_to_stream: pre-seed JSController to catch sync detach
robobun Jul 30, 2026
e22db5f
test: fix s3 stream arg, fetch RSS bound, serve backpressure observable
robobun Jul 30, 2026
7b6042d
JSSink::detach: unpipe ByteStream; FetchTasklet: bytes.sink.is_some()
robobun Jul 30, 2026
925745b
FetchRequestBodySink: restore flush_promise for async-iterable pump
robobun Jul 30, 2026
c5cb0f2
SourceHandle: treat JSController(0) placeholder as no-op in close/ready
robobun Jul 30, 2026
7cd300b
fetch: drive write_end_request on native ByteStream sink completion
robobun Jul 30, 2026
f375b8d
test: spawn stdin + Bun.write fed by native ByteStream (req/resp.body)
robobun Jul 30, 2026
99372d0
NetworkSink: keep flush_promise for writer().flush(); settle on S3 done
robobun Jul 30, 2026
ecde998
FetchRequestBodySink: delete dead flush_promise/pending_bytes fields
robobun Jul 30, 2026
5efc802
FileSink::assign_to_stream: add native ByteStream fast-path
robobun Jul 30, 2026
8479003
FetchRequestBodySink: restore flush_promise for async-iterable pump
robobun Jul 30, 2026
50be62c
test: correct Bun.write(Bun.file,Response) comment re: FileSink path
robobun Jul 30, 2026
4383813
FileSink: assert ByteStream sink unset before fast-path hookup
robobun Jul 30, 2026
9bc59a7
Blob: pre-check Source::Bytes before FileSink assign_to_stream
robobun Jul 30, 2026
534baaf
NetworkSink: drop source!=None flush short-circuit; keep flush_promise
robobun Jul 30, 2026
ca6df91
Blob: revert Source::Bytes pre-check; native path resolved 0 early
robobun Jul 30, 2026
d5a4b5a
trim comment-cop flagged comments
robobun Jul 30, 2026
213dd64
trim comment-cop flagged comments (all 40)
robobun Jul 30, 2026
41eac3e
SinkHandle::end(ValueBufferer): route Err through write thunk; drop d…
robobun Jul 30, 2026
940ee1f
SinkHandle::end: propagate full StreamError to sinks
robobun Jul 30, 2026
3a21562
FileReader: native SinkHandle fast-path for fetch request body
robobun Jul 30, 2026
efb4406
test: spawn+fetch native SourceHandle/SinkHandle end-to-end coverage
robobun Jul 30, 2026
07a9582
HTTPServerWritable: fire source.ready() on drain so readStreamIntoSin…
robobun Jul 30, 2026
e915bca
readDirectStream: leave m_onPull empty so onReady cannot re-enter pull
robobun Jul 30, 2026
21badb2
Detach native source on end_from_stream; keep S3 wrapper ref across f…
robobun Jul 30, 2026
b60016a
FileSink: keep JSController on Pending path; resume ByteStream from o…
robobun Jul 30, 2026
f8d1aa2
FileSink: snapshot SourceHandle before ready()/close() to avoid JsCel…
robobun Jul 30, 2026
90302f8
S3UploadStreamWrapper: release pump ref when S3 fails on native ByteS…
robobun Jul 30, 2026
83015f4
FileSink::on_write: call writer.end() on done+Drained outside pending…
robobun Jul 30, 2026
4d62746
trim comment-cop flagged on_write comment
robobun Jul 30, 2026
ff21b71
multipart: re-check queue after on_writable; ByteStream: surface pre-…
robobun Jul 30, 2026
235563e
trim comment-cop flagged comments
robobun Jul 30, 2026
785e879
test: skip spawn stderr backpressure test on Windows
robobun Jul 30, 2026
38481f7
Address review nits: NetworkSink write contract, ByteStream sink guar…
robobun Jul 30, 2026
2b63270
Trim review-bot-flagged comments
robobun Jul 31, 2026
aa59ec1
test: bound parked poll loop with 30s deadline
robobun Jul 31, 2026
fd8b824
FileSink: fall through to assign_to_stream when ByteStream already ha…
robobun Jul 31, 2026
f78b9b2
streams: pause upstream socket when native sink backpressures; write(…
robobun Jul 31, 2026
93cd4d0
streams: write() returns Promise on backpressure for HTTPServerWritab…
robobun Jul 31, 2026
8230dbf
Remove stray test registry artifacts
robobun Jul 31, 2026
3494ea7
FetchRequestBodySink: park JS pump on every scheduled write
robobun Jul 31, 2026
5dacd83
NewSource: replace 5 fn-ptr fields with producer:SourceHandle
robobun Jul 31, 2026
9ea865d
SinkHandle: collapse Http*/H3Response into ServerResponse(AnyRequestC…
robobun Jul 31, 2026
62dff10
FetchTasklet: re-sig stream callbacks; use SourceHandle producer
robobun Jul 31, 2026
149e5e9
RequestContext: re-sig write_chunk/end_chunk/on_request_body_stream_d…
robobun Jul 31, 2026
7fabfdd
streams: degenericize SourceHandle, add SinkKind + 3 producer variants
robobun Jul 31, 2026
1df1d05
AnyRequestContext: add write_chunk/end_chunk/on_request_body_stream_d…
robobun Jul 31, 2026
389fc3d
Add SINK_KIND to JsSinkAbi; degenericize assign_to_stream/detach
robobun Jul 31, 2026
3eb13d8
s3: hoist S3DownloadStreamWrapper, wire to SourceHandle::S3DownloadBody
robobun Jul 31, 2026
8cab178
streams: add UpstreamSource trait + impls; collapse SourceHandle arms
robobun Jul 31, 2026
f003ff4
ByteStream: route drain/cancel/attach signals through producer
robobun Jul 31, 2026
ec520b4
PendingValue: replace 3 stream fn-ptrs with producer:SourceHandle
robobun Jul 31, 2026
c292f99
webcore: add DownstreamSink trait; SinkHandle dispatches via it
robobun Jul 31, 2026
a4353f5
De-parameterize SourceHandle<X> fields + source() impls
robobun Jul 31, 2026
544e087
server: replace on_stream_drained fn-ptr with producer:ServerRequestBody
robobun Jul 31, 2026
503c27d
SourceHandle: type Subprocess/ShellWritable ptrs at call-sites
robobun Jul 31, 2026
af4ae58
Dead-code sweep: drop detach_abi dup, unused JsSinkAbi externs, inlin…
robobun Jul 31, 2026
55602a3
FetchRequestBodySink: degenericize source; allow dead_code in sink ex…
robobun Jul 31, 2026
69dd9f7
RequestContext: rename clear_*_drain_handler -> detach_request_body_p…
robobun Jul 31, 2026
2969578
streams: restore SAFETY comments + allow ptr-deref on UpstreamSource …
robobun Jul 31, 2026
bf5fde6
RequestContext: allow not_unsafe_ptr_arg_deref on write_chunk/end_chunk
robobun Jul 31, 2026
860eaf9
Sink: split decl_js_sink_externs per-subset, drop dead_code allow
robobun Jul 31, 2026
00203c2
s3: clear sink.source before clean-EOF end; settle sink.pending on re…
robobun Jul 31, 2026
776ed0f
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
robobun Jul 31, 2026
6a10dca
s3: drop pre-existing TODO comment surfaced by de-indent
robobun Jul 31, 2026
3fd09b0
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
1d7b29c
UpstreamSource/DownstreamSink: take &mut self, centralize unsafe dere…
robobun Jul 31, 2026
c28f981
SourceHandle/SinkHandle: BackRef<T> variants; Box::leak in fast-paths…
robobun Jul 31, 2026
3dc2bec
Drop SinkKind: unify controller onReady/onClose via JSReadableSinkCon…
robobun Jul 31, 2026
45d1b3f
RequestContext: restore raw-ptr dispatch for re-entrant write_chunk/e…
robobun Jul 31, 2026
687c7a8
webcore: drop dead DownstreamSink trait
robobun Jul 31, 2026
bffe087
FileSink: return Backpressure for FileReader source on pending write
robobun Jul 31, 2026
b8d9780
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
a8ca0cc
test(spawn): await cancel() via Promise.withResolvers instead of polling
robobun Jul 31, 2026
f781147
test: drop native-path tests that also pass via JS pump on main
robobun Jul 31, 2026
0f106b7
test(spawn): drop fetch-body-as-stdin bounded-memory test
robobun Jul 31, 2026
1ea7dc5
headers.h: drop dead per-sink __detachPtr/onClose/onReady CPP_DECLs
robobun Jul 31, 2026
ee20d5f
headers.h: drop remaining H3ResponseSink dead decls
robobun Jul 31, 2026
8210cae
test(spawn): fetch response.body → child stdin bounded-memory regress…
Jarred-Sumner Jul 31, 2026
f1c0f54
test(spawn): run FileSink leak iterations concurrently (debug+ASAN ti…
robobun Jul 31, 2026
c0a5be4
Merge remote-tracking branch 'origin/main' into farm/40eaa140/fetch-n…
Jarred-Sumner Jul 31, 2026
024bc9f
test: cover JS-pull spawn stdin, locked-after-attach, serve proxy bac…
Jarred-Sumner Jul 31, 2026
11418cd
test(spawn): await child.exited + close source in bounds-memory fixtu…
robobun Jul 31, 2026
b9025ef
Backpressure regression tests; normalize resourceUsage().maxRSS units
Jarred-Sumner Jul 31, 2026
37f6cd6
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
aec4a56
boringssl: remove duplicate is_safe_alt_name from merge
robobun Jul 31, 2026
d6d45d1
test(serve): measure stall-phase RSS, not whole-process maxRSS, in pr…
robobun Jul 31, 2026
c50455e
test(s3): drop vacuous RSS bound from ByteStream -> NetworkSink uploa…
robobun Jul 31, 2026
d9d0046
test(spawn): parallelize stdin FileSink leak iterations and drop expl…
robobun Jul 31, 2026
440f8c3
Remove orphaned setHandlers builtin name
robobun Jul 31, 2026
688a574
test: move runFixtureMaxRSS/emptyProcessMaxRSS helpers into harness.ts
robobun Jul 31, 2026
b07816c
test(fetch): wait for body bytes at the target before asserting the s…
Jarred-Sumner Jul 31, 2026
3cd26a7
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
b58eb63
Revert "test(serve): measure stall-phase RSS, not whole-process maxRS…
Jarred-Sumner Jul 31, 2026
45d0d3b
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
2ebb0fc
test: halve the bounded-memory fixtures' payload to 128 MB
Jarred-Sumner Jul 31, 2026
f3d5e30
Body: do not report a Locked body's content-length as allocated memory
robobun Jul 31, 2026
2ebc01e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 31, 2026
7cc45af
Trim long comments flagged by comment-cop
robobun Jul 31, 2026
d6f092d
bench: add fetch streaming/backpressure harness
Jarred-Sumner Jul 31, 2026
6b34885
Merge branch 'farm/40eaa140/fetch-node-readable-body-spin' of https:/…
Jarred-Sumner Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions bench/fetch/streaming-backpressure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Streaming/backpressure benchmark for fetch bodies. Runs under bun or node.
//
// [MB=64] [STALL=0] [CONCURRENCY=1] bun bench/fetch/streaming-backpressure.mjs <scenario>
//
// scenarios: fetch-to-fetch | readable-body | download-proxy
// prints one JSON line: elapsedMs, peakRssMB (sampled), cpuMs, per-run totals.
// All servers are in-process net/http servers on loopback, so the numbers are
// the runtime's own overhead moving MB-per-stream x CONCURRENCY streams.
// STALL>0 pauses the consumer that long before draining (rate-mismatch case).
import http from "node:http";
import net from "node:net";
import { spawn } from "node:child_process";

Check warning on line 12 in bench/fetch/streaming-backpressure.mjs

View check run for this annotation

Claude / Claude Code Review

Dead import: spawn from node:child_process in bench file

`spawn` is imported from `node:child_process` at line 12 but never used — the three scenarios (`fetch-to-fetch`, `readable-body`, `download-proxy`) use only `net`, `http`, `Readable`, and `pipeline`. The PR description's benchmark table lists a `spawn-pipe` scenario that isn't in the file's scenario table, so this is a leftover from an earlier iteration. Per REVIEW.md "Delete dead code in the same PR that makes it dead" — remove the import.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 spawn is imported from node:child_process at line 12 but never used — the three scenarios (fetch-to-fetch, readable-body, download-proxy) use only net, http, Readable, and pipeline. The PR description's benchmark table lists a spawn-pipe scenario that isn't in the file's scenario table, so this is a leftover from an earlier iteration. Per REVIEW.md "Delete dead code in the same PR that makes it dead" — remove the import.

Extended reasoning...

What the finding is

bench/fetch/streaming-backpressure.mjs is a new file added by this PR. Line 12 reads:

import { spawn } from "node:child_process";

but spawn is never referenced anywhere in the 84-line file body. The file's scenario table at line 78 defines exactly three scenarios — fetch-to-fetch, readable-body, and download-proxy — implemented by fetchToFetch(), readableBody(), and downloadProxy() respectively. Those functions use only net.createServer/net.connect, http.createServer, Readable.from/Readable.fromWeb, pipeline, and fetch. There is no call to spawn( anywhere in the file.

Why it's here

The PR description's Benchmark section lists six scenarios including spawn-pipe (fetch.body -> spawn stdin), file-upload, and js-stream-upload, but the checked-in bench file's scenario table contains only three of them. The commit message on the tip of the branch is "bench: add fetch streaming/backpressure harness", so this file was pared down between when the PR-description numbers were captured and when the harness was committed. The spawn import survived the trim; the scenario that used it did not.

Why existing checks don't catch it

Nothing lints bench/ files for unused imports — they are neither shipped runtime code (not under src/) nor tests (not under test/). ESM does not warn on an unused named import binding, and the file runs identically with or without the line (node:child_process is a built-in module with no side-effecting top-level code).

Step-by-step proof

  1. The diff adds bench/fetch/streaming-backpressure.mjs as a new file (new file mode 100644), so its full contents are visible in the diff hunk.
  2. Line 12 of the added file: import { spawn } from "node:child_process";.
  3. Grep the file body for spawn: the only occurrence is the import statement itself. The three scenario functions (lines 49-76) contain no reference to spawn, and the scenario dispatch table (line 78: const table = { "fetch-to-fetch": fetchToFetch, "readable-body": readableBody, "download-proxy": downloadProxy };) has no spawn-pipe entry.
  4. The PR description's benchmark table (in the "Benchmark" section) includes a row spawn-pipe (fetch.body -> spawn stdin), confirming a spawn-based scenario existed in an earlier iteration and was removed from the file but not from the imports.
  5. REVIEW.md, "Code style & idioms reviewers enforce": "Delete dead code in the same PR that makes it dead (required scope — name the deletions in the description): superseded implementations, helpers whose last caller you rewired…".

Impact

None at runtime. This is a bench harness, not shipped code and not part of the test suite; an unused ESM binding to a built-in module has no observable effect on the benchmark's output, timing, or memory numbers. The only cost is noise: a reader looking for the spawn-pipe scenario referenced in the PR description will find the import, grep for its use, and find nothing.

How to fix

Delete line 12:

 import http from "node:http";
 import net from "node:net";
-import { spawn } from "node:child_process";
 import { Readable } from "node:stream";
 import { pipeline } from "node:stream/promises";

(Optionally, either restore the spawn-pipe scenario to the file or drop it from the PR-description benchmark table so the two agree — but that's a separate PR-description-mismatch nit.)

import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const scenario = process.argv[2];
const MB = Number(process.env.MB ?? 64), STALL = Number(process.env.STALL ?? 0), C = Number(process.env.CONCURRENCY ?? 1);
const CHUNK = Buffer.alloc(64 * 1024, 0x47), COUNT = MB * 16, TOTAL = CHUNK.length * COUNT;
const rt = typeof Bun !== "undefined" ? "bun " + Bun.version_with_sha : "node " + process.version;

let peak = 0;
const rssNow = () => (process.memoryUsage.rss ? process.memoryUsage.rss() : process.memoryUsage().rss);
const sampler = setInterval(() => { const r = rssNow(); if (r > peak) peak = r; }, 10);
const t0 = Date.now(), cpu0 = process.cpuUsage();
setTimeout(() => { console.log(JSON.stringify({ scenario, MB, STALL, C, runtime: rt, TIMEOUT: true, peakRssMB: Math.round(peak / 1048576) })); process.exit(1); }, 300000).unref();

async function makeSource() {
const srv = net.createServer(sock => {
sock.write("HTTP/1.1 200 OK\r\ncontent-length: " + TOTAL + "\r\nconnection: close\r\n\r\n");
let n = 0;
const pump = () => { while (n < COUNT) { n++; if (!sock.write(CHUNK)) return sock.once("drain", pump); } sock.end(); };
pump();
sock.on("error", () => {});
});
await new Promise(r => srv.listen(0, "127.0.0.1", r));
return { url: `http://127.0.0.1:${srv.address().port}/`, close: () => srv.close() };
}
async function makeSlowSink() {
let done; const drained = new Promise(r => (done = r));
const srv = net.createServer(sock => {
let got = 0; if (STALL) { sock.pause(); setTimeout(() => sock.resume(), STALL); }
sock.on("data", d => { got += d.length; if (got >= TOTAL) done(got); });
sock.on("error", () => {});
});
await new Promise(r => srv.listen(0, "127.0.0.1", r));
return { url: `http://127.0.0.1:${srv.address().port}/`, drained, close: () => srv.close() };
}

async function fetchToFetch() {
const source = await makeSource(), sink = await makeSlowSink();
const up = await fetch(source.url);
fetch(sink.url, { method: "POST", body: up.body, duplex: "half" }).catch(() => {});
await sink.drained; source.close(); sink.close();
}
async function readableBody() {
const sink = await makeSlowSink();
const body = Readable.from((function* () { for (let i = 0; i < COUNT; i++) yield CHUNK; })());
fetch(sink.url, { method: "POST", body, duplex: "half" }).catch(() => {});
await sink.drained; sink.close();
}
async function downloadProxy() {
const source = await makeSource();
const server = http.createServer(async (req, res) => {
const up = await fetch(source.url);
res.writeHead(200);
await pipeline(Readable.fromWeb(up.body), res).catch(() => {});
});
await new Promise(r => server.listen(0, "127.0.0.1", r));
await new Promise(resolve => {
let got = 0;
const c = net.connect(server.address().port, "127.0.0.1", () => { if (STALL) { c.pause(); setTimeout(() => c.resume(), STALL); } c.write("GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n"); });
c.on("data", d => { got += d.length; if (got >= TOTAL) { c.destroy(); resolve(); } });
c.on("error", () => {});
});
server.close(); source.close();
}
const table = { "fetch-to-fetch": fetchToFetch, "readable-body": readableBody, "download-proxy": downloadProxy };
const fn = table[scenario];
if (!fn) { console.error("scenarios:", Object.keys(table).join(" ")); process.exit(2); }
await Promise.all(Array.from({ length: C }, (_, i) => fn(i)));
clearInterval(sampler);
const cpu = process.cpuUsage(cpu0);
console.log(JSON.stringify({ scenario, MB, STALL, C, totalGB: +(TOTAL * C / 2**30).toFixed(2), runtime: rt, elapsedMs: Date.now() - t0, peakRssMB: Math.round(peak / 1048576), cpuMs: Math.round((cpu.user + cpu.system) / 1000) }));
process.exit(0);
12 changes: 4 additions & 8 deletions docs/runtime/streams.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,22 @@ When using a direct `ReadableStream`, the destination handles all chunk queueing

### Handling backpressure

`controller.write()` returns the number of bytes written. When the destination's internal buffer is full (for example, a slow HTTP client), it returns a **negative number** instead. The chunk is still accepted — the negative return is a signal to pause and wait for the destination to drain.

To wait for the drain, `await controller.flush(true)`:
`controller.write()` returns the number of bytes written, or a **pending `Promise<number>`** when the destination's internal buffer is full (for example, a slow HTTP client). The chunk is accepted either way; the promise resolves once the destination has drained, so `await`ing the result is enough:

```ts
const stream = new ReadableStream({
type: "direct",
async pull(controller) {
for (const chunk of chunks) {
const n = controller.write(chunk);
if (typeof n === "number" && n < 0) {
// destination is backed up; wait for it to drain before writing more
await controller.flush(true);
}
await controller.write(chunk);
}
controller.close();
},
});
```

`await controller.flush(true)` is equivalent and can be used after a write returns a `Promise`.

For default (non-`direct`) `ReadableStream`s and async-generator response bodies, Bun applies this backpressure automatically — the producer is paused while the destination is backed up.

---
Expand Down
20 changes: 7 additions & 13 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,22 +700,16 @@ interface ReadableStreamDirectController {
/**
* Write a chunk directly to the destination.
*
* Returns the number of bytes written, or a **negative number** when the
* destination's internal buffer is full (backpressure). When negative, the
* chunk *was* accepted; pause writing and `await controller.flush(true)`,
* which resolves once the destination has drained:
* Returns the number of bytes written, or a **pending `Promise<number>`**
* when the destination's internal buffer is full (backpressure). The chunk
* *was* accepted either way; `await`ing the result is enough:
*
* ```ts
* const n = controller.write(chunk);
* if (typeof n === "number" && n < 0) {
* await controller.flush(true);
* }
* await controller.write(chunk);
* ```
*
* For some destinations (e.g. {@link Bun.FileSink} on Windows pipes) the
* write itself is asynchronous and a `Promise<number>` is returned instead;
* the `typeof` check above skips the backpressure wait for those — the
* promise carries its own flow control.
* The promise resolves once the destination has drained.
* `await controller.flush(true)` is equivalent.
Comment thread
robobun marked this conversation as resolved.
*/
write(data: Bun.BufferSource | ArrayBuffer | string): number | Promise<number>;
end(): number | Promise<number>;
Expand All @@ -724,7 +718,7 @@ interface ReadableStreamDirectController {
*
* @param wait When `true`, the returned promise resolves only once the
* destination has drained its own internal buffer (i.e. backpressure has
* cleared). Use this after {@link write} returns a negative value.
* cleared). Use this after {@link write} returns a `Promise`.
*/
flush(wait?: boolean): number | Promise<number>;
start(): void;
Expand Down
Loading
Loading