Skip to content

Response.clone(): share tee chunk refs instead of StructuredClone per branch - #35843

Merged
Jarred-Sumner merged 5 commits into
mainfrom
farm/e019fca7/body-clone-share-tee-chunks
Jul 31, 2026
Merged

Jarred-Sumner merged 5 commits into
mainfrom
farm/e019fca7/body-clone-share-tee-chunks

Conversation

@robobun

@robobun robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator

What

Response.clone() / Request.clone() on a stream body deep-copied every chunk into the second tee branch. An N-deep clone chain therefore retained N independent copies of every body chunk.

// {"depth":100,"leafReadBytes":10485760,"rssDeltaMB":1042}   (node: ~55)
const MB = 1 << 20, N = 100, SZ = 10 * MB; let pulled = 0;
const big = () => new ReadableStream({
  pull(c) { if (pulled >= SZ) { c.close(); return; } c.enqueue(new Uint8Array(65536)); pulled += 65536; }
});
const base = process.memoryUsage().rss;
let cur = new Response(big()); const chain = [cur];
for (let i = 0; i < N; i++) { cur = cur.clone(); chain.push(cur); }
const n = (await chain.at(-1).arrayBuffer()).byteLength;
console.log({ depth: N, leafReadBytes: n, rssDeltaMB: ((process.memoryUsage().rss - base) / MB).toFixed(1) });

Memory (RSS delta, 10 MB streaming body, only the leaf clone is read)

depth bun before bun (this PR) node v26.3.0 deno 2.9.4
10 130 MB 15 MB 43 MB 123 MB
30 335 MB 15 MB 49 MB 329 MB
100 1043 MB 16 MB 56 MB 1035 MB

bun is now flat across depth and below node (node still allocates one Uint8Array wrapper per tee level; bun enqueues the same JSValue).

Cause

ReadableStream__tee (the extern called from Rust Body clone) passed cloneForBranch2 = true to readableStreamTee, which routed each chunk through structuredCloneForStream (ArrayBuffer::slice memcpy) before enqueuing into branch2. In a clone chain each level copies again, so a single source chunk ends up duplicated once per tee level.

The fetch spec's "clone a body" step does say ReadableStreamTee(stream, true), but Node (undici), Chrome, and Firefox all share the chunk reference between branches instead. Deno follows the spec and exhibits the same O(depth × bytes) growth. See whatwg/streams#1156 for the spec discussion.

Fix

Pass cloneForBranch2 = false in ReadableStream__tee. The cloned branch now receives the exact same Uint8Array object the original branch does, so a clone chain retains one copy of the body regardless of depth.

This supersedes #33130's right-sized-copy optimisation: instead of copying only the view's bytes, we don't copy at all. Branch1 always received the original reference, so branch2 sharing that reference never retains more than branch1 already did. The now-unreachable cloneForBranch2 = true path (structuredCloneChunk, m_shouldClone, the $structuredCloneForStream private global and host function, and CloneMode::Full) is deleted; the cloneForBranch2 parameter is kept on readableStreamDefaultTee to mirror the spec signature and asserted false.

ReadableStream.prototype.tee() was already passing false; only the body-clone path changes. Byte-controller tee (type: "bytes") is unchanged and still copies per spec.

Verification

test/js/web/fetch/body-clone.test.ts gains a 50-deep clone chain RSS check (fails at ~219 MB on main, passes at <50 MB with this change) and the existing per-chunk tests now assert clonedChunk === originalChunk. All 63 tests in the file pass.


[review] gate passed · iteration 5 · 11 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/fetch/body-clone.test.ts
bun test v1.4.0 (0edb26454)

test/js/web/fetch/body-clone.test.ts:
(pass) Request with streaming body can be cloned [17.11ms]
(pass) Response with streaming body can be cloned [9.26ms]
(pass) Request with large streaming body can be cloned [17.40ms]
(pass) Request with large streaming body can be cloned (pull) [29.66ms]
(pass) Response with chunked streaming body can be cloned [50.22ms]
(pass) Request with streaming body can be cloned multiple times [11.05ms]
(pass) Request with string body can be cloned [5.62ms]
(pass) Response with string body can be cloned [5.08ms]
(pass) Request with ArrayBuffer body can be cloned [8.25ms]
(pass) Response with ArrayBuffer body can be cloned [6.60ms]
(pass) Request with Uint8Array body can be cloned [6.58ms]
(pass) Response with Uint8Array body can be cloned [7.54ms]
(pass) Request with mixed body types can be cloned [16.51ms]
(pass) Response with mixed body types can be cloned [14.65ms]
(pass) Request with non-ASCII string body can be cloned [5.97ms]
(pass) Resp
... (truncated)

release without fix: 12 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/web/fetch/body-clone.test.ts:
(pass) Request with streaming body can be cloned [2.85ms]
(pass) Response with streaming body can be cloned [0.45ms]
(pass) Request with large streaming body can be cloned [4.24ms]
(pass) Request with large streaming body can be cloned (pull) [4.45ms]
(pass) Response with chunked streaming body can be cloned [33.70ms]
(pass) Request with streaming body can be cloned multiple times [0.42ms]
(pass) Request with string body can be cloned [0.10ms]
(pass) Response with string body can be cloned [0.07ms]
(pass) Request with ArrayBuffer body can be cloned [0.18ms]
(pass) Response with ArrayBuffer body can be cloned [0.11ms]
(pass) Request with Uint8Array body can be cloned [0.09ms]
(pass) Response with Uint8Array body can be cloned [0.07ms]
(pass) Request with mixed body types can be cloned [0.72ms]
(pass) Response with mixed body types can be cloned [0.30ms]
(pass) Request with non-ASCII string body can be cloned [0.10ms]
(pass) Response with non-ASCII string body can be cloned [0.07ms]
(pass) Request with streaming non-ASCII body can be cloned [0.24ms]
(pass) Response with streaming non-ASCII bod
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/fetch/body-clone.test.ts
bun test v1.4.0 (0edb26454)

test/js/web/fetch/body-clone.test.ts:
(pass) Request with streaming body can be cloned [13.55ms]
(pass) Response with streaming body can be cloned [13.22ms]
(pass) Request with large streaming body can be cloned [16.48ms]
(pass) Request with large streaming body can be cloned (pull) [28.65ms]
(pass) Response with chunked streaming body can be cloned [50.55ms]
(pass) Request with streaming body can be cloned multiple times [10.94ms]
(pass) Request with string body can be cloned [5.51ms]
(pass) Response with string body can be cloned [4.90ms]
(pass) Request with ArrayBuffer body can be cloned [8.64ms]
(pass) Response with ArrayBuffer body can be cloned [6.89ms]
(pass) Request with Uint8Array body can be cloned [7.01ms]
(pass) Response with Uint8Array body can be cloned [7.23ms]
(pass) Request with mixed body types can be cloned [17.22ms]
(pass) Response with mixed body types can be cloned [15.54ms]
(pass) Request with non-ASCII string body can be cloned [6.37ms]
(pass) Res
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 691ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/459] cxx obj/vendor/boringssl/crypto/trust_token/trust_token.cc.o
[2/459] cxx obj/vendor/boringssl/crypto/trust_token/voprf.cc.o
[3/459] cxx obj/vendor/boringssl/crypto/x509/a_sign.cc.o
[4/459] cxx obj/vendor/boringssl/crypto/x509/a_verify.cc.o
[5/459] cxx obj/vendor/boringssl/crypto/x509/i2d_pr.cc.o
[6/459] cxx obj/vendor/boringssl/crypto/trust_token/pmbtoken.cc.o
[7/459] gen cpp.rs (cppbind)
[8/459] cxx obj/vendor/boringssl/crypto/x509/t_x509a.cc.o
[9/459] cxx obj/vendor/boringssl/crypto/x509/algorithm.cc.o
[10/459] cxx obj/vendor/boringssl/crypto/x509/v3_akeya.cc.o
[11/459] cxx obj/vendor/boringssl/crypto/x509/v3_alt.cc.o
[12/459] cxx obj/vendor/boringssl/crypto/x509/t_req.cc.o
[13/459] cxx obj/vendor/boringssl/crypto/x509/v3_akey.cc.o
[14/459] cxx obj/vendor/boringssl/crypto/x509/t_x509.cc.o
[15/459] cxx obj/vendor/boringssl/crypto/x509/t_crl.cc.o
[16/459] cxx obj/vendor/boringssl/crypto/x509/rsa_pss.cc.o
[17/459] cxx obj/vendor/boringssl/crypto/x509/asn1_gen.cc.o
[18/459] cxx obj/vendor/boringssl/c
... (truncated)
diff hotspot
src/js/builtins.d.ts                               |  2 -
 src/js/builtins/BunBuiltinNames.h                  |  2 -
 src/jsc/bindings/ZigGlobalObject.cpp               |  2 -
 src/jsc/bindings/webcore/StructuredClone.cpp       | 84 ----------------------
 src/jsc/bindings/webcore/StructuredClone.h         |  2 -
 .../bindings/webcore/streams/JSReadableStream.cpp  |  2 +-
 .../bindings/webcore/streams/JSStreamTeeState.h    |  3 -
 .../webcore/streams/ReadableStreamOperations.cpp   | 58 ++-------------
 .../bindings/webcore/streams/WebStreamsExports.cpp |  5 +-
 .../bindings/webcore/streams/WebStreamsInternals.h | 11 ++-
 test/js/web/fetch/body-clone.test.ts               | 84 ++++++++++++++++------
 11 files changed, 81 insertions(+), 174 deletions(-)

gate history · 5 passed · 0 rejected · iteration 5

evidence per changed file
file                                                      reads  edits  tests
src/js/builtins.d.ts                                          2      3      0
src/js/builtins/BunBuiltinNames.h                             2      2      0
src/jsc/bindings/ZigGlobalObject.cpp                          2      3      0
src/jsc/bindings/webcore/StructuredClone.cpp                  3      3      0
src/jsc/bindings/webcore/StructuredClone.h                    2      3      0
src/jsc/bindings/webcore/streams/JSReadableStream.cpp         1      1      0
src/jsc/bindings/webcore/streams/JSStreamTeeState.h           1      1      0
…c/bindings/webcore/streams/ReadableStreamOperations.cpp      6      2      0
src/jsc/bindings/webcore/streams/WebStreamsExports.cpp        2      4      0
src/jsc/bindings/webcore/streams/WebStreamsInternals.h        2      3      0
test/js/web/fetch/body-clone.test.ts                          3      6      0

@robobun

robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green. All review feedback addressed through 39c18f8; body-clone.test.ts (63 tests) passes on every lane that ran it.

CI build 82012: 135 jobs passed, 5 build-bun jobs timed out (darwin-x64, linux-x64-musl, linux-aarch64-musl, freebsd-x64, windows-x64; same pattern on main builds 81770/81444/80215) cascading 54 downstream jobs. Test failures that did surface are unrelated to this diff and tagged flaky: bun-install-registry hoisting (win-aarch64), fetch-leak abort-stream RSS (darwin-aarch64), valkey pub/sub timeout, 20144 SIGINT/SIGKILL, require-cache RSS 67/64 MB threshold.

Repro: 10 MB streaming body cloned 100 deep: rssDeltaMB 1043 → 16 (node: 56, deno: 1035).

Ready for review/merge; needs a maintainer to push past the build-lane timeouts.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Stream-specific structured-clone bindings were removed. Readable stream teeing now shares materialized chunks between branches. Fetch tests verify identity, concurrent consumption, and bounded memory use across deep clone chains.

Stream clone removal

Layer / File(s) Summary
Remove stream clone contracts
src/js/builtins.d.ts, src/js/builtins/BunBuiltinNames.h, src/jsc/bindings/ZigGlobalObject.cpp, src/jsc/bindings/webcore/StructuredClone.cpp, src/jsc/bindings/webcore/StructuredClone.h
Removed stream-specific builtin names, globals, declarations, and host-function implementations.
Share chunks between tee branches
src/jsc/bindings/webcore/streams/JSReadableStream.cpp, src/jsc/bindings/webcore/streams/JSStreamTeeState.h, src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp, src/jsc/bindings/webcore/streams/WebStreamsExports.cpp, src/jsc/bindings/webcore/streams/WebStreamsInternals.h
Removed the clone-selection argument and state. Default teeing enqueues the same chunk reference to both branches.
Validate shared chunks and clone chains
test/js/web/fetch/body-clone.test.ts
Tests now verify object and backing-buffer identity, concurrent branch consumption, and bounded memory use across 50 response clones.

Suggested reviewers: alii, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: stream tee branches share chunk references instead of using structured cloning.
Description check ✅ Passed The description explains the change, cause, fix, impact, benchmarks, and verification results, although its headings differ from the template.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:56 PM PT - Jul 25th, 2026

❌ @robobun, your commit d28dea9 has 1 failures in Build #82012 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35843

That installs a local version of the PR into your bun-35843 executable, so you can run:

bun-35843 --bun

@claude claude Bot left a comment

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.

Beyond the inline nits, I confirmed ReadableStream__tee is only reached via the Rust Body clone path (ReadableStream.rs), so no other consumer's semantics change; and readableByteStreamTee takes no cloneForBranch2 parameter, so byte-controller tee is unaffected as the description states. Deferring to a human because this is an observable spec deviation on a Web API (chunk mutations now alias across branches) — well-justified against Node/Chrome/Firefox/Deno, but worth a maintainer sign-off.

Extended reasoning...

Verified the two claims the correctness of the one-line change rests on: (1) grep shows ReadableStream__tee is declared in WebStreamsInternals.h, defined in WebStreamsExports.cpp, and called only from src/runtime/webcore/ReadableStream.rs's tee(), which is the body-clone helper — so flipping cloneForBranch2 here cannot affect any path other than Request/Response body cloning; (2) readableByteStreamTee has no cloneForBranch2 parameter, so type: "bytes" streams keep their per-spec copy behavior. The two inline findings (now-dead structuredCloneChunk/m_shouldClone path; the backing assertion in the rewritten fetch test no longer measures the clone branch) cover the remaining review feedback.

Comment thread test/js/web/fetch/body-clone.test.ts Outdated
Comment thread src/jsc/bindings/webcore/streams/WebStreamsInternals.h Outdated
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun fix the review comments but also show a benchmark wiht memory usage comparisons for bun, node, and deno in the PR body for the same code.

@robobun
robobun requested a review from alii as a code owner July 26, 2026 01:30
Comment thread src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp Outdated
Comment thread src/jsc/bindings/webcore/streams/WebStreamsExports.cpp Outdated
Comment thread src/jsc/bindings/webcore/streams/WebStreamsInternals.h Outdated
Comment thread src/jsc/bindings/webcore/streams/WebStreamsExports.cpp
Comment thread src/jsc/bindings/webcore/streams/WebStreamsInternals.h
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

@Jarred-Sumner done. Review comments addressed in 25f5387 and 6c4deb9 (dead cloneForBranch2=true path removed, fetch test tightened, parameter dropped). Benchmark table is in the PR body now.

Measured RSS delta for a 10 MB streaming body, reading only the last clone in the chain:

depth bun before bun (this PR) node v26.3.0 deno 2.9.4
10 130 MB 15 MB 43 MB 123 MB
30 335 MB 15 MB 49 MB 329 MB
100 1043 MB 16 MB 56 MB 1035 MB

Deno turns out to implement the spec's per-chunk StructuredClone, so it shows the same growth bun did; I've corrected the code comments accordingly. Node shares the chunk reference and stays roughly flat; bun now enqueues the same JSValue into both branches and sits below node.

Comment thread src/jsc/bindings/webcore/StructuredClone.cpp Outdated

@claude claude Bot left a comment

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.

All three rounds of my earlier feedback are addressed in 25f5387/6c4deb9/39c18f8 and no new issues surfaced this pass. Deferring only because this is a user-observable spec deviation on a Web API (cloned branches now share chunk references, so mutating a chunk in one branch is visible in the other) — worth an explicit maintainer sign-off on that semantic.

What was reviewed:

  • Confirmed readableStreamTee/readableStreamDefaultTee have exactly the two updated call sites; no stragglers with the old bool arg.
  • Confirmed the only remaining cloneArrayBuffer symbol is the unrelated file-static in JSReadableByteStreamController.cpp.
  • m_shouldClone, structuredCloneChunk, and both private-global plumbing chains are fully gone.
  • New RSS test branches on isASAN/isDebug with a bound (80/120 MB) well below the ~200 MB unfixed baseline.
Extended reasoning...

Overview

The PR flips Response.clone()/Request.clone()'s tee from cloneForBranch2 = true (StructuredClone every chunk into branch2) to sharing the same JSValue in both branches, matching Node/Chrome/Firefox and fixing O(depth × bytes) retention on clone chains. Follow-up commits deleted every piece of the now-dead cloneForBranch2 = true machinery: structuredCloneChunk, JSStreamTeeState::m_shouldClone, the $structuredCloneForStream and $cloneArrayBuffer private globals (host functions, builtin names, GlobalPropertyInfo entries, .d.ts declarations), and the bool cloneForBranch2 parameter itself.

Security risks

None identified. Sharing chunk references between tee branches means user code can mutate a chunk and observe it in the sibling branch, but that's already the behavior of ReadableStream.prototype.tee() and of Node/Chrome/Firefox for body clones — it's not a new capability.

Level of scrutiny

Medium-high. The core change is one line, but it's an intentional spec deviation (fetch spec says cloneForBranch2 = true; whatwg/streams#1156 tracks the disagreement) with user-observable semantics, and it triggered ~150 lines of dead-code deletion across the C++ streams implementation and private-global plumbing. Jarred has been directing the follow-up work but hasn't formally approved; the semantic change deserves an explicit maintainer ack.

Other factors

All feedback from my three prior review passes is addressed. Repo-wide grep confirms no orphaned callers of the resignatured tee functions and no remaining references to the deleted private globals beyond the unrelated file-static cloneArrayBuffer in JSReadableByteStreamController.cpp. The new RSS regression test correctly branches its threshold on isASAN/isDebug per REVIEW.md, and the reworked fetch test asserts reference identity (toBe) which is the strongest invariant for this change. The comment-cop bot's flags on the spec-deviation comments were reasonably rebutted — they document a deliberate upstream deviation with a citation, which is exactly what REVIEW.md says comments should carry.

@claude claude Bot left a comment

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.

All three earlier review nits are addressed as of 39c18f8; nothing new found this pass. Deferring because this is an intentional spec deviation on a user-facing Web API (both clone branches now receive the same chunk object, so a mutation on one is observable on the other) — worth a maintainer sign-off on the semantics.

What was reviewed:

  • No dangling references to the deleted $cloneArrayBuffer / $structuredCloneForStream plumbing; the surviving cloneArrayBuffer in JSReadableByteStreamController.cpp is the unrelated file-static helper.
  • Both readableStreamTee callers (prototype .tee() and ReadableStream__tee) match the new parameterless signature.
  • readableByteStreamTee still copies via cloneAsUint8Array, so type: "bytes" tee behavior is unchanged.
Extended reasoning...

Overview

The PR changes Response.clone() / Request.clone() on a stream body to share chunk references between tee branches instead of structured-cloning each chunk into branch2. The one-line semantic change is in ReadableStream__tee (WebStreamsExports.cpp), and the rest is deletion of the now-unreachable cloneForBranch2=true machinery: structuredCloneChunk, m_shouldClone, the $structuredCloneForStream and $cloneArrayBuffer private globals, their builtin-name entries, and the cloneForBranch2 parameter on readableStreamTee / readableStreamDefaultTee. Tests in body-clone.test.ts now assert reference identity between branches and add an RSS-bounded 50-deep clone-chain regression test.

Security risks

None identified. The change strictly removes a per-chunk allocation/copy path; no new user-controlled data reaches native code, and the byte-controller tee (which handles BYOB views and buffer transfers) is untouched. Sharing a Uint8Array between two JS consumers of the same body is what ReadableStream.prototype.tee() already did.

Level of scrutiny

Medium-high. The C++ diff is almost entirely deletions and a signature simplification, which is low-risk mechanically, and I verified every deleted symbol has no remaining references. But this is a deliberate deviation from the fetch spec's "clone a body" step (specced as cloneForBranch2 = true), aligning with Node/Chrome/Firefox per whatwg/streams#1156. That's a user-observable behavior change — code that mutates a chunk from one clone branch will now see the mutation in the other — and REVIEW.md's "Never change a Bun-native default to fix Node compatibility" principle makes Web-API semantic shifts a maintainer call.

Other factors

All three of my earlier inline comments (dead cloneForBranch2=true path, vacuous backing assertion in the fetch test, orphaned $cloneArrayBuffer global) were addressed across 25f5387 / 6c4deb9 / 39c18f8, and the comment-cop flags were resolved. The maintainer is already engaged (requested and received the cross-runtime benchmark table), so deferring rather than approving keeps the sign-off with them. The new RSS test branches its threshold on isASAN/isDebug with ~80 MB of headroom below the pre-fix ~200 MB, which looks reasonable per the leak-test guidance.

robobun added 5 commits July 31, 2026 04:23
… branch

ReadableStream__tee (the extern called from Rust for Request/Response body
clone) passed cloneForBranch2 = true, which StructuredClone-copies every
chunk into the second tee branch. An N-deep clone chain therefore retained
N independent copies of every body chunk: a 10 MB streaming body cloned 100
times held ~1 GB of duplicated buffers.

Node (undici), Chrome, Firefox, and Deno all share the chunk reference
between tee branches when cloning a body. The fetch spec text asks for
cloneForBranch2 = true but no engine ships that; see whatwg/streams#1156.

This flips the extern to cloneForBranch2 = false. The cloned branch now
receives the same Uint8Array the original branch does, so a clone chain
retains one copy of the body regardless of depth. The #33130 tests that
asserted exact-size copies are updated to assert reference identity, which
is the stronger invariant (zero copies rather than right-sized copies).
With both callers of readableStreamTee passing cloneForBranch2 = false, the
default tee's per-chunk StructuredClone branch is unreachable. Remove
structuredCloneChunk(), the m_shouldClone bitfield and if-block, the
$structuredCloneForStream private global + host function, and the
CloneMode::Full branch in StructuredClone.cpp that only it reached. Keep
the cloneForBranch2 parameter on readableStreamDefaultTee to mirror the
spec signature (asserted false).

Also drop the backing-buffer-sum assertion from the fetch() clone test; it
now measured branch1's receive buffers rather than anything the clone path
controls. The per-chunk b.value === a.value check is the load-bearing
invariant.

Correct the Deno claim in comments: Deno passes cloneForBranch2 = true
like the spec.
No caller passes true after the previous commit; remove the parameter from
readableStreamTee / readableStreamDefaultTee and both call sites rather
than keeping it with a comment explaining why it's unused.
The previous commit refactored cloneArrayBufferImpl into the cloneArrayBuffer
host function, but $cloneArrayBuffer has had no JS-builtin caller since the
streams implementation moved to C++. Delete the host function, its header
declaration, builtin name, private-global registration, and type declaration.
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/e019fca7/body-clone-share-tee-chunks branch from d28dea9 to 0edb264 Compare July 31, 2026 11:26

@claude claude Bot left a comment

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.

All three rounds of earlier feedback are addressed (dead cloneForBranch2=true path, m_shouldClone, structuredCloneForStream/cloneArrayBuffer plumbing all removed; fetch test tightened to reference-identity). I didn't find any correctness issues, but leaving final sign-off to a maintainer since this is a user-observable spec deviation on Response/Request.clone() (chunk identity is now shared across tee branches).

What was reviewed:

  • Confirmed both readableStreamTee callers updated and no stale references to the deleted symbols remain (the surviving cloneArrayBuffer in JSReadableByteStreamController.cpp is an unrelated file-static helper).
  • defaultTeeChunkStepsMicrotask now enqueues the same chunk into both controllers; byte-tee's cloneAsUint8Array path is untouched.
  • New RSS test branches its threshold on isASAN/isDebug and sits well below the ~200 MB pre-fix baseline; the comment/threshold mismatch flagged by a finder was checked and is fine.
Extended reasoning...

Overview

The PR flips ReadableStream__tee (the Rust FFI entry used by Body clone) from cloneForBranch2 = true to sharing chunk references, matching Node/undici, Chrome, and Firefox. With both callers now passing false, the entire clone-for-branch2 machinery is deleted: structuredCloneChunk(), JSStreamTeeState::m_shouldClone, the structuredCloneForStream and cloneArrayBuffer host functions, their BunBuiltinNames.h / ZigGlobalObject.cpp / builtins.d.ts plumbing, and the cloneForBranch2 parameter itself. Tests are updated from copy-equality to reference-identity assertions and a 50-deep clone-chain RSS regression is added.

Security risks

None identified. No untrusted-input parsing, auth, or crypto is touched. The change removes an allocation/copy path; the only new observable is that mutating a chunk read from one clone branch is now visible to the other — the same semantics Node and browsers already ship, discussed in whatwg/streams#1156.

Level of scrutiny

Moderate-to-high. The implementation itself is a small, well-understood simplification (delete a branch, enqueue the same JSValue twice), and the dead-code removal is mechanical — I grepped for every deleted symbol and confirmed no remaining references outside the unrelated file-static cloneArrayBuffer in JSReadableByteStreamController.cpp. What warrants a maintainer's eye is the decision: this is a deliberate deviation from the fetch spec's "clone a body" step on a public Web API, and while it aligns with every other major runtime, it changes user-observable behavior (clonedChunk === originalChunk where it was previously a fresh copy).

Other factors

Jarred already engaged on the thread and directed the follow-up work (benchmark table, review-comment fixes), so the direction has implicit maintainer awareness. All three of my earlier inline comments are resolved in 25f5387 / 6c4deb9 / 39c18f8. The bug-hunting system found nothing this run; one finder flagged the RSS test's "well under 50 MB" comment vs. the 80/120 threshold, which verifiers correctly ruled out — the threshold is set well below the ~200 MB unfixed baseline per REVIEW.md's leak-test guidance. CI is green on the diff; the failing lanes are pre-existing build timeouts and known flakes unrelated to this change.

@Jarred-Sumner
Jarred-Sumner merged commit 5b7c3ca into main Jul 31, 2026
49 of 50 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/e019fca7/body-clone-share-tee-chunks branch July 31, 2026 12:02
Jarred-Sumner pushed a commit that referenced this pull request Aug 22, 2026
### Problem
- GitHub closes only the first reference after a keyword, so "Fixes #1,
#2" leaves #2 open. "Supersedes #3" links nothing, and no reference
closes a pull request.
- The last 1000 merged PRs name 274 such references. PR #32292 is open
although merged #36135 says "Supersedes #32292".

### Fix
- `.github/workflows/close-linked-issues.yml` runs on
`pull_request_target` `closed` (a merge into the default branch of
`oven-sh/bun`) and on `workflow_dispatch` with a PR number and
`dry_run`. Everything is inline in one `actions/github-script` step,
with no checkout.
- Each open target is closed as `completed` with the comment "Closed as
completed by #N." or "Superseded by #N.". Closed or missing targets, the
PR itself and other repositories are skipped.
- The parser has no regex. A closing keyword (close, fix, resolve,
supersede, replace, any tense) must lead the reference, alone or in a
list. A negated, hedged or noun keyword, or one whose subject is another
reference, does not count ("may fix", "the rm fix #1", "#100 supersedes
#1").
- Verified: `test/internal/close-linked-issues.test.ts` (333 cases) runs
the YAML's script against fake `github`, `context` and `core`. Also the
1000-PR parse (Notes).

### Background
- GitHub's own keywords are close, fix and resolve (-s, -ed). Each links
one reference, and only a merge into the default branch closes it.
- `pull_request_target` runs in the base repository with a write token,
also for fork PRs. That is safe only when no PR-controlled code runs.
Here the description is the only PR input, parsed as text.

<details><summary>Notes</summary>

A close through the API does not create the "closed this in #N" timeline
link that GitHub makes for its own closes. The comment carries the PR
number instead.

How the parser was calibrated. I pulled the descriptions of the last
1000 merged PRs and listed every line with a keyword next to a
reference. The keyword families, list shapes and reference forms in the
script are the ones that appear there. A reference is `#1`,
`owner/repo#1`, an issue or pull URL (bare or in `<>`), or a markdown
link. Four lines would have been wrong with a plain
keyword-then-reference rule, and each led to a rule:

- "the open `rm` fix #37521" (#38379): "fix" as a noun. Base forms (fix,
close, resolve, supersede, replace) count only at the start of a
sentence or line, or after will, should, does, and, and a few similar
words. "to" is not one of them ("unable to fix #1", "how to fix #1").
- "May also fix #12318 / #10046, untested" (#38242): hedged. may, might,
could, would, partially and the negations disqualify the keyword,
looking past adverbs such as "also".
- "Supersedes the closed #26040" (#36289) and "a comment on closed
#35351" (#35365): "closed" as an adjective. A determiner or preposition
before the keyword disqualifies it.
- "supersedes #33130's optimisation" (#35843): a number that continues
into a word is not a reference.

Review added: a reference before the keyword is the subject ("#100
supersedes #1"), also through "which" or "that" ("reverts #100, which
fixed #1") and across a removed span ("#100 ~~also~~ fixes #1"). A hedge
two words before the keyword disqualifies it ("hopefully this fixes #1",
"could this fix #1?"). A clause that starts with if, when, once, until
or unless is not a statement. The tokenizer keeps a line break as a
token so that "Fixes #1" on one line and "Fixes #2" on the next stay two
statements. Code spans, fences, indented code, blockquotes, HTML
comments and strikethrough are skipped. The block stripping follows
CommonMark for fences (also inside a blockquote), indented code,
blockquotes with lazy continuation, setext underlines and HTML comments,
and GFM for `~~` flanking.

Result over the 1000 descriptions: 274 distinct references in 135 PRs. I
checked the current state of all of them through GraphQL. All but one
are closed (202 issues completed, 5 duplicates, 66 pull requests). The
one open target is PR #32292, superseded by merged #36135. No open
target is a false positive. Every review change kept this result.

Patterns that are deliberately not handled: a bulleted list under
"Closes:" on its own line (not seen in the sample), references separated
by whitespace only ("#1 #2"), "fix for #1", and GH-1 style references. A
`?` after the list is not treated as a question. The block parser tracks
no list containers, so a second paragraph of a list item indented by
four spaces is read as an indented code block and skipped. A removed
span or inline comment reads as one word, so "Fixes <!-- n --> #1" finds
nothing.

The test suite covers: the phrases above, stopping at the right place in
real sentences, CRLF descriptions, URLs with fragments or a `/files`
suffix, case-insensitive `Owner/Repo#1`, the fake API where a lookup, an
update or a comment fails, the `dry_run` input, an invalid `pr_number`
input, an unmerged PR, a PR merged into a non-default branch, the merge
event body against a later edit, and a description with no closing
statement.

The first revision of this PR checked out the repository and ran
`scripts/close-linked-issues.ts`. Jarred asked for no checkout and no
script file, so the script moved inline into the workflow and the test
now reads it out of the YAML.
</details>

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 9 · 2 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/internal/close-linked-issues.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/close-linked-issues.test.ts
bun test v1.4.1 (4448a2e)

test/internal/close-linked-issues.test.ts:
(pass) finds "Fixes #39852" [176.21ms]
(pass) finds "Closes #31772. Fixes #31771." [22.28ms]
(pass) finds "- Fixes #39930" [12.28ms]
(pass) finds "Fixes: #30429" [10.46ms]
(pass) finds "FIXES #1" [7.86ms]
(pass) finds "(Fixes #1)" [8.97ms]
(pass) finds "**Fixes #1**" [10.20ms]
(pass) finds "__Fixes #1__" [9.83ms]
(pass) finds "_Fixes #1_" [11.25ms]
(pass) finds "Fixes **#1**" [9.72ms]
(pass) finds "**Fixes** #1" [7.13ms]
(pass) finds "**Fixes:** #1" [8.11ms]
(pass) finds "Fixes #1 and **#2**" [11.47ms]
(pass) finds "Fixes **#1**, **#2**" [9.13ms]
(pass) finds "## Why (fixes #13771, closes #30543)" [16.08ms]
(pass) finds "Closes #11418" [19.46ms]
(pass) finds "Resolves #1. Resolved #2. Resolve #3." [12.09ms]
(pass) finds "Fixes #34055, #30327, #24394, #20816, #32403, #11898, #10056." [17.11ms]
(pass) finds "Fixes #18192 and #31675 as a consequence" [10.45ms]
(pass) finds "Fixes #1, #2, and #3" [10.96ms]
(pass) finds "Fixes #1 & #2" [7.63ms]
(pass) finds "Closes #33280,  Closes #32864 and Closes #29696 (the timer in #32949 is orthogonal)" [20.29ms]
(pass) finds "Closes #33182 and #32947 on top of current main (which already has #36304 for catalogs)." [16.12ms]
(pass) finds "Fixes #1,\n#2" [7.76ms]
(pass) finds "Fixes #1, #2,\nand #3" [9.27ms]
(pass) finds "Fixes #1\nand #2" [8.57ms]
(pass) finds "Fixes #1\n& #2" [6.80ms]
(pass) finds "Fixes #1 and\n#2" [7.31ms]
(pass) finds "Supersedes #39908 (same change, moved from a fork branch)" [13.21ms]
(pass) finds "Supersedes #38778 and #38391. Carries the entry point arm of #35053." [14.43ms]
(pass) finds "Supersedes #39193 and keeps its three tests." [11.48ms]
(pass) finds "This supersedes #33306 and #32803. Their tests are kept here." [13.73ms]
(pass) finds "- This replaces #33793. Its 
... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
.github/workflows/close-linked-issues.yml | 950 ++++++++++++++++++++++++++++++
 test/internal/close-linked-issues.test.ts | 598 +++++++++++++++++++
 2 files changed, 1548 insertions(+)
```

</details>

**gate history** · 29 passed · 0 rejected · iteration 9

<details><summary>evidence per changed file</summary>

```
file                                       reads  edits  tests
.github/workflows/close-linked-issues.yml      6     12      0
test/internal/close-linked-issues.test.ts      3     11      0
```

</details>

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants