Skip to content

FileSink: make ref()/unref() on a pipe sink actually control keep-alive - #33533

Open
robobun wants to merge 4 commits into
mainfrom
farm/1bd8d7d4/filesink-ref-keepalive
Open

robobun wants to merge 4 commits into
mainfrom
farm/1bd8d7d4/filesink-ref-keepalive

Conversation

@robobun

@robobun robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

FileSink.ref() / unref() on a FIFO or pipe sink were effectively no-ops, contradicting the documented keep-alive contract.

Repro

import { spawn, execSync } from "node:child_process";
import * as fs from "node:fs";
const FIFO = `/tmp/fsink-${process.pid}.fifo`;
execSync(`mkfifo ${FIFO}`);
// reader opens but does not drain for 1s, so the write stays pending
spawn("sh", ["-c", `exec 3<${FIFO}; sleep 1; cat <&3 >/dev/null`], { detached: true, stdio: "ignore" }).unref();
for (;;) { try { fs.closeSync(fs.openSync(FIFO, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK)); break; } catch { execSync("sleep 0.01"); } }

const sink = Bun.file(FIFO).writer({ highWaterMark: 1024 * 1024 });
sink.unref();                                  // ask to NOT keep the loop alive
sink.write(Buffer.alloc(4 * 1024 * 1024, 97)); // pending write re-arms keep-alive anyway
// EXPECTED: process exits immediately. ACTUAL: stays alive ~1000ms.

Cause

A pipe FileSink only held the event loop open while a write to it was in flight, and the JS-facing ref()/unref() could not override that:

  • JSFileSink::m_refCount starts at 1 (generate-jssink.ts), so ref() from the default state never reaches native updateRef() (only a 0→1 transition calls it).
  • unref() did reach native and dropped the poll's keep-alive, but the next on_write / on_auto_flush called update_ref(has_pending_data) and silently re-armed it.

So unref() was undone by the very next write, and ref() on the default sink did nothing.

Fix

Record the user's ref/unref choice in a new keep_alive_allowed cell (mirroring m_refCount > 0) and route every automatic keep-alive change through one helper, set_keep_alive, that ANDs the automatic decision with that flag:

  • unref() now sticks across later writes.
  • ref() restores the automatic management (in-flight-write keep-alive) without pinning an idle sink.

The docs and JSDoc claimed the process "stays alive until .end()", which was never true: Bun.stdout.writer() on a pipe is pollable too, so bun x.js | cat would hang forever. Corrected docs/runtime/file-io.mdx and the bun-types JSDoc to describe the actual in-flight-write behavior.

Verification

New ref/unref keep-alive tests in test/js/bun/util/filesink.test.ts:

  • unref() before a pending write is not re-armed by it
  • unref() after a write already went pending lets the process exit
  • ref() puts the automatic keep-alive back (the pending write drains once the reader consumes it)
  • ref()/unref() on a regular file are no-ops and don't break writes
USE_SYSTEM_BUN=1 bun test   # first test times out (SIGTERM) — bug present
bun bd test                 # 48 pass, 0 fail

no test proof · iteration 6 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/filesink.test.ts

@robobun
robobun requested a review from alii as a code owner July 6, 2026 19:34
@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:51 AM PT - Jul 10th, 2026

✅ @autofix-ci[bot], your commit f92d9f602245f6deab28fdde5b1d21dbad6e2d6f passed in Build #71418! 🎉


🧪   To try this PR locally:

bunx bun-pr 33533

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

bun-33533 --bun

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1af29f3a-751e-4280-87f8-690d9fa6a6c3

📥 Commits

Reviewing files that changed from the base of the PR and between fc865b3 and f92d9f6.

📒 Files selected for processing (6)
  • docs/runtime/file-io.mdx
  • packages/bun-types/s3.d.ts
  • src/io/keep_alive.rs
  • src/io/lib.rs
  • src/runtime/webcore/FileSink.rs
  • test/js/bun/util/filesink.test.ts

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

@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: diff is green, red checks are unrelated

test/js/bun/util/filesink.test.ts (the 4 new ref/unref keep-alive tests) passes on every lane that ran it. The red on build 71418 (sha f92d9f60, after the UserKeepAlive refactor + autofix format):

  • TypeScript types (GitHub Actions): ENOENT: .../typescript/lib/lib.es2020.d.ts in the fixture setup. The tsgo variant of the same test passes, and the bun-types workflow on main shows the same failure. This diff only touches JSDoc comment text in packages/bun-types/s3.d.ts, not any type signatures.
  • buildkite/bun/darwin-14-aarch64-test-bun: Expired (the job was never picked up by an agent).
  • test/js/node/http/node-http.test.ts ECONNREFUSED on Windows x64-baseline was flagged flaky and retried.

Jarred's review feedback is addressed in 6c1499d99f (the keep-alive gate is now bun_io::UserKeepAlive). The change is ready; it needs a maintainer merge or an infra retry on the two checks above.

robobun added 2 commits July 9, 2026 18:10
…live

A FileSink.writer() on a FIFO or pipe only held the event loop open while a
write to it was in flight. The JS-facing ref()/unref() could not change that:

- JSFileSink::m_refCount starts at 1, so ref() from the default state never
  reached native updateRef() (only a 0->1 transition calls it).
- unref() did reach native and dropped the poll's keep-alive, but the next
  on_write / on_auto_flush called update_ref(has_pending_data) and silently
  re-armed it.

Record the user's ref/unref choice in keep_alive_allowed (mirroring
m_refCount > 0) and route every automatic keep-alive change through one helper
that ANDs the automatic decision with that flag. unref() now sticks across
later writes; ref() restores the automatic management without pinning an idle
sink.

The docs and JSDoc claimed the process stays alive until .end(), which was
never true (Bun.stdout.writer() on a pipe is pollable too, so 'bun x.js | cat'
would hang). Corrected both to describe the actual in-flight-write behavior.
The previous ref() test drained the FIFO cross-process and relied on the
child exiting to signal EOF, which hung on darwin aarch64 CI. Rewrite it to
assert, via a bounded race against proc.exited, that ref() after unref()
keeps the child alive for a pending write, then kill it. No cross-process
drain, no dependence on child-exit timing.
@robobun
robobun force-pushed the farm/1bd8d7d4/filesink-ref-keepalive branch from db94ad0 to bb72224 Compare July 9, 2026 18:14

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why ins't this using poll_ref, which encapsulates this behavior more cleanly than this one-off approach?

@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

The short answer: bun_io::KeepAlive (poll_ref) tracks one Active/Inactive/Done state, and a pipe FileSink turns out to need two independent bits.

The two inputs:

  1. is there something to keep alive — a write is in flight. on_write / on_auto_flush already recompute this every tick from has_pending_data.
  2. has the user opted out — JSFileSink::m_refCount > 0.

The bug was that (2) was never consulted: on_write called writer.update_ref(has_pending_data) unconditionally, so a JS unref() was undone by the very next write. The fix has to store (2) somewhere that survives the next on_write.

If I replace the poll-flag route with poll_ref: KeepAlive:

  • the auto-management path becomes poll_ref.ref_() / unref() driven by has_pending_data, and the next on_write still re-arms after a JS unref() — same bug.
  • KeepAlive::disable() would make the unref() stick, but Done is terminal: a later JS ref() could not restore it, and the docs/jsdoc say ref() brings back the automatic management.
  • running poll_ref alongside the poll's own keep-alive (the PollerPosix::Fd vs WaiterThread split in src/spawn/process.rs) would keep the process alive if either is active, so JS unref() couldn't let the process exit while a write is pending.

So any shape ends up storing the "ref allowed" bit explicitly. I put it on FileSink as keep_alive_allowed and funnelled the existing seven writer.update_ref(..) sites through one set_keep_alive(wants) that ANDs the two — the underlying mechanism stays the writer's FilePoll flags, which is what a pipe sink already uses and what Windows' BaseWindowsPipeWriter::update_ref expects.

The other option is to drop the auto-management entirely and make the default "keep alive until .end()", at which point poll_ref: KeepAlive (with update_ref(value) just calling ref_()/unref()) would be exactly right. I didn't do that because Bun.stdout.writer() and Bun.file(fd).writer() on a pipe are the same code path, so

const w = Bun.stdout.writer();
w.write("hi");

piped to cat would never exit without an explicit w.end(). The current "only while a write is in flight" is what keeps that working.

Happy to flip to either of those shapes if you'd prefer — a poll_ref: KeepAlive replacing the poll-flag route (still with the bool gate), or the always-on default (no gate, behavioural change for Bun.stdout.writer()). Let me know which you want.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun add a UserKeepAlive? that has both has the user opted out and are we currently able to keep the event loop alive?

Instead of a one-off Cell<bool> on FileSink, introduce UserKeepAlive in
bun_io::keep_alive next to KeepAlive. It holds the two bits that FileSink
needs: allowed (what JS ref()/unref() asked for) and wanted (what the
automatic in-flight-write management asks for right now), and is_active() is
the AND of both. FileSink stores it as poll_ref and applies is_active() to the
writer's existing poll/uv-handle, so nothing about platform plumbing changes.

Addresses review feedback.
@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Done in 6c1499d. bun_io::UserKeepAlive lives next to KeepAlive in src/io/keep_alive.rs:

pub struct UserKeepAlive { allowed: bool, wanted: bool }
impl UserKeepAlive {
    pub fn set_allowed(&mut self, value: bool) -> bool;  // JS ref()/unref()
    pub fn set_wanted(&mut self, value: bool) -> bool;   // auto management
    pub fn is_active(&self) -> bool;                     // allowed && wanted
}

FileSink holds it as poll_ref: JsCell<UserKeepAlive> and applies is_active() to the writer's existing poll/uv-handle (w.update_ref(ctx, active)), so the platform plumbing is unchanged.

I started with the version that embeds a KeepAlive inner (so set_* bumps the loop counter directly), but that double-counts on Windows: the writer's uv pipe handle has its own ref state that libuv feeds into active_handles, and Blob::get_writer's Windows path calls w.start() directly without going through FileSink::setup, so there's no single place to uv_unref it up front. Routing the gate's output through the handle the sink already owns avoids that and keeps Windows identical to today. Happy to flip to the embedded-KeepAlive shape if you'd rather I also plumb the Windows Blob::get_writer path.

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

This still reproduces on main at b52d3e5 (bun 1.4.3) through the on_write path. The repro below uses an external reader that takes part of the buffered tail and then stalls (Linux, needs sh, head, mkfifo):

import fs from "node:fs"; import { execSync, spawn } from "node:child_process"; import os from "node:os"; import path from "node:path";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pf-")); const fifo = path.join(dir, "ff"); execSync(`mkfifo ${fifo}`);
// reader: opens now, takes 8192 bytes after 300 ms, then stalls 4 s with the fd open
spawn("sh", ["-c", `exec 3<${fifo}; sleep 0.3; head -c 8192 <&3 >/dev/null; echo 'reader took 8192' >&2; sleep 4`], { stdio: ["ignore", "ignore", "inherit"] }).unref();
let wfd; for (;;) { try { wfd = fs.openSync(fifo, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK); break; } catch (e) { if (e.code !== "ENXIO") throw e; } }
const w = Bun.file(wfd).writer();
const r = w.write(Buffer.alloc(256 * 1024, "u")); // pipe full, tail buffered, writable poll armed
w.unref();
r.catch(() => {});
const t0 = Date.now();
setTimeout(() => console.error(`timer done at +${Date.now() - t0}ms`), 1000);
process.on("exit", () => console.error(`exit at +${Date.now() - t0}ms`));

Expected: exit at about +1000 ms, when the timer fires. Actual: exit at +4303ms. The partial read at +300 ms fires the writable poll, and on_write calls writer.update_ref(evtloop, has_pending_data) (src/runtime/webcore/FileSink.rs:392). That re-arms the keep-alive that unref() dropped, so the process stays until the reader closes. If the reader never reads, the same script exits at +1000 ms, so only the re-ref in on_write is wrong. The set_keep_alive(has_pending_data) change in this PR covers that call.

#41835 depends on a sticky unref(). Its net.Socket({ fd }) _destroy calls sink.end() and then sink.unref(), so a destroyed socket does not hold the process for its unflushed tail. With this bug, a peer that reads a little after destroy() and then stalls pins the process again.

The branch conflicts with main in src/runtime/webcore/FileSink.rs, test/js/bun/util/filesink.test.ts, and docs/runtime/file-io.mdx (main has #38641, #39611, and #41709 since). It needs a rebase.

Jarred-Sumner pushed a commit that referenced this pull request Sep 30, 2026
…allback (#44250)

### Problem
- With nothing else alive, a JS stream piped into a `FileSink` on a pipe
stops at the first full drain. Un-awaited `Bun.write(Bun.stdout, new
Response(stream))` delivers 4,194,304 of 8,388,608 bytes, exits with
code 0, and never settles.
- `FileSink::on_write` (`src/runtime/webcore/FileSink.rs:366`) ran its
microtask checkpoint before it resumed the pump. The run loop does not
count the microtask the pump queues. `on_close` had no checkpoint.

### Fix
- `completion_scope()` opens an event-loop scope while a stream is piped
in. Its exit is the checkpoint. `on_write` and `on_close` open it before
they enter JS.
- Correct because a callback from the loop owes a checkpoint for the JS
it enters.
- Verified: `test/js/bun/util/filesink.test.ts`, seven new tests, 0 of 7
on main. Each scope has a test that fails without it. Also 15 more Linux
suites.
- Self-reviewed: 6 concerns raised, 5 addressed as proposed, 1 otherwise
(Notes).

### Background
- The pump (`readStreamIntoSink`) feeds the sink from a JS stream and
waits for `ready()` after a short write. A microtask checkpoint runs
queued promise reactions.
- Considered a drain in the run loop after each poll callback: every
loop turn pays. A scope around every `FileSink` poll callback charges
plain writers too.

### Downsides
- A sink written from script pays 7 more instructions per `on_write` and
per `on_close`. `.text` grows by 256 bytes.
- An un-awaited `Bun.write` whose stream fails now exits with code 1.
Main exits 0 in 9 of 10 runs.
- After `proc.unref()`, a stream stdin that can always produce now
arrives in full. Main cuts it.

<details><summary>Notes</summary>

All numbers are from release builds of main ad60a9b and of this
branch at 028985b on Linux x64, unless a line says debug.

**Repro** (no `beforeExit` listener, no timer, no top-level await):

```js
// bun repro.mjs | (sleep 0.4; wc -c)      expected 8388608, main prints 4194304
const first = new Uint8Array(4 * 1024 * 1024).fill(97);
const chunk = new Uint8Array(64 * 1024).fill(98);
let pulls = 0;
Bun.write(Bun.stdout, new Response(new ReadableStream({
  pull(c) {
    if (++pulls === 1) return c.enqueue(first);
    if (pulls <= 65) return c.enqueue(chunk);
    c.close();
  },
})));
```

**Why main loses the step.** The run loop is `while
vm.is_event_loop_alive() { vm.tick(); vm.auto_tick_active(); }`
(`src/runtime/cli/run_command.rs`). `auto_tick_active` runs the poll
callback. `on_write` drops the loop ref of the poll because the buffer
is empty, and `src.ready()` makes the pump read the next chunk. The
chunk steps, close steps and error steps of that read are queued with
`queueReactionJob`
(`src/jsc/bindings/webcore/streams/JSReadRequest.cpp`). The callback
returns, nothing is alive, and the loop ends before the next `tick()`.
One ref'd timer gives the loop another turn and hides the bug.

**The fix itself**, 20 runs per shape. A run counts when every byte
arrived and the reaction of the promise ran.

| Shape | main | this branch |
| --- | --- | --- |
| chunks arrive after the pump parked, to a piped stdout | 0 of 20 | 20
of 20 |
| the same into a FIFO | 0 of 20 | 20 of 20 |
| stream closes while the pump is parked | 2 of 20 | 20 of 20 |
| stream fails while the pump is parked | 1 of 20 | 20 of 20 |
| direct stream, closed with bytes still buffered | 0 of 20 | 20 of 20 |
| small chunk, sink ended against a full socket | 0 of 20 | 20 of 20 |
| direct stream that awaits each `flush()` (control) | 20 of 20 | 20 of
20 |

**Each scope is needed** (debug+ASAN builds, the seven new tests):

| Build | Failing tests |
| --- | --- |
| main | 7 |
| this branch | 0 |
| this branch without the scope in `on_write` | 5: chunks to stdout,
chunks to FIFO, `beforeExit` count, close, error |
| this branch without the scope in `on_close` | 1: sink closed with no
source parked |

**Microtask checkpoints and `enter()` calls per callback**, from debug
logs (`BUN_DEBUG_ALL=1`), constant over 3 runs. The column for main is
from a debug build of 1313ca6, where `FileSink.rs` is the same file
as on ad60a9b.

| Callback | main | this branch |
| --- | --- | --- |
| drain that resumes the pump | 1 checkpoint, 1 `enter()` | 1
checkpoint, 2 `enter()` |
| drain that leaves bytes in the buffer | 0, 0 | 0, 0 |
| close, a source had parked | 1, 1 | 1, 3 |
| close, no source parked | 1, 1 | 2, 2 |
| sink written from script, drain or close | 1, 1 | 1, 1 |

On main the one checkpoint of a drain runs before the pump is resumed.
On this branch it runs after.

**`beforeExit` emissions** for the 8 MiB pump with a listener installed,
10 runs: main 1 to 21, this branch 1 in 10 of 10, Node v26.3.0 1 in 10
of 10.

**Cost for a sink written from script.** One `on_write(65536, Drained)`
call: 46 instructions on main, 53 on this branch, no `call` instruction
in either. One `on_close` call: 28 and 35, 2 `call` instructions in
both. Counted with gdb `nexti` on release builds with symbols, identical
over 8 calls. Binary size with `size`: `.text` 80,667,862 to 80,668,118,
`data` and `bss` equal, the file keeps its size of 80,836,168 bytes.
`on_write` grows from 1,302 to 1,530 bytes and `on_close` from 1,860 to
1,983.

**Not measured:** `write(2)` calls that return `EAGAIN` per drain.
`strace`, `perf` and `valgrind` are not installed where I measured, and
under gdb `catch syscall` the writer is so slow that the pipe never
fills.

**A stream that fails with no handler**, 10 runs. Main: exit code 0 and
nothing on stderr in 9 runs, `error: boom` and exit code 1 in 1 run.
This branch: `error: boom` and exit code 1 in 10 runs. The same stream
awaited at top level gives `error: boom` and exit code 1 on both builds.

**`proc.unref()` and a stream stdin**, 10 runs per cell. The child
starts to read only after the pump has taken the first chunk.

| Stream | main | this branch |
| --- | --- | --- |
| can always produce | 4,194,304 of 8,388,608 | 8,388,608 |
| all data queued and closed before the spawn | 219,264 in 9 of 10 runs
| 219,264 in 9 of 10 runs |
| produces one chunk, then waits on a promise that never settles |
4,194,304, parent exits | 4,194,304, parent exits |
| can always produce, no `unref()` | 8,388,608 | 8,388,608 |

This change does not touch `Writable::unref`, which clears the loop ref
of the stdin writer also while it holds accepted bytes (second row).
What `unref()` must mean for a writer that owes bytes is a decision for
a maintainer and relates to #33533. No test here uses `unref()`.

**Sites that this change leaves alone, and why.**

| Site | Reason |
| --- | --- |
| `on_ready` | The POSIX writer never calls it. It is the `on_writable`
slot on Windows, where the unfixed build has no failing case. |
| `EndOfFile` arm of `on_write` | No test reaches it with a piped stream
on POSIX. |
| Windows settle for a borrowed fd, in `end_writer` and in the abort
handler | No failing case on Windows. #42819 rewrites that code. |
| `on_auto_flush` | It runs inside the deferred task queue, where
`EventLoop::exit()` does not drain. The task that `run_pending_later`
queues just before the resume keeps the loop alive for the next
`tick()`. |
| Other sinks and other poll owners | Not examined here. |

**Other PRs that touch the same lines.**

- #43761 added a ref guard at the top of `on_close` and merged while
this PR was open. This branch contains it: guard first, scope second.
Its stdin test passes and the tests here pass.
- #42032 adds a checkpoint after the `beforeExit` dispatch. On unfixed
code with that hunk, six of the seven tests pass, because the loop
passes through that dispatch after each drain. The seventh fails:
`beforeExit` fires more than once for one stream (8 times in my run).
With this change and that hunk together all seven pass.

**Self-review.** Concerns raised and what I did:

1. A ref guard in `on_close` repeated #43761 without its tests. The
proposal was to stack this PR on #43761. I removed the guard and did not
stack: the scope does not read the sink when it ends, so it does not
need the guard. #43761 has merged since.
2. A `cfg(windows)` scope in `end_writer` had no failing test and sits
in code that #42819 rewrites. Removed.
3. A scope and a guard in `on_ready`, and an `EndOfFile` clause in
`on_write`, had no failing test. Removed.
4. A gate on `VM::is_entered()` had no failing test. I built the change
without the gate and ran a script that closes the sink inside the
`Bun.write` call with a microtask already queued. The order of the
microtask did not change, because a script frame already runs inside an
entered scope. Removed.
5. The predicate also matched a `stdin: "pipe"` sink that script never
read, and shell pipes. It now tests only for a piped stream.
6. The tests would pass on unfixed code once #42032 lands. Added the
test that counts `beforeExit`.

**Platforms.** On Windows Server 2019 x64 a debug build of main passes
every portable shape, also with a first chunk of 64 MiB and of 256 MiB
where the child waits 0.6 to 2.3 s for the reader. So on Windows the
five portable tests guard against a regression only. The first revision
of the FIFO test read its end with `Bun.file(fd).bytes()`. On macOS that
read never finished, 8 of 8 attempts, although the child had exited. The
test now reads with `read(2)` in a poll with a deadline, and passes on
macOS.

**Suites** on the debug build of this branch, 60 s per test, all pass:
`filesink` (77 after the merge with main), `bun-write` (86),
`spawn-stdin-readable-stream` (4 files, 55), `spawn-streaming-stdin`,
`spawn-stdin-destroy`, `spawn-stdin-pipe-fd-leak`,
`readablestream-helpers`, `streams` (624), `compression`,
`sync-pull-fast-path`, `native-source-onclose-leak`,
`direct-readable-stream`, `bunshell` (436).

</details>

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

---

**no test proof** · iteration 4 · platform-specific test(s) that do not
run on this machine, deferring to CI, which covers all platforms:
test/js/bun/util/filesink.test.ts

<!-- robobun:evidence:end -->

This branch has not been deployed

No deployments
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