Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
151 commits
Select commit Hold shift + click to select a range
0f4d30b
Worker / worker_threads: WebCore-shaped lifetimes, joined threads, on…
dylan-conway Aug 6, 2026
88a7c06
fixup: key the Windows open-handle list by handle, not by owner; rele…
dylan-conway Aug 6, 2026
fc2f8dc
fixup: PipeReader::start no longer mutates the source before installi…
dylan-conway Aug 6, 2026
1d4eece
fixup: libuv log helper leaves no binding behind when the log scope c…
dylan-conway Aug 6, 2026
13e47f3
fixup: Subprocess records ownership of its extra stdio pipes; uv log …
dylan-conway Aug 6, 2026
4a455ee
fixup: Subprocess stdio-pipe ownership helper — lifetime parameter an…
dylan-conway Aug 6, 2026
2f50300
fixup: one vocabulary for VM teardown — stop_for_vm_teardown, release…
dylan-conway Aug 6, 2026
8f79c8a
fixup: Windows writers record handle ownership in set_parent, coverin…
dylan-conway Aug 6, 2026
4f76c33
VmHandle: the object other threads hold to reach a VM; teardown close…
dylan-conway Aug 7, 2026
8f0031d
WorkTask / ConcurrentPromiseTask / AnyTaskJob complete through VmHand…
dylan-conway Aug 7, 2026
50aa3e7
KeepAlive loses its cross-thread ref/unref; napi threadsafe-function …
dylan-conway Aug 7, 2026
bcf029d
C++ reaches a VM from other threads through its VmHandle, not Virtual…
dylan-conway Aug 7, 2026
7f6a24a
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 7, 2026
769e724
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
a6b6b02
FetchTasklet: the HTTP thread reaches the VM through VmHandle::post
dylan-conway Aug 7, 2026
4dfb5b8
S3 tasks: the HTTP thread reaches the VM through VmHandle::post
dylan-conway Aug 7, 2026
a7efad8
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
f6dfc90
Off-thread posts below bun_jsc go through an erased JsPoster; EventLo…
dylan-conway Aug 7, 2026
e4a2169
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
dd3483a
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 7, 2026
249e227
More producers reach the VM through VmHandle: fs watchers, transpiler…
dylan-conway Aug 7, 2026
17f7e97
FSWatcher: handle fields are POSIX-only; Windows keeps a JS-thread vm…
dylan-conway Aug 7, 2026
25da5ee
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
158463b
Remaining producers on VmHandle; fix shell poster resolving a VM on n…
dylan-conway Aug 7, 2026
188950d
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
5fb9959
EventLoop has no cross-thread entry points any more
dylan-conway Aug 7, 2026
0c99028
One gate for native code entering JS: VirtualMachine::script_allowed(…
dylan-conway Aug 7, 2026
3cf214d
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
5efa8b1
Decide once whether the exit sequence may run script
dylan-conway Aug 7, 2026
30efd19
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
089eb99
worker_threads: parentPort is a real MessagePort, so close()/unref() …
dylan-conway Aug 7, 2026
c53e113
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
7a79a19
Teardown discards microtasks and nextTicks queued before exit instead…
dylan-conway Aug 7, 2026
674591d
Stop phase: no microtask drain at teardown; Windows cancels in-flight…
dylan-conway Aug 7, 2026
b5ea0b8
Windows: sweep open uv handles only when tearing down a worker
dylan-conway Aug 7, 2026
c00be45
teardown: log between freeing the uSockets loop and closing the uv loop
dylan-conway Aug 7, 2026
003a525
Keep-alive: JS-thread refs bypass the cross-thread handle; Windows lo…
dylan-conway Aug 7, 2026
626a1c2
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
e13136d
Bun.spawnSync's isolated event loop gets its own poster (LoopKind::Is…
dylan-conway Aug 7, 2026
8c70dce
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
c80dccb
TLS over a JS duplex joins the stop phase; a socket finalizer never d…
dylan-conway Aug 7, 2026
f5d860a
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
c52e880
sqlite: an exiting VM checkpoints and closes the connections it opene…
dylan-conway Aug 7, 2026
0331da9
worker_threads: parentPort delivers only after the entry module evalu…
dylan-conway Aug 7, 2026
5e10ca5
sqlite exit sweep: main closes every connection, a worker its own; ru…
dylan-conway Aug 7, 2026
addd276
Windows: a socket over a named pipe joins the stop phase; its error c…
dylan-conway Aug 7, 2026
dfcfaa2
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
d931d33
MessagePort: assigning a non-function to onmessage releases the ref a…
dylan-conway Aug 7, 2026
2f87020
worker: a pending top-level await no longer blocks the worker from st…
dylan-conway Aug 7, 2026
2a71cc9
Turn LSAN leak validation back on for the worker / MessagePort / Broa…
dylan-conway Aug 7, 2026
bbd3403
Script is refused at the native→JS boundary; the stop phase runs to q…
dylan-conway Aug 7, 2026
9421dcd
worker start: "evaluation began" comes from the module loader, the en…
dylan-conway Aug 7, 2026
182fd1d
Delete the per-callsite script_allowed() checks the boundary made dea…
dylan-conway Aug 7, 2026
769b824
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 7, 2026
eced10f
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
13518fd
The task queue owns refusal on the JS thread too; a socket finalizer …
dylan-conway Aug 7, 2026
a6cf32f
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
3d67706
dns: every Resolver with a live c-ares channel joins the stop phase
dylan-conway Aug 7, 2026
dc620af
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
de5802b
dns: a resolver holds a reference across its own terminate-close
dylan-conway Aug 7, 2026
e1da1b8
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
e8896dc
forbidExecution() sets VM::executionForbidden — the flag the native→J…
dylan-conway Aug 7, 2026
a2a835d
Review pass: GlobalEventScope, ThreadSafeRefCount for WebWorker, nami…
dylan-conway Aug 7, 2026
1c0bdad
Stop phase: one sweep with script, forbid, one silent sweep — quiesce…
dylan-conway Aug 7, 2026
c95ca98
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
9a1d239
LoopHandle + Postable: a job captures one handle and cannot forget ho…
dylan-conway Aug 7, 2026
d281502
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
292fa54
Full-branch review fixes: pool work that outlives its VM, fetch/sqlit…
dylan-conway Aug 7, 2026
11ee73b
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 7, 2026
8f4c31a
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
be1c7f5
Source lints: no dead_code escapes on Borrow; SAFETY comments on the …
dylan-conway Aug 7, 2026
fefe6b9
One typed carrier for pool work: bun_jsc::job
dylan-conway Aug 7, 2026
4ee0efc
Exit path: one C++ entry clears what unwound script; forbidExecution …
dylan-conway Aug 7, 2026
80ebced
Embedded pool work is counted on the VM handle: transpile slots and z…
dylan-conway Aug 7, 2026
cc9112e
fetch: a request out on the HTTP thread is registered and counted wor…
dylan-conway Aug 7, 2026
0ee661a
node:fs: every fs.promises operation and recursive readdir are Jobs
dylan-conway Aug 7, 2026
72e66be
Bun.Archive operations and Bun.password hash/verify are Jobs
dylan-conway Aug 7, 2026
bd7bba8
S3 requests and streaming downloads out on the HTTP thread are regist…
dylan-conway Aug 7, 2026
56fb909
fs.watchFile: pool work on a watcher or the per-VM scheduler is count…
dylan-conway Aug 7, 2026
b7c187a
Shell builtins' pool work and fs.cp are counted work of their VM; fs.…
dylan-conway Aug 7, 2026
0657ff9
napi: async work is counted work of its VM and is cancelled if unstar…
dylan-conway Aug 7, 2026
d13ad67
Lints: no dead_code escapes in the JsAffine derive; SAFETY comments o…
dylan-conway Aug 7, 2026
ad428ba
Drop a duplicated comment
dylan-conway Aug 7, 2026
c637700
worker_threads: the parent's public port is ref'ed while the Worker h…
dylan-conway Aug 7, 2026
4b675f9
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
5705e8e
Bun.build from a VM that shuts down mid-build is cancelled, not aband…
dylan-conway Aug 7, 2026
41c68ea
S3 requests hand themselves back when the exiting main thread parks t…
dylan-conway Aug 7, 2026
d6b2103
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
d360ab8
Module loader lookup hooks (resolve / importModule / fetch) run with …
dylan-conway Aug 7, 2026
07d5981
Drop the module-loader hook termination deferral
dylan-conway Aug 7, 2026
f580fa1
Teardown: park the HTTP thread last, refuse new S3 work once stopping…
dylan-conway Aug 7, 2026
0656b77
Bump WebKit to 171babe26c3b (JSModuleLoader: propagate a TerminationE…
dylan-conway Aug 7, 2026
8b02c48
BUN_DESTRUCT_VM_ON_EXIT defaults to off again
dylan-conway Aug 7, 2026
1369a20
Address review comments: stale docs and leftovers, one debug/release …
dylan-conway Aug 7, 2026
4dd45ca
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
6a2324b
terminate() closes the worker's script gate at request time; promise …
dylan-conway Aug 7, 2026
ce6473f
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
1e508bc
Review follow-ups: shell cp starts its fs.cp task without touching th…
dylan-conway Aug 7, 2026
289b77d
Fuzzer batch + review follow-ups: termination window fixes, node:vm t…
dylan-conway Aug 7, 2026
072a485
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
5fc56c1
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 7, 2026
2f3b554
worker: a stop request also ends promise waits during load; own off-t…
dylan-conway Aug 7, 2026
532fe83
Fuzzer batch 2: stop gate at every native→JS entry, drain continuatio…
dylan-conway Aug 7, 2026
c25acf5
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 7, 2026
649ed86
Fuzzer batch 3: cancelled builds fail late requests, a stopped VM's p…
dylan-conway Aug 7, 2026
f4305d4
worker: no IPC endpoint of its own over the process's channel; a reje…
dylan-conway Aug 8, 2026
7c89dee
worker: an uncaught error closes the script gate as it arms terminati…
dylan-conway Aug 8, 2026
4a8aa7e
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 8, 2026
f8d8fba
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
b3b021d
Review round: terminate publishes its flag under vm_lock; entry evalu…
dylan-conway Aug 8, 2026
f8d3ba5
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
0a7c0b4
dns: a connect-path lookup outlives the thread that issued it; cancel…
dylan-conway Aug 8, 2026
968d1ef
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
914ee77
clippy; a promise wait interrupted by the stop gate leaves the termin…
dylan-conway Aug 8, 2026
547ad7a
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
fe13d4c
UDP sockets join the VM's stop phase
dylan-conway Aug 8, 2026
b787111
Hot-path costs of the VM handle; Windows: list file reads on the read…
dylan-conway Aug 8, 2026
faf6ee2
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
0881e1a
Windows: stopping a reader over a SyncFile takes the file path, not u…
dylan-conway Aug 8, 2026
48a0532
Message drains: bounded work per lock acquisition, no whole-queue han…
dylan-conway Aug 8, 2026
2a74fab
napi: an addon's external-buffer finalizers run when its Worker exits…
dylan-conway Aug 8, 2026
6050992
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 8, 2026
58c5adc
Windows: list a reader's file by the boxed File, not by the reader's …
dylan-conway Aug 8, 2026
727a6ec
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
dd125a5
test: keep test_instance_data todo (uv_thread_create is unsupported o…
dylan-conway Aug 8, 2026
757f351
Bun.build release-unstarted: publish only after the JS side is releas…
dylan-conway Aug 8, 2026
c483c91
Windows worker teardown: drain in-flight uv requests inside the stop …
dylan-conway Aug 8, 2026
e68364c
UDP receive is bounded per readiness event; Windows: a file detached …
dylan-conway Aug 8, 2026
436c481
clippy (teardown sweep loop shape, &raw in Bun__VmHandle__stateAddres…
dylan-conway Aug 8, 2026
cf7f6b5
streams: a chunk for a VM that is shutting down is a pending terminat…
dylan-conway Aug 8, 2026
7ca1a53
Three exception-discipline gaps on worker paths (debug asserts under …
dylan-conway Aug 8, 2026
7f4b6fd
Constructing an EventTarget / AbortSignal / event listener allocates …
dylan-conway Aug 8, 2026
fdf16cd
JSVMClientData clients: upstream's names and main-VM exemption; store…
dylan-conway Aug 8, 2026
9b16722
test: terminate() at every native→JS entry point, at three fixed poin…
dylan-conway Aug 8, 2026
2b15932
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
f40caa6
Cross-thread posts never happen inside another lock's critical sectio…
dylan-conway Aug 8, 2026
b931000
test: a completion for a worker that is gone is refused and released …
dylan-conway Aug 8, 2026
7bcfb21
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 8, 2026
6417452
One handle representation across the FFI (a counted reference); refus…
dylan-conway Aug 8, 2026
8133dd1
Revert the per-event cap on UDP receive batches
dylan-conway Aug 8, 2026
9a5ea73
review: LoopKind is the two embedded loops only; JsAffine derive for …
dylan-conway Aug 8, 2026
ec9c105
An accepted socket is accounted for before its open handler is gated …
dylan-conway Aug 8, 2026
d4d4d8b
Every task type says how it is freed unrun: Taskable::release_unrun i…
dylan-conway Aug 8, 2026
c2473f4
Merge remote-tracking branch 'origin/main' into claude/worker-stability
dylan-conway Aug 8, 2026
82d22b4
review: onmessageerror alone does not ref a MessagePort; PipeWriter c…
dylan-conway Aug 8, 2026
825e71f
DuplexUpgradeContext: one queue entry per context; releasing an unrun…
dylan-conway Aug 8, 2026
628e0f4
A server socket that opens under a requested stop gets its wrapper (t…
dylan-conway Aug 8, 2026
20f9dd0
lints: SAFETY comments on the two flagged unsafe blocks, refusal-gate…
dylan-conway Aug 8, 2026
4a896d5
review: NapiEnv drains its finalizers to empty at cleanup; JSCSchedul…
dylan-conway Aug 8, 2026
7cbfbdb
test: say why the late external-buffer finalizer case is bun-only (no…
dylan-conway Aug 8, 2026
3ff95aa
node:worker_threads: a parent's message reaches parentPort only, not …
dylan-conway Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@ int us_loop_close_all_groups(struct us_loop_t *loop) {
int any = 0;
while (g) {
struct us_socket_group_t *next = g->next;
/* Only connecting/connected sockets are stranded — listen sockets are
* 1:1 owned by a Zig Listener / uWS App that holds a raw pointer and
* closes them in finalize(). Closing them here turns that into a UAF
* after drainClosedSockets(). */
/* Only connecting/connected sockets are stranded here. Listen sockets are
* 1:1 owned by a Listener / uWS App that holds a raw pointer to them; the
* runtime's stop phase has already stopped those owners before this sweep,
* and closing a listen socket from under one that was not would be a UAF. */
if (g->head_sockets || g->head_connecting_sockets || g->low_prio_count) {
us_socket_group_close_all_ex(g, /* also_listeners */ 0);
any = 1;
Expand Down
7 changes: 5 additions & 2 deletions packages/bun-uws/src/Loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ struct Loop {
return getLazyLoop().loop;
}

static void clearLoopAtThreadExit() {
if (getLazyLoop().cleanMe) {
/* A thread that ran a loop is exiting: free this thread's loop whether uSockets created the
* native loop (cleanMe) or was handed one (Windows: the thread's libuv loop, which the caller
* closes afterwards; us_loop_free leaves a borrowed native loop alone). */
static void freeLoopAtThreadExit() {
if (getLazyLoop().loop) {
getLazyLoop().loop->free();
}
}
Expand Down
13 changes: 6 additions & 7 deletions scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,13 +623,12 @@ function emitHostExports({ n, cfg, sources, o, dirStamp }: Ctx): void {
// the two crates so unrelated edits (e.g. src/bundler) don't re-run the
// scrape. restat=1 + writeIfNotChanged means a no-marker-change edit
// produces identical output and the cargo step is pruned.
const rsInputs = sources.rust.filter(
p =>
p.endsWith(".rs") &&
(p.includes(`${cfg.cwd}/src/runtime/`.replace(/\//g, "/")) ||
p.includes(`${cfg.cwd}/src/jsc/`.replace(/\//g, "/"))) &&
!p.endsWith("generated_host_exports.rs"),
);
const slashed = (p: string) => p.replace(/\\/g, "/");
const scrapeDirs = [slashed(`${cfg.cwd}/src/runtime/`), slashed(`${cfg.cwd}/src/jsc/`)];
const rsInputs = sources.rust.filter(p => {
const q = slashed(p);
return q.endsWith(".rs") && scrapeDirs.some(d => q.includes(d)) && !q.endsWith("generated_host_exports.rs");
});

n.build({
outputs: [output],
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "ddea71318fec9b923465c7c45ded8fa713ca3251";
export const WEBKIT_VERSION = "171babe26c3b330ac0263d1bed3550571908c838";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
6 changes: 6 additions & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,13 @@ pub mod feature_flag {
new_feature_flag!(pub BUN_ASSUME_PERFECT_INCREMENTAL, "BUN_ASSUME_PERFECT_INCREMENTAL", { default: None });
new_feature_flag!(pub BUN_BE_BUN, "BUN_BE_BUN", {});
new_feature_flag!(pub BUN_DEBUG_NO_DUMP, "BUN_DEBUG_NO_DUMP", {});
// Run the full VM teardown when the main thread exits (workers always do).
// The CI runner turns it on for LeakSanitizer-validated files on ASAN.
new_feature_flag!(pub BUN_DESTRUCT_VM_ON_EXIT, "BUN_DESTRUCT_VM_ON_EXIT", {});
// Test suite only, builds with debug assertions: a worker VM's handle makes
// cross-thread completions wait for its close, so each producer's "refused"
// release path runs deterministically (bun_jsc::vm_handle::refusal_gate).
new_feature_flag!(pub BUN_DEBUG_TEST_WORKER_REFUSAL_GATE, "BUN_DEBUG_TEST_WORKER_REFUSAL_GATE", {});

// Disable "nativeDependencies"
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER, "BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER", {});
Expand Down
16 changes: 13 additions & 3 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ pub trait CompletionStruct: Node + Send + 'static {
transpiler: &mut Transpiler<'a>,
bump: &'a Arena,
) -> Result<(), crate::Error>;
/// Bundle thread, on dequeue: `false` if the owner released this build
/// while it was still queued ([`free_released_unstarted`] then frees it).
fn try_start(&mut self) -> bool;
fn free_released_unstarted(this: *mut Self);
fn complete_on_bundle_thread(&mut self);
fn set_result(&mut self, result: BundleV2Result);
fn set_log(&mut self, log: bun_ast::Log);
Expand All @@ -74,8 +78,8 @@ pub trait CompletionStruct: Node + Send + 'static {
/// `FileMap` layout stays in T6.
fn file_map(&mut self) -> Option<NonNull<FileMap>>;
/// Returns a §Dispatch handle (erased owner + `&'static` vtable) the impl
/// provides, so the bundler can read `result == .err` /
/// `jsc_event_loop.enqueueTaskConcurrent` without naming the concrete
/// provides, so the bundler can read `result == .err` / `is_cancelled`,
/// and post plugin hops to the owning VM, without naming the concrete
/// struct.
fn as_js_bundle_completion_task(&mut self) -> dispatch::CompletionHandle;

Expand Down Expand Up @@ -221,7 +225,13 @@ impl<C: CompletionStruct> BundleThread<C> {
break;
}
// SAFETY: queue stores non-null *mut C pushed via enqueue(); owner keeps it alive
// until complete_on_bundle_thread() signals completion.
// until complete_on_bundle_thread() signals completion — unless it
// released the build while it sat here (its VM went away).
if !unsafe { (*completion).try_start() } {
C::free_released_unstarted(completion);
continue;
}
// SAFETY: as above; started ⇒ the owner waits for us.
let completion = unsafe { &mut *completion };
// SAFETY: `generation` is only read/written on this (bundle) thread.
let generation = unsafe { (*instance).generation };
Expand Down
14 changes: 8 additions & 6 deletions src/bundler/DeferredBatchTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ pub struct DeferredBatchTask {
running: bool,
}

impl bun_event_loop::Taskable for DeferredBatchTask {
const TAG: bun_event_loop::TaskTag = task_tag::BundleV2DeferredBatchTask;
/// Embedded in its `BundleV2`, which outlives the queue entry and owns
/// everything the drain would have touched; nothing to free.
unsafe fn release_unrun(_: *mut Self) {}
}

impl DeferredBatchTask {
pub(crate) fn init(&mut self) {
// Kept as `&mut self` (not `-> Self`) — this struct is embedded
Expand Down Expand Up @@ -47,12 +54,7 @@ impl DeferredBatchTask {
debug_assert!(!self.running);
self.running = false;
}
// PORTING.md §Dispatch: tag+ptr, not TaggedPointer. Tag constant lives in
// `bun_event_loop::task_tag::BundleV2DeferredBatchTask`.
let task = ConcurrentTask::create(Task::new(
task_tag::BundleV2DeferredBatchTask,
std::ptr::from_mut::<Self>(self).cast::<()>(),
));
let task = ConcurrentTask::create(Task::init(std::ptr::from_mut::<Self>(self)));

self.get_bundle_v2().enqueue_on_js_loop_for_plugins(task);
}
Expand Down
100 changes: 100 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ pub struct Graph<'a> {
/// tasks will be run, and the count is "moved" back to `pending_items`
pub(crate) deferred_pending: u32,

/// onResolve / onLoad requests a plugin currently holds (dispatched to its
/// VM, not yet answered). Bundle thread only. Failed wholesale when that
/// VM shuts down mid-build (`BundleV2::is_done`).
pub(crate) outstanding_resolves: OutstandingList<crate::bundle_v2::api::JSBundler::Resolve>,
pub(crate) outstanding_loads: OutstandingList<crate::bundle_v2::api::JSBundler::Load>,
/// The owning VM cancelled this pass; plugin requests were failed and no
/// deferred batch will run.
pub(crate) cancelled: bool,

/// A map of build targets to their corresponding module graphs.
pub build_graphs: EnumMap<options::Target, PathToSourceIndexMap>,

Expand Down Expand Up @@ -160,6 +169,9 @@ impl<'a> Graph<'a> {
ast: MultiArrayList::default(),
pending_items: 0,
deferred_pending: 0,
outstanding_resolves: OutstandingList::default(),
outstanding_loads: OutstandingList::default(),
cancelled: false,
build_graphs: EnumMap::default(),
server_component_boundaries: server_component_boundary::List::default(),
html_imports: HtmlImports::default(),
Expand Down Expand Up @@ -221,6 +233,15 @@ impl<'a> Graph<'a> {
if self.deferred_pending > 0 {
self.pending_items += self.deferred_pending;
self.deferred_pending = 0;
// Their units are back in `pending_items`.
let mut load = self.outstanding_loads.head;
while !load.is_null() {
// SAFETY: linked ⇒ arena-live; bundle thread.
unsafe {
(*load).deferred = false;
load = (*load).outstanding.next;
}
}

transpiler.drain_defer_task.init();
transpiler.drain_defer_task.schedule();
Expand All @@ -237,3 +258,82 @@ impl<'a> Graph<'a> {
// here so `InputFile` and the derived `items_side_effects()` SoA accessor share
// the same type that `LinkerContext::mark_file_live_for_tree_shaking` expects.
use bun_ast::SideEffects;

/// Intrusive doubly-linked membership in an [`OutstandingList`].
pub struct OutstandingLink<T> {
prev: *mut T,
pub(crate) next: *mut T,
linked: bool,
}
impl<T> Default for OutstandingLink<T> {
fn default() -> Self {
Self {
prev: core::ptr::null_mut(),
next: core::ptr::null_mut(),
linked: false,
}
}
}
pub trait OutstandingNode: Sized {
fn link(&mut self) -> &mut OutstandingLink<Self>;
}
/// A bundle pass's outstanding plugin requests; single-threaded (bundle thread).
pub struct OutstandingList<T: OutstandingNode> {
head: *mut T,
}
impl<T: OutstandingNode> Default for OutstandingList<T> {
fn default() -> Self {
Self {
head: core::ptr::null_mut(),
}
}
}
impl<T: OutstandingNode> OutstandingList<T> {
pub(crate) fn push(&mut self, node: *mut T) {
// SAFETY: `node` is arena-live and unlinked; bundle thread.
unsafe {
let l = (*node).link();
debug_assert!(!l.linked);
l.linked = true;
l.prev = core::ptr::null_mut();
l.next = self.head;
if !self.head.is_null() {
(*self.head).link().prev = node;
}
}
self.head = node;
}
/// No-op if `node` is not linked (already answered / never dispatched).
pub(crate) fn unlink(&mut self, node: &mut T) {
let node_ptr: *mut T = node;
let l = node.link();
if !l.linked {
return;
}
l.linked = false;
let (prev, next) = (l.prev, l.next);
l.prev = core::ptr::null_mut();
l.next = core::ptr::null_mut();
// SAFETY: neighbours are linked ⇒ arena-live; bundle thread.
unsafe {
if prev.is_null() {
debug_assert!(core::ptr::eq(self.head, node_ptr));
self.head = next;
} else {
(*prev).link().next = next;
}
if !next.is_null() {
(*next).link().prev = prev;
}
}
}
pub(crate) fn pop(&mut self) -> Option<*mut T> {
let head = self.head;
if head.is_null() {
return None;
}
// SAFETY: linked ⇒ arena-live.
self.unlink(unsafe { &mut *head });
Some(head)
}
}
20 changes: 16 additions & 4 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2841,15 +2841,27 @@ pub mod parse_worker {
.any_loop_mut()
.expect("BundleV2.linker.loop must be set before scheduling ParseTask")
{
bun_event_loop::AnyEventLoop::Js { owner } => {
owner.enqueue_task_concurrent(
bun_event_loop::AnyEventLoop::Js { .. } => {
let ct =
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
}),
);
});
let poster = worker
.ctx
.js_poster
.as_ref()
.expect("JS-owned bundle has a poster");
if let bun_event_loop::Posted::Refused(ct) = poster.post(ct) {
// Owning JS VM torn down mid-bundle: free the hop and the result.
// SAFETY: refused ⇒ we own the task box and the leaked result.
unsafe {
bun_event_loop::ConcurrentTask::ConcurrentTask::release_refused(ct);
drop(bun_core::heap::take(result));
}
}
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// SAFETY: `result` is a valid heap pointer with `task` at the given offset;
Expand Down
29 changes: 20 additions & 9 deletions src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,26 @@ fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) {
.any_loop_mut()
.expect("BundleV2.linker.loop must be set before scheduling ServerComponentParseTask")
{
bun_event_loop::AnyEventLoop::Js { owner } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
}),
);
bun_event_loop::AnyEventLoop::Js { .. } => {
let ct = bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
});
let poster = worker
.ctx
.js_poster
.as_ref()
.expect("JS-owned bundle has a poster");
if let bun_event_loop::Posted::Refused(ct) = poster.post(ct) {
// Owning JS VM torn down mid-bundle: free the hop and the result.
// SAFETY: refused ⇒ we own the task box and the leaked result.
unsafe {
bun_event_loop::ConcurrentTask::ConcurrentTask::release_refused(ct);
drop(bun_core::heap::take(result));
}
}
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// SAFETY: `result` is a freshly Box-leaked `parse_task::Result` (above) and
Expand Down
Loading