Skip to content

Worker / worker_threads: WebCore-shaped lifetimes, joined threads, one ordered VM teardown - #37075

Merged
dylan-conway merged 151 commits into
mainfrom
claude/worker-stability
Aug 8, 2026
Merged

Worker / worker_threads: WebCore-shaped lifetimes, joined threads, one ordered VM teardown#37075
dylan-conway merged 151 commits into
mainfrom
claude/worker-stability

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Aug 6, 2026

Copy link
Copy Markdown
Member

Makes Worker / node:worker_threads stable rather than experimental: every crash, use-after-free, assertion, leak and hang class around a VM's lifetime and teardown, on all platforms. Missing worker_threads API surface (resourceLimits, trackUnmanagedFds, moveMessagePortToContext, …) is out of scope.

Lifetime model

  • WebCore's ActiveDOMObject / ScriptExecutionContext registry is restored, so Worker, MessagePort, BroadcastChannel and WebSocket are stopped in a real stop phase before the JSC VM is destroyed instead of from inside ~VM. Worker is split, as upstream, into the script object and a WorkerMessagingProxy that owns the parent↔thread relationship.
  • Worker threads are refcounted and joined by their parent (Node's model). A parent tracks its children, stops them in its own stop phase and joins them before its VM goes away, so terminate() propagates through nested workers and resolves only once the thread is gone. No pthread_exit.
  • VirtualMachine::teardown() is the one ordered sequence for a finished worker and for main-thread exit: exit handlers run, then script is forbidden and everything the VM owns is stopped natively (WebCore objects, servers, listeners, watchers, sockets, dns, sqlite, in-flight fetch/S3 requests, Bun.build passes waiting on this VM's plugins — as in Node, no 'close'/'error' handler runs after 'exit') → timers cancelled, children joined, in-flight off-thread work waited for or released, VM handle closed, queued work released → JSC VM destroyed → loops freed (uSockets; libuv on Windows) → destroy.
  • Every off-thread completion — thread pool (fs, crypto, zlib, transpiler, dns.lookup, shell builtins, Bun.Archive, password hashing), the HTTP thread (fetch, S3), the bundle thread, child-process waiter, fs watcher threads, napi async work / threadsafe functions, JSC helper threads — reaches a VM only through a per-VM VmHandle that teardown closes. Pool work is one typed carrier (bun_jsc::Job) whose JS-affine half only the owning thread can touch and teardown releases; work whose storage lives in JS objects or on another thread is counted and waited for; work that can block on an external party is registered so the stop phase aborts it. A late completion is refused and released by its producer instead of touching a dead VM. EventLoop has no cross-thread entry points any more.
  • A worker's "may run script" gate closes the moment its stop is requested — a parent's terminate(), its own process.exit(), or an uncaught error — not when its thread gets around to tearing down (Node's can_call_into_js / is_stopping). Every native→JS entry consults it (timer and immediate callbacks, event listeners, socket/server callbacks, pool-job completions, JSC deferred work, N-API), so nothing dispatches into a worker that is being stopped, whichever event source it came from. Promise settlement is the one native→promise boundary and never accepts an empty value: a JS conversion that a termination interrupted becomes "reject with the pending exception", which itself yields to the termination.
  • The event loop stays fair under producers that outpace it: one turn refills from the concurrent queue a bounded number of times; message drains take a fixed budget per task (a bounded batch per lock acquisition, never a whole-queue hand-back) and continue after the loop has polled; a UDP socket is read a bounded number of batches per readiness event. A worker posting faster than its parent deserializes, or a datagram socket that never runs dry, no longer holds that loop's timers, I/O — or its own pending stop.
  • Cross-thread costs of the handle are kept off hot paths: its read-mostly state sits on its own cache line away from the counters other threads update, and C++ tests the "may run script" byte inline rather than calling out per callback.

Behaviour changes (Node parity)

  • parentPort is a real MessagePort: parentPort.close() ends the worker, .ref()/.unref() work, receiveMessageOnPort returns falsy messages, and parent messages are delivered only after the worker's entry module has run (a preload's un-awaited import() does not count as the entry running).
  • A worker with a pending top-level await starts and receives messages; it exits 13 if the await never settles, and a top-level await rejecting later fails the worker at that moment.
  • await worker.terminate() resolves the exit code (1 for a running worker); threadId stays valid until exit; everything a worker posted before it exited is delivered before 'exit'/'close'; postMessage() to a terminated worker is a no-op rather than an error; a rejection that is only a consequence of terminate() (a lookup or request cancelled by the stop) is not reported as the worker's 'error'.
  • process.exit() / worker exit no longer runs microtasks or nextTicks queued before it. A worker's own process.exit() or uncaught error runs its 'exit' handlers; a parent terminate() does not. process.exit() from inside (nested) node:vm contexts in a worker unwinds like any exception, and a node:vm timeout inside a worker no longer leaves the worker unable to run script afterwards.
  • Workers inside a process that has an IPC channel do not get a process.send() of their own over the process's channel fd.
  • N-API's pure constructors/accessors are callable while an exception is pending (as in Node), so node-addon-api can build the Error for a call a termination interrupted instead of aborting the process.
  • Assigning a non-function to port.onmessage releases the keep-alive a handler took.
  • Servers, listeners, sockets, UDP sockets, watchers and dns.Resolvers are closed by the exiting VM rather than left to GC finalizers; sqlite connections a VM opened are checkpointed and closed by that VM's exit; a Bun.build whose VM goes away mid-build is cancelled (its plugin requests failed, the pass finished) rather than abandoned or waited on, and one still queued behind other builds is released without waiting for them.
  • A connect-path DNS lookup (Bun.connect, net, WebSocket to a hostname) is process-wide and outlives the thread that happened to issue it: on macOS a worker exiting mid-lookup no longer answers every other thread's coalesced waiters with an error (and caches it for the TTL).
  • An addon's external-buffer finalizers run when the Worker that loaded it exits (napi_create_external_{arraybuffer,buffer}), as Node's environment teardown finalizes every remaining reference.
  • Releasing the last keep-alive from an immediate or a late promise reaction (e.g. port.close() inside setImmediate) is noticed before the loop parks.
  • Windows: a worker thread closes its loops. Open pipe/tty/process handles and readers mid file-read are closed through their owners in the stop phase, and requests still in flight are drained there — against a live VM that still accepts (and then awaits) the follow-on work a completion may start — before anything is released; sockets over named pipes and TLS-over-duplex sockets join the stop phase; a reader dropped mid-read keeps the buffer its pending read lands in.

Two of the crashes were in JavaScriptCore rather than Bun: a TerminationException raised while the module loader resolves an import continued into finishLoadingImportedModule (fixed in oven-sh/WebKit#391, picked up by the WebKit version bump here). A worker parked in Atomics.wait with no timeout still cannot be terminated (#32802); that needs a JSC change and is tracked separately.

Testing

New tests accompany each behaviour fix (worker_threads, Web Worker lifecycle edges — terminate() at every phase of dns/fs/build/vm/napi/http work, message ordering and flooding, process exit ordering, sqlite). Seven more upstream test-worker-* files are vendored (one of them, the message-port infinite-message-loop test, passes only with these changes) and previously todo/skipped worker-related napi and regression cases run again. LeakSanitizer validation is turned back on for the ~70 worker / MessagePort / BroadcastChannel test files that were exempt. A source lint rejects laundering a JsResult<JSValue> into an empty JSValue. Main-thread process.exit() keeps its current fast path by default; the full main-thread teardown stays behind BUN_DESTRUCT_VM_ON_EXIT=1. Workers always tear down fully.

Known / not in this PR

  • A worker parked in Atomics.wait with no timeout still cannot be terminated (worker: make termination interrupt a worker blocked in Atomics.wait #32802; needs a JSC change).
  • worker_threads message throughput through the real MessagePort is ~0.8× the previous ad-hoc path in a flood microbenchmark (round-trip latency and Web Worker messaging are unchanged); a follow-up, not a behaviour regression.
  • Windows: several concurrent connects to localhost can leave one connect stuck (pre-existing; reproduces on current releases; DNS-coalescing on the connect path).
  • A UDP socket whose receive buffer never drains (e.g. one echoing datagrams to itself on a fast machine) keeps its event loop from running anything else, including a worker's own exit; pre-existing loop-fairness issue, most visible on Windows, follow-up.
  • Memory a burst of concurrent workers used stays resident after they exit (sequential worker churn plateaus; it is the concurrent peak that is not returned to the OS) — allocator thread-exit policy, follow-up.
  • Worker start semantics for a never-settling top-level await, file-stream fairness on a saturated loop, and a few diagnostics-only items found while fuzzing are tracked separately.

Fixes #31281
Fixes #30421
Fixes #15964
Fixes #29173
Fixes #34690
Fixes #31880
Fixes #33936
Fixes #32073
Fixes #33313
Fixes #32828
Fixes #11760
Fixes #26501
Fixes #18661
Fixes #15408
Fixes #23102
Fixes #21101
Fixes #13570
Fixes #31224
Fixes #28643
Fixes #37163
Fixes #25860

Likely also addressed (mechanism matches, not verified end-to-end): #34095; #22376 and the other emscripten-pthread reports (#25454, #19453, #29211, #29635) whose glue installs both a parentPort listener and self.onmessage — the double delivery behind them (#25860) is fixed, the packages themselves were not run; and the parentPort.on('message').unref() hang half of #32609 (its Worker.performance half is API surface, not addressed here).

…e ordered VM teardown

Makes a thread's own lifecycle — Web Worker and node:worker_threads — follow
the upstream WebKit and Node models instead of ad-hoc guards, so terminating
or finishing a worker (including nested workers, and including Windows) tears
everything down in a defined order without touching freed state.

Lifetime model (WebCore)
- ActiveDOMObject and the ScriptExecutionContext registry are real again:
  Worker, MessagePort, BroadcastChannel and WebSocket get a stop phase
  (stop()/hasPendingActivity/suspendIfNeeded) that runs before the JSC VM is
  destroyed; ContextDestructionObserver is refcounted + weak-capable;
  JSEventListener is fenced by JSVMClientData::willDestroyVM.
- ScriptExecutionContext outlives its Zig::GlobalObject and is destroyed after
  ~VM at a defined point; it addresses the thread's VM directly.
- Worker is split into the script-visible object and a WorkerMessagingProxy
  that owns the parent<->thread relationship (message inboxes, cross-VM
  requests, close/exit notification), as upstream does.

Threads
- Worker threads are refcounted and joined by their parent (Node's model)
  instead of detached; a parent tracks its child workers, asks them to stop
  in its own stop phase and joins them before its VM goes away, so
  terminate() propagates through nested workers and resolves only once the
  thread is gone. The thread returns normally rather than pthread_exit.

Teardown
- VirtualMachine::teardown(kind) is the single sequence used by a finished
  worker thread and by main-thread exit under BUN_DESTRUCT_VM_ON_EXIT:
  A) stop with script allowed (WebCore stop phase; servers, listeners, fs/stat
  watchers stopped on every exit, not only under --isolate; socket groups;
  DNS), B) forbid script, cancel timers/immediates, join children, release
  queued work, C) JSC VM teardown, D) free the thread's uSockets loop and, on
  Windows, close its libuv loop after every handle has unlinked, E) destroy.
- Windows: a worker thread now actually closes its loops. The uSockets loop
  over the thread's uv loop is freed; the keep-the-loop-polling timer is
  closed; keep-alive counts routed through the concurrent counter are folded
  before the loop close; the timer heap's embedded uv_timer/uv_idle are
  closed after ~VM (JSC's RunLoop timers ride that heap until then); open
  pipe/tty/process handles are closed through their owners before in-flight
  requests are drained; the loop close logs any handle an owner left open.
- WebSocket keeps one keep-alive mechanism (PendingActivity) and stop() drops
  the connection without calling back into script; a Worker or WebSocket
  constructed after its context stopped never starts.

Behaviour
- worker.terminate() on a running node:worker_threads Worker resolves 1, as
  in Node; threadId stays valid until the thread has exited.
- User close/exit handlers of servers, sockets and listeners run at worker
  terminate (they are stopped, not dropped). A worker whose child worker is
  blocked in an uninterruptible native call now waits for it (as Node does).

Tests: nested terminate propagation, terminate() exit code, terminating a
worker that owns a child process with a pending stdin write.
…ase both event loops' queued work

Review follow-up. The list of open pipe/tty/process handles that a thread
teardown closes was keyed by the reader/writer that started on a handle, but
handles move between owners (spawn stdio -> Subprocess -> PipeReader ->
FileReader via from()), can be adopted without start_with_current_pipe (lazy
subprocess stdout/stderr, every node:child_process spawn), and some never pass
through PipeReader/PipeWriter at all (the IPC channel pipe, named-pipe sockets
before adoption). That left the common stdout paths unlisted, a moved-from
reader dangling in the list, and IPC writes still able to hold the drain.

The list now lives in the libuv wrapper and is keyed by the handle: Pipe::init,
tty init (except the process-static stdin tty) and Process::spawn add the
handle; UvHandle::close / close_and_destroy remove it; whoever drives it
records itself in the entry's owner slot — readers and writers through
Source::set_owner (start, start_with_pipe, lazy adopt and from() all pass
through set_parent/set_data), IPC SendQueue at configure, WindowsNamedPipe
until its writer adopts the pipe, Process at spawn. Teardown closes each open
handle through its current owner, or directly if nothing adopted it. One
close_without_reporting on the writer trait serves both Windows writers and
WindowsNamedPipe.

Also: queued tasks and pending immediates are released on both the regular
and the macro event loop before the JSC VM and the loops go (a macro can leave
work on the macro loop), EventLoop::deinit asserts they are gone, and the one
owner that destroys a VM without a teardown (bake's build VM) releases them
itself first. The new spawn+terminate test is concurrent like its siblings.
…keeps its args in release

A Subprocess's extra stdio pipes (stdio_pipes: uv pipes with no reader or
writer in front of them) are handles the Subprocess owns; it now records
that on the open-handle list, so a thread teardown closes them through it
and finalize_streams later finds the slots empty. finalize_streams also uses
the same is_closing-aware close as WindowsSpawnResult's Drop instead of an
unconditional uv_close (a second close of an already-closing pipe asserts
inside libuv).

The libuv crate's local log macro compiled its arguments out in release, so a
binding used only for logging became an unused-variable error there; it is now
a constant-false branch in release with the arguments still type-checked,
like bun_core::scoped_log!.
@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator
Updated 5:22 AM PT - Aug 8th, 2026

@dylan-conway, your commit 3ff95aa is building: #90570

dylan-conway and others added 24 commits August 6, 2026 16:27
…/drain/join, no "shutdown"/"terminate" for it

Names introduced by this branch used teardown/shutdown/terminate/exit/close
interchangeably. Now: the event is a VM teardown; what its stop phase asks of
an object is stop_for_vm_teardown (WebCore stop() semantics), registries are
stop_all_for_vm_teardown / stop_active_handles_for_vm_teardown; "terminate"
is reserved for the user's worker.terminate() request; "release" drops queued
work unrun; "drain" runs to completion; "join" is for threads; the thread's
loops are free_thread_loop (uSockets) / close_thread_loop (libuv). The
WebSocket client operation used by WebSocket::stop() is what it does —
drop_connection_without_callback — since it is not teardown-specific. C++
that mirrors WebKit keeps WebKit's names.
…g set_pipe (FileSink over a subprocess stdin)
…s it (no producers converted yet)

Adds bun_jsc::VmHandle — an Arc-shared, Send+Sync handle that provides the
only three things off-thread code legitimately needs from a VM: post a
completion to one of its event loops and wake it, ref/unref its keep-alive,
and hold a borrow while using memory the VM owns. Access goes through an
atomic gate (raise `active`, then observe state); VirtualMachine::teardown
marks it Stopping at the start of the stop phase, ScriptForbidden after
forbidExecution, and close()s it after child workers are joined — close
publishes Closed and waits until no thread is inside an access or borrow, so
afterwards nothing off-thread can reach the VM's queues, waker or memory, and
a late post is refused (the poster gets its task back to release itself).
The same object answers script_allowed() for native code that would enter
JS. Every VirtualMachine owns one (handle(), current_loop_kind()).

This commit only adds the type and the teardown flip points; producers still
use their existing pointers and are converted in the following commits.
…le::post; refused completions release off-thread

The three thread-pool carriers no longer capture BackRef<EventLoop> /
BackRef<VirtualMachine> ("the VM outlives every task"). They hold a VmHandle
and the LoopKind current at creation, deliver their completion with
handle.post(), and when the VM has been torn down meanwhile (Refused) release
the task on the pool thread: the context gets release_off_thread() for its
own portable resources, and the carrier's storage is reclaimed without
running the Drops of its JS-thread-only members (keep-alive on a loop that no
longer counts, promise handle of a heap that is gone). This covers every user
of the carriers — node:fs async ops, node:crypto jobs, zlib/brotli/zstd,
transpiler, glob, image, archive, password hashing, the libc dns.lookup
backend — without per-site changes.
…ref/unref are JS-thread operations

KeepAlive::{ref,unref}_concurrently{,_from_event_loop} resolved the VM
through the EventLoopCtx vtable's raw VirtualMachine pointer — the one
cross-thread pair in an otherwise JS-thread interface. Their only remaining
users were napi_ref/unref_threadsafe_function, which Node documents as
main-thread-only; those now use the ordinary ref/unref. The vtable entries,
their VirtualMachine implementation and the MiniEventLoop stubs are removed.
Cross-thread keep-alive goes through VmHandle::ref/unref_keep_alive.
…Machine*

JSVMClientData owns a BunVmHandle* (a boxed clone of the VM's handle, created
on the JS thread, released with the client data). Everything C++ does to a VM
from another thread goes through it: ScriptExecutionContext's ref/unref of the
event loop and postTaskConcurrently, the JSC deferred-work scheduler
(DeferredWorkTimer runs on helper threads), EventLoopTaskNoContext for tasks
run on the work pool, the debugger connection's keep-alive, MessagePort /
BroadcastChannel keep-alive toggles, and the WebView backends. The FFI is
Bun__VmHandle__refKeepAlive / Bun__VmHandle__queueTaskConcurrently /
Bun__queueJSCDeferredWorkTaskConcurrently(handle, job); a post refused after
teardown deletes the C++ task or job unrun on the calling thread. The
VirtualMachine*-taking cross-thread entry points are gone.
# Conflicts:
#	src/jsc/any_task_job.rs
#	src/jsc/event_loop.rs
The tasklet no longer holds `&'static VirtualMachine`. The three HTTP-thread
decisions that used to read the VM's non-atomic is_shutting_down flag — the
last-ref drop (deinit must run on the JS thread), the request-body drain
notification, and the progress callback — now post their task and act on the
outcome: Queued behaves as before; Refused runs the branch that existed for
"the JS side will never see this" (reclaim Rust-side boxes only, drop the
resume ref, free the scheduled response body / release the parked socket /
release both refs). JS-thread paths use the VM through the tasklet's global.
S3HttpSimpleTask and S3HttpDownloadStreamingTask held Option<BackRef<VM>>
set at creation and posted from the HTTP thread through it. They now hold a
VmHandle + LoopKind; a refused post (VM torn down) releases the finished task
on the HTTP thread — the HTTP client's data and header copies (the portable
part of their Drop, split out as release_portable) plus owned buffers and
storage — without the keep-alive unref that only makes sense on the JS
thread. Request setup reads verbose/TLS options from the current VM directly.
…opHandle loses its cross-thread post

EventLoopHandle / JsEventLoop / AnyEventLoop no longer offer enqueue_task_concurrent: their interface is
JS-thread-only, and js_poster() hands out the thread-safe object other threads use instead.
bun_event_loop::JsPoster is an erased VmHandle clone (vtable filled by bun_jsc) for crates that cannot name
VmHandle; bun_jsc::ConcurrentPoster is the typed form for producers that serve either a JS VM or a
MiniEventLoop (mini loops are owned by their thread and keep direct posting).

Converted: node:fs AsyncFSTask (direct thread-pool ops) and fs.cp, spawn's waiter thread → Process exit
delivery (frees the result and drops the process ref if the VM is gone), the bundler's parse/server-
component/plugin/defer hops back to an owning JS loop, JSBundler's plugin notify, and the shell tasks
(interpreter/rm/Async/yes). ThreadSafe<T> can release its storage without unprotecting through a dead
heap; ConcurrentTask::release_refused frees a refused heap task.

VirtualMachine::destroy drops the VM's own handle clone (fixes a 48-byte-per-worker leak of the handle's
shared state). BUN_DESTRUCT_VM_ON_EXIT defaults to on while this branch is in development so every process
exit runs the shared teardown; a TODO marks flipping it back before merge.
… jobs (under borrow), install wake-ups, hot reload, Archive, readdir

- fs.watch (path-watcher thread) and fs.watchFile (pool/timer) post through a handle captured at
  creation; refused batches free themselves and drop their activity ref. FSWatcher's
  `vm() -> &'static mut VirtualMachine` accessor is gone.
- RuntimeTranspilerStore: a TranspilerJob lives inside the VM and reads VM state while it runs on
  the pool, so the whole run happens under VmHandle::borrow() — the VM's teardown waits for it — and
  the completion posts through the handle. First user of the borrow guard.
- The resolver's PackageManager wake handler gets a per-VM WakeContext (module queue + handle)
  instead of recovering the whole VirtualMachine from a field pointer on install/HTTP threads.
- Hot reloader: the watcher thread posts reload tasks through a handle captured at init; the
  HotReloaderEventLoop shim trait and event_loop()/event_loop_ref() on HotReloaderCtx are removed.
- Bun.Archive tasks and recursive readdir complete through the handle with off-thread release.
- VirtualMachine::enqueue_task_concurrent(&mut self) ("from another thread") is deleted.
…on-JS threads; free the wake context per VM

- ConcurrentPoster::Js now carries the JS loop's own erased poster (EventLoopHandle::Js →
  JsEventLoop::js_poster) instead of looking up "the current VM" through TLS, which panicked when a
  shell task was constructed on a thread without a VM (ls/mv/shell tests). Independent of teardown.
- RuntimeState owns the resolver's WakeContext, so it (and the handle it holds) is freed at VM
  teardown instead of leaking 24+48 bytes per worker.
- Converted to post through a handle captured on the JS thread, with off-thread release on refusal:
  Bun.password jobs, the Windows memory-pressure thread (takes a VmHandle, not a VM address),
  zlib/brotli/zstd async writes, napi_async_work, napi threadsafe-function dispatch, napi finalizers
  fired off the JS thread (NapiEnv owns its own handle clone: NapiEnv__vmHandle), Windows
  Bun.write / copyFile mkdirp completions, Bun.build's completion task and plugin dispatch, and the
  dev server's file-watcher hot-reload event.
With every producer converted, EventLoop::enqueue_task_concurrent / ref_concurrently /
unref_concurrently had no callers left and are deleted: another thread reaches a VM's queues, waker
and keep-alive count only through VmHandle (or the erased JsPoster below bun_jsc), which the VM's
teardown closes. The debug-only off-thread read of vm.has_terminated inside enqueue_task_concurrent
goes with it. The Windows Bun.write / copyFile / readFile paths that adjusted the loop's keep-alive
from the JS thread use the honestly named EventLoop::ref_keep_alive / unref_keep_alive.
…) (Node's can_call_into_js)

EventLoop::run_callback / run_callback_with_result / …_and_forcefully_drain_microtasks — the funnel
for native code calling user JS from outside the task queue — refuse the call once teardown has
forbidden script (in addition to the existing pending-exception check).

The ~60 ad-hoc `if vm.is_shutting_down() { return }` guards in front of user callbacks and promise
settlements (TCP/TLS sockets, listeners and SNI/ALPN callbacks, upgraded duplexes, ServerWebSocket
handlers, the server's all-connections-closed promise, MySQL/PostgreSQL connections and queries,
Valkey, HTTP/2 frame dispatch, Archive tasks, fetch progress, thread-pool job completions, stream
cancel signals) now ask script_allowed() instead. Those guards flipped the moment teardown began,
so in an exiting or terminated worker close/error handlers and pending query/promise settlements
were skipped; they now run during the stop phase and are suppressed only after execution is
forbidden. The remaining is_shutting_down() reads are literal "has teardown begun" ordering
decisions (who frees a server/sink/finalizer payload), not JS-entry checks.
VirtualMachine::on_exit can be entered with an exception pending — process.exit() from inside a
throwing or catching callback (an ordinary exception), or after worker.terminate() / a termination
request. It now settles that up front: an ordinary pending exception is cleared before 'exit'
handlers run (as Node's EmitProcessExit does under a TryCatch); if a termination is pending, script
is forbidden for the whole exit sequence, so 'beforeExit'/'exit' handlers and the stop phase's
close events are skipped instead of entering JSC with a termination exception pending (the
`!exception()` / continueDynamicImport assertion family). process 'exit'/'beforeExit' dispatch,
which does not go through run_callback, consults the same gate. Adds VM::has_termination_request().
This was referenced Aug 13, 2026
dylan-conway added a commit that referenced this pull request Aug 14, 2026
### Problem
- Regression from #37075: a `node:vm` `timeout` or `worker.terminate()`
is lost when it lands while an addon is inside one of the ungated N-API
functions. Release builds keep running the script forever
(`vm.runInNewContext(...)` never returns, `await worker.terminate()`
never resolves); debug builds abort on the next exception check with
`ASSERTION FAILED: !!(scope).exception() ==
vm.traps().needHandling(JSC::VMTraps::NeedExceptionHandling)`, or in
`napi_get_value_bigint_int64` with `ASSERTION FAILED: Unexpected
exception observed` from `NAPI_RETURN_SUCCESS`.
- Cause: #37075 rewrote `NAPI_PREAMBLE_NO_PENDING_CHECK`
(`src/jsc/bindings/napi.cpp`) as an unconditional
`JSC::SuspendExceptionScope`. That scope writes the `vm.exception` it
saw on entry back when it exits. On entry nothing is pending; an
exception check inside the body services the pending trap and raises the
`TerminationException`; on exit the scope puts `null` back. The trap was
consumed, so nothing raises it again. Of the 38 C++ users of the macro,
the bodies with such a check are `napi_get_value_string_*`,
`napi_get_value_bigint_int64/uint64`, `napi_create_bigint_int64/uint64`
and `napi_create_symbol` with a description.
- Two related gaps in the same set of functions, fixed along the way:
before #37075 (and in the first version of this PR) the C++ macro
checked for exceptions on entry, so a waiting termination made the
ungated call itself fail with `napi_pending_exception`; and the ungated
functions implemented in `src/runtime/napi/napi_body.rs`
(`napi_create_array*`, `napi_create_string_*`,
`napi_create_int32/uint32/int64`, `napi_get_undefined/null/boolean`,
`napi_is_*`, `napi_get_*_info`, handle scopes) had no handling at all:
`napi_create_array` delivered a termination itself and failed, and the
ones using `JsResult` helpers failed (release) or asserted (debug) with
any exception pending on the VM. Node's `CHECK_ENV`-only functions never
fail for either reason, and node-addon-api aborts the process
(`Error::ThrowAsJavaScriptException napi_throw`) when one of them does;
a node-addon-api callback making only ungated calls hit that under a
`vm` timeout on Bun and not on Node.

### Fix
- `NapiUngatedScope` (napi.cpp) is what every ungated function now runs
under: (a) it stashes the entry exception only when one is actually
pending (`std::optional<JSC::SuspendExceptionScope>`), so a clean VM is
left alone and whatever the body raises stays pending, and (b) it holds
a `JSC::DeferTraps`, so no exception check inside the body (its own or
in the JSC helpers it calls) services VM traps. A termination requested
before or during the call stays a request and is delivered by the next
check after the call returns (the caller's `RETURN_IF_EXCEPTION` in
`NapiClass`, a JS loop check, or a gated N-API call). Nothing is lost,
and the ungated calls never report a termination, as in Node.
- `NAPI_PREAMBLE_NO_PENDING_CHECK` declares one; the 26 Rust functions
construct the same C++ object in place through
`NapiUngatedScope__construct/__destruct` from an `ungated!` macro that
replaces `get_env!` (80 bytes of 8-aligned storage, checked by a
`static_assert`; a guard destroys it on every return path). This also
covers what #36091 and #36093 were addressing separately
(`napi_create_string_*` and the `*_info` accessors with a VM exception
pending).
- Consequence of (b): no JS or addon code may run under the scope. The
zero-length path of `node_api_create_external_string_{latin1,utf16}` ran
the addon's finalizer there; the two functions now share one body
(`createExternalString`) and run the finalizer after its scopes have
closed. The other users only read, allocate, or register callbacks.
- `napi_get_value_bigint_int64` checks for an exception after its
conversion like the `uint64` variant already did.
- Tests, in the `pending-exception gate` block of
`test/napi/napi.test.ts` (addon side in
`test/napi/napi-app/standalone_tests.cpp`, drivers in `module.js` and
`ungated-calls-spin-worker.js`), all compared against Node's output;
each round of ungated calls covers both the C++ and the Rust half:
- a `node:vm` `timeout` on a script looping through the ungated
functions: hangs without the fix (10/10 runs on a release build),
asserts on a debug build
  - `worker.terminate()` of a worker doing the same: same failure modes
- the ungated functions succeed with an exception pending on the VM and
leave it pending
- 200ms of ungated calls under a 20ms timeout with an exception pending:
no call reports the timeout, the pending exception is still the original
one afterwards, and the script still times out on return
- Verified on a debug+ASAN build at 12d4d5c: the four tests pass, the
rest of `test/napi/napi.test.ts` is unchanged, and the upstream
`test_string`, `test_array`, `test_typedarray`, `test_dataview`,
`test_handle_scope`, `test_promise`, `test_date`, `test_error`,
`test_exception`, `test_number`, `test_conversions`,
`2_function_arguments`, `test_buffer`, `test_worker_terminate` suites
pass (a few GC-heavy files exceed the 5s harness timeout under a local
debug build and pass when run directly; same without this change). A
node-addon-api wrapped function looping under `vm` timeouts, which
aborted on every run of 20 with the entry check, survives 60/60 as on
Node.


Supersedes #36091 and #36093 (per-function `SuspendExceptionScope`
wrappers for `napi_create_string_*` and the `*_info` accessors); both
are covered by `ungated!` here.

### Background
- Ungated functions: Node implements pure value constructors/accessors
(`napi_create_object`, `napi_get_cb_info`, `napi_get_value_*`,
references, ...) with `CHECK_ENV` only, so an addon may call them while
an exception is pending, and they never fail because execution is being
terminated. node-addon-api relies on both. In Bun these are split
between `napi.cpp` (`NAPI_PREAMBLE_NO_PENDING_CHECK`) and `napi_body.rs`
(`ungated!`); `NAPI_PREAMBLE` / `preamble!` are the gated versions that
refuse while an exception is pending.
- VM traps: JSC delivers asynchronous requests (a watchdog timeout,
which `node:vm`'s `timeout` uses, or a termination request, which
`worker.terminate()` uses) by setting a trap bit. The next exception
check that services traps (`RETURN_IF_EXCEPTION`, which most JSC entry
points and Bun's `NAPI_RETURN_IF_VM_EXCEPTION` expand to) consumes the
bit and throws the `TerminationException`, which is uncatchable from JS
and is what unwinds the running script. Once thrown, it is the only
record of the request.
- `JSC::DeferTraps` makes trap servicing a no-op for its scope: checks
inside it see no exception and the bits stay set, so the first check
after the scope delivers the request. `JSC::SuspendExceptionScope`
clears `vm.exception` on construction and writes the saved value back
unconditionally on destruction.

<details>
<summary>Earlier shape of this PR (3b36256 to 63653e1)</summary>

The first version kept the pre-#37075 entry check in the C++ macro (so a
waiting termination was delivered by, and reported from, the ungated
call) and wrapped the conditional suspend in a small scope object whose
destructor re-raised a termination the body had delivered after putting
the entry exception back. That fixed the hang but kept the Bun-only
node-addon-api failure mode described under Problem, so it was replaced
by the `DeferTraps` form (f599d79), which dbc32b6 then extended to the
Rust-side functions. The Bun-only test that pinned the re-raise became
the same-output "200ms of ungated calls under a 20ms timeout" test.

</details>

---------

Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Jarred-Sumner pushed a commit that referenced this pull request Aug 14, 2026
… and C++ bindings (#37332)

Scheduled dead-code sweep. Areas this run: `src/libuv_sys`,
`src/cares_sys`, `src/brotli_sys`, `src/simdutf_sys` (both sides of the
FFI), `src/runtime/test_runner`, a few `bun_runtime` leftovers, and C++
bindings outside the files the other open dead-code PRs touch. Net: 43
files, +218 (the new lint) / -1723.

### Removed

**`bun_libuv_sys` (Windows-only crate, -619)**
- 169 `extern "C"` declarations that no Rust code references: the
tcp/udp/tty/poll/prepare/check/fs_poll/threading/dl/os-info/metrics
surface of `uv.h` (`uv_tcp_*`, `uv_udp_*`, `uv_tty_get_winsize`,
`uv_prepare_*`, `uv_check_*`, `uv_fs_poll_*`, `uv_thread_*`, `uv_key_*`,
`uv_once`, `uv_os_get_passwd`, `uv_os_environ`, `uv_queue_work`,
`uv_getnameinfo`, `uv_random`, `uv_async_init`, `uv_dlopen`, ...). The C
symbols themselves stay exported for napi addons via
`symbols.def`/`symbols.dyn`/`linker*.lds` and the force-link list in
`napi_body.rs`; only bun's own unused Rust declarations go.
- Structs and callback aliases only those declarations used:
`uv_getnameinfo_t`, `uv_random_t`, `uv_timespec64_t`, `uv_timeval64_t`,
`uv_dir_t`, `uv_dirent_t`, `uv_env_item_t`, `uv_passwd_t`, `uv_group_t`,
`uv_metrics_t`, `uv_key_t`, `uv_once_t`, `uv_thread_options_t`,
`uv_lib_t`, `uv_getnameinfo_cb`, `uv_random_cb`, `uv_fs_poll_cb`,
`uv_thread_cb`, plus the
`uv_sem_t`/`uv_errno_t`/`uv_handle_s`/`uv_loop_s`/`uv_run_mode`/`uv_pipe_t`/`uv_fs_s`/`struct_uv_req_s`/`struct_uv_stream_s`/`uv_dirent_type_t`/`FILE`/`uv_thread_t`/`uv_loop_option`/`uv_membership`/`uv_tty_vtermstate_t`/`uv_clock_id`
aliases.
- Wrappers nobody called: the `UvReq` marker trait and its 10 impls,
`Loop::dump_active_handles`, `Pipe::set_pending_instances_count`,
`Pipe::as_stream_ptr`, `uv_async_t::init`, `uv_stat_t::birthtime`.
- Alias tables nothing read: `StdioFlags` (the three
`StdioFlags::INHERIT_FD` uses in `bun_spawn` now spell `UV_INHERIT_FD`
like the rest of that function), `UV_FS_O_*` (duplicates of the `O`
module), `UV_PRIORITY_*`, `UV_CLOCK_*`,
`UV_LOOP_BLOCK_SIGNAL`/`UV_METRICS_IDLE_TIME`, the
udp/tty-mode/membership/copyfile flag groups, `UV_MAXHOSTNAMESIZE`,
`UV_IF_NAMESIZE`, `MAX_PIPENAME_LEN`.

**`bun_cares_sys` (-118)**: `ares_library_init`, `ares_version`,
`ares_init`, `ares_set_socket_functions`, `ares_send`, `ares_search`,
`ares_gethostbyname`, `ares_getsock`, `ares_timeout`,
`ares_create_query`, `ares_expand_name`, `ares_expand_string`,
`ares_parse_uri_reply`, `ares_free_string`, the `ares_socket_functions`
and `struct_ares_uri_reply` structs, the `ares_ssize_t`/`struct_timeval`
aliases, two commented-out `ares_fds`/`ares_process` declarations, and
the Windows `timeval`/`iovec` definitions in `lib.rs` that existed only
for them.

**`bun_brotli_sys`**: `BrotliDecoder::{is_finished, get_error_code,
version}` and the `BrotliDecoderIsFinished`/`BrotliDecoderVersion`
declarations (callers use `BrotliDecoderGetErrorCode` directly).

**simdutf FFI (-230)**: 22 unused declarations in `simdutf.rs` and 30
wrappers in `bun-simdutf.cpp` (the 22 plus 8 that had already lost their
Rust declaration), and their stubs in the parser bench shim. The lint
asserts the two files now declare exactly the same set of `simdutf__*`
names.

**`bun_runtime`**
- `test_runner::expect::JSValueTestExt`: 17 forwarder methods (`to_fmt`,
`jest_deep_equals`, `values`, `keys`, `to_u32`, ...) that
`bun_jsc::JSValue` inherent methods of the same name shadow, so no call
site ever resolved to them; rustc reports them unused once the trait is
scoped to the crate. The four methods that do add behavior stay.
- Never-read fields: `CustomMatcherParamsFormatter::global_this` (and
its now-unused lifetime), `SuccessfulReturnsFormatter::global_this`,
`FetchOptions::global_this`.
- `S3ErrorJsc::to_js` (every caller uses the `s3_error_to_js` free fn).

**C++ bindings (roughly -590)**
- `Events_functionGetEventListeners` / `ListenerCount` / `Once` / `On`
in `JSEventEmitter.cpp` (declared in the header, never installed
anywhere) and `jsEventEmitterCast` + `JSEventEmitterWrapper`, whose only
callers they were. `jsEventEmitterCastFast` is untouched.
- `jsFunctionDebugNoop`, `jsFunctionSyncBuiltinExports`
(`NodeModuleModule.cpp`; the live export is
`jsFunctionSyncBuiltinESMExports`).
- `jsCookieStaticFunctionSerialize` (never attached to the constructor;
only `parse`/`from` are) plus its helpers `toCookieWrapped` and
`Cookie::serialize(VM&, span<Ref<Cookie>>)`. The prototype `serialize()`
is untouched.
- Nine `JSC_DECLARE_CUSTOM_GETTER(jsBakeResponsePrototypeGet*)`
declarations with no definition.
- `UTF8Encoding()`, `createMockResultStructure` (the `LazyProperty`
initializer inlines the same code),
`ActiveDOMObject::queueTaskToDispatchEvent` and the
`queueTaskToDispatchEventInternal` / `isAllowedToRunScript` /
`PendingActivity::object` helpers that became unreferenced with it,
`StreamQueue::setTotalSize`, `WorkerMessagingProxy::{askedToTerminate,
loaderContextIdentifier, workerThread}` getters, 19 `m_subspaceFor*` /
`m_clientSubspaceFor*` slots no class allocates from, the
`commonStringInitializer` typedef, the `BUN_FOREACH_CJS_NATIVE_MODULE`
macro, a `DocumentLoadTiming` forward declaration.
- The five cross-realm (transferable stream) stubs in
`CrossRealmTransform.cpp` / `WebStreamsInternals.h`, which only threw
"not implemented" and had no callers, plus `CrossRealmMessageType`.
`WebStreamsInternals.h` described these signatures as frozen
placeholders from the streams port; nothing reaches them, so they are
deleted here, but this is the one group in the PR that is a judgment
call rather than a leftover, so it is easy to drop if you would rather
keep the placeholders.
- `EventInterfaces.h` / `EventTargetInterfaces.h`: checked-in copies of
WebCore's generated enums listing every event and event-target interface
in WebKit. Trimmed to the 5 + 9 entries bun's `Event` / `EventTarget`
subclasses report; survivors keep their numeric values
(`Event::m_eventInterface` is a 7-bit field, and `EventFactory.cpp` /
`EventTargetFactory.cpp` both have `default:` arms).

### Verification

- Each symbol: `rg` across `src/`, `scripts/`, `test/`, `packages/` and
the regenerated `build/debug/codegen/` output (plus substring searches
for token-pasted names and `vendor/WebKit/Source` for anything `extern
"C"`).
- Rust candidates came from scoping same-crate-only `pub` items to
`pub(crate)` and intersecting rustc's `dead_code` output across all 8
target families, so platform-specific items (e.g. `EmptyCopyFileState`
on darwin, `Listener::NamedPipe` on Windows) were excluded
automatically; `bun_libuv_sys` was evaluated on the Windows targets
since the module is `cfg(windows)`.
- `cargo check --workspace` on all 10 CI triples, `cargo check
--workspace --tests`, `cargo fmt --check`, clippy on the touched crates,
full `bun bd`, and `bun bd test` over events, cookies, node:module,
TextEncoder/TextDecoder, expect.extend, toHaveReturnedWith, streams,
fetch, zlib/brotli, mock, and `test/internal/source-lints/` (the dns
file's 30 network-dependent tests fail in the sandbox with ENOTFOUND
both before and after; its 92 offline tests pass).
-
`test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts`
pins 67 of the removed symbols (every entry was checked to match on
`main` and not on this branch).

### Notes for whoever merges

- #32268 adds `RETURN_IF_EXCEPTION` lines inside the `Events_function*`
bodies deleted here (its actual fix is in `jsEventEmitterCastFast`,
which this PR does not touch); whichever lands second needs a trivial
rebase that drops those hunks.
- Found dead but deliberately left alone: `VM::has_termination_request`,
`JSPromise::settle_task`, `job.rs` `on_js_thread` / `off_thread` (all
added two days ago in #37075, presumably for follow-ups);
`JSC__VM__hasTerminationRequest` goes with them. Single unconstructed
variants / unread payloads inside otherwise-live ABI or ported enums
(`SSRKind::Regular`, `DeclarationContext::Keyframes`,
`StmtListWhich::AllStmts`, `OptionsData::Saved(usize)`,
`AsyncState::Done(ExitCode)`) and the remaining partially-used libuv
constant tables were also left as-is. rustc's "field is never read" hits
on `multi_array_columns!` schema structs (`JSMeta`, `BuilderEntry`,
`LineOffsetTable`, `ServerComponentBoundary`, the isolated-install
`Entry`, `BundledAst::tla_check`) and on the type-punned
`SerializedSourceMapLoaded` are live data, not dead code.

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

---

**[review]** gate passed · iteration 0 · 43 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 6 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts
bun test v1.4.0 (3f65303)

test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts:
59 |       ["src/libuv_sys/libuv.rs", /\bUV_FS_O_APPEND\b/],
60 |       ["src/libuv_sys/libuv.rs", /\bUV_PRIORITY_LOW\b/],
61 |       ["src/libuv_sys/libuv.rs", /\bUV_CLOCK_MONOTONIC\b/],
62 |       ["src/spawn/process.rs", /\bStdioFlags\b/],
63 |     ]),
64 |   ).toEqual([]);
         ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/libuv_sys/libuv.rs: \bfn uv_tcp_connect\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_udp_recv_start\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_tty_get_winsize\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_prepare_init\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_fs_poll_start\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_thread_create\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_os_get_passwd\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_queue_work\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_async_init\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_dlopen\b",
... (truncated)

release without fix: 6 FAILED
bun test v1.4.0-canary.1 (8db13c3)

test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts:
59 |       ["src/libuv_sys/libuv.rs", /\bUV_FS_O_APPEND\b/],
60 |       ["src/libuv_sys/libuv.rs", /\bUV_PRIORITY_LOW\b/],
61 |       ["src/libuv_sys/libuv.rs", /\bUV_CLOCK_MONOTONIC\b/],
62 |       ["src/spawn/process.rs", /\bStdioFlags\b/],
63 |     ]),
64 |   ).toEqual([]);
         ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/libuv_sys/libuv.rs: \bfn uv_tcp_connect\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_udp_recv_start\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_tty_get_winsize\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_prepare_init\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_fs_poll_start\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_thread_create\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_os_get_passwd\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_queue_work\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_async_init\b",
+   "src/libuv_sys/libuv.rs: \bfn uv_dlopen\b",
+   "src/libuv_sys/libuv.rs: \bstruct uv_passwd_t\b",
+   "src/libuv_sys/libuv.rs: \bstruct uv_getnameinfo_t\b",
+   "src/libuv_sys/libuv.rs: \bstruct uv_thread_options_t\b",
+   "src/libuv_sys/l
... (truncated)
```

</details>

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

```console
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/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts
bun test v1.4.0 (3f65303)

test/internal/source-lints/dead-symbols-ffi-sys-test-runner-cpp.test.ts:
(pass) dead libuv FFI declarations (windows-only crate) do not reappear [64.93ms]
(pass) dead c-ares and brotli FFI declarations do not reappear [19.79ms]
(pass) dead simdutf wrappers stay removed on both sides of the FFI [35.67ms]
(pass) dead bun_runtime helpers do not reappear [14.26ms]
(pass) dead C++ bindings do not reappear [28.17ms]
(pass) WebCore event interface enums stay trimmed to the interfaces bun implements [9.48ms]

 6 pass
 0 fail
 11 expect() calls
Ran 6 tests across 1 file. [2.71s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 883ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/126] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[2/126] gen generated_host_exports.rs
generated_host_exports.rs: 93 exports (host=3, lazy=10, generic=80, rust=0); 239 extern-C blocks audited
[3/126] gen cpp.rs (cppbind)
[3/126] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_simdutf_sys v0.0.0 (/workspace/bun/src/simdutf_sys)
�[1m�[92m   Compiling�[0m bun_libuv_sys v0.0.0 (/workspace/bun/src/libuv_sys)
�[1m�[92m   Compiling�[0m bun_brotli_sys v0.0.0 (/workspace/bun/src/brotli_sys)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/works
... (truncated)
```

</details>

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

```
src/brotli_sys/brotli_c.rs                         |  14 -
 src/cares_sys/c_ares.rs                            | 110 +---
 src/cares_sys/lib.rs                               |  16 +-
 src/jsc/bindings/BunHttp2CommonStrings.h           |   2 -
 src/jsc/bindings/Cookie.cpp                        |  19 -
 src/jsc/bindings/Cookie.h                          |   2 -
 src/jsc/bindings/DOMFormData.h                     |   1 -
 src/jsc/bindings/JSBakeResponse.cpp                |  10 -
 src/jsc/bindings/JSMockFunction.cpp                |  23 -
 src/jsc/bindings/TextEncoding.cpp                  |   8 -
 src/jsc/bindings/TextEncoding.h                    |   2 -
 src/jsc/bindings/webcore/ActiveDOMObject.cpp       |  21 -
 src/jsc/bindings/webcore/ActiveDOMObject.h         |  13 -
 src/jsc/bindings/webcore/DOMClientIsoSubspaces.h   |  10 -
 src/jsc/bindings/webcore/DOMIsoSubspaces.h         |  11 -
 src/jsc/bindings/webcore/EventInterfaces.h         | 127 ----
 src/jsc/bindings/webcore/EventTargetInterfaces.h   | 104 ----
 src/jsc/bindings/webcore/JSCookie.cpp              |  36 --
 src/jsc/bindings/webcore/JSEventEmitter.cpp        |  91 ---
 src/jsc/bindings/webcore/JSEventEmitter.h          |   5 -
 src/jsc/bindings/webcore/JSEventEmitterCustom.cpp  |   9 -
 src/jsc/bindings/webcore/JSEventEmitterCustom.h    |  21 -
 src/jsc/bindings/webcore/Performance.h             |   1 -
 src/jsc/bindings/webcore/WorkerMessagingProxy.h    |   3 -
 .../webcore/streams/CrossRealmTransform.cpp        |  48 +-
 .../streams/JSWritableStreamDefaultController.cpp  |   2 +-
 src/jsc/bindings/webcore/streams/StreamQueue.h     |   1 -
 src/jsc/bindings/webcore/streams/StreamsForward.h  |   7 -
 .../bindings/webcore/streams/WebStreamsInternals.h |  15 -
 src/jsc/modules/NativeModuleList.h                 |   3 -
 src/jsc/modules/NodeModuleModule.cpp               |  16 -
 src/libuv_sys/libuv.rs                             | 641 +--------------------
 src/parsers/benches/support/s
... (truncated)
```

</details>

**gate history** · 4 passed · 0 rejected · iteration 0

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

```
file                                              reads  edits  tests
src/brotli_sys/brotli_c.rs                            1      2      0
src/cares_sys/c_ares.rs                               5     10      0
src/cares_sys/lib.rs                                  1      2      0
src/jsc/bindings/BunHttp2CommonStrings.h              0      0      0
src/jsc/bindings/Cookie.cpp                           0      0      0
src/jsc/bindings/Cookie.h                             0      0      0
src/jsc/bindings/DOMFormData.h                        0      0      0
src/jsc/bindings/JSBakeResponse.cpp                   0      0      0
src/jsc/bindings/JSMockFunction.cpp                   0      0      0
src/jsc/bindings/TextEncoding.cpp                     0      0      0
src/jsc/bindings/TextEncoding.h                       0      0      0
src/jsc/bindings/webcore/ActiveDOMObject.cpp          0      0      0
src/jsc/bindings/webcore/ActiveDOMObject.h            0      0      0
src/jsc/bindings/webcore/DOMClientIsoSubspaces.h      0      0      0
src/jsc/bindings/webcore/DOMIsoSubspaces.h            0      0      0
src/jsc/bindings/webcore/EventInterfaces.h            1      0      0
(+ 27 more files)
```

</details>

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

---------

Co-authored-by: Alistair Smith <hi@alistair.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment