From 9ece6edad222b362581449db4167061ae2724750 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 20:47:34 +0000 Subject: [PATCH 01/12] bake: take DevServer and its submodules to zero unsafe DevServer lives in a DevServerCell (JsCell) owned by the server as OwnedThis; uws routes, HMR sockets, deferred requests, the bundler handle and hot-reload tasks reach it through ThisPtr/BackRef + with_mut. HmrSocket and DeferredRequest are CellRefCounted heap objects; the watcher-thread hand-off is an Arc with Guarded event slots and a boxed HotReloadTask; IncrementalGraph methods that touch sibling DevServer state run on a GraphRef split-borrow instead of container_of; body readers own their handler; C++ entry points are HOST_EXPORTs. --- src/bun_alloc/lib.rs | 9 + src/bundler/ParseTask.rs | 2 +- src/bundler/lib.rs | 10 + src/bundler/options.rs | 4 +- src/bundler/transpiler.rs | 2 +- src/http_jsc/headers_jsc.rs | 28 + src/jsc/FetchHeaders.rs | 12 + src/jsc/VmHandle.rs | 22 + .../InspectorBunFrontendDevServerAgent.cpp | 12 +- src/ptr/lib.rs | 84 + src/runtime/bake/BakeSourceProvider.cpp | 10 +- src/runtime/bake/DevServer.rs | 4205 +++++++---------- src/runtime/bake/bake_body.rs | 5 +- src/runtime/bake/dev_server/assets.rs | 19 +- .../bake/dev_server/error_report_request.rs | 95 +- src/runtime/bake/dev_server/hmr_socket.rs | 283 +- .../bake/dev_server/incremental_graph.rs | 2361 ++++----- .../bake/dev_server/inspector_agent.rs | 63 +- src/runtime/bake/dev_server/lifecycle.rs | 59 - src/runtime/bake/dev_server/memory_cost.rs | 23 +- src/runtime/bake/dev_server/mod.rs | 893 ++-- src/runtime/bake/dev_server/route_bundle.rs | 15 + .../bake/dev_server/source_map_store.rs | 62 +- src/runtime/bake/mod.rs | 22 +- src/runtime/dispatch.rs | 44 +- src/runtime/server/AnyRequestContext.rs | 28 +- src/runtime/server/RequestContext.rs | 27 +- src/runtime/server/mod.rs | 50 +- src/runtime/server/server_body.rs | 14 +- src/uws/lib.rs | 2 +- src/uws_sys/BodyReaderMixin.rs | 229 +- src/uws_sys/Response.rs | 20 + src/uws_sys/WebSocket.rs | 67 + src/uws_sys/lib.rs | 2 +- src/watcher/Watcher.rs | 117 +- src/watcher/lib.rs | 3 +- 36 files changed, 4018 insertions(+), 4885 deletions(-) delete mode 100644 src/runtime/bake/dev_server/lifecycle.rs diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 64a4a3b40677..0fff70ef1124 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -117,6 +117,15 @@ impl StdAllocator { // no longer leaks on grow. pub use mimalloc_arena::MimallocArena; pub type Arena = MimallocArena; + +/// The process allocator as a `'static` [`Arena`] ([`MimallocArena::borrowing_default`]): +/// for long-lived `Transpiler<'static>`-style holders whose arena must be +/// genuinely `'static`. Allocations made through it are ordinary global-heap +/// allocations and are only reclaimed by their own destructors. +pub fn default_arena() -> &'static Arena { + static DEFAULT_ARENA: std::sync::OnceLock = std::sync::OnceLock::new(); + DEFAULT_ARENA.get_or_init(MimallocArena::borrowing_default) +} mod baby_vec; pub use baby_vec::BabyVec; /// Arena-backed `Vec` with `u32` length/capacity. diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index bf59458d4fd6..52c5878d6f0c 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2591,7 +2591,7 @@ pub mod parse_worker { // TYPE_ONLY mirrors of `bake.Framework`. Project the fields the parser // reads into the parser-side mirror and bump-alloc // so `opts` can borrow it. - opts.framework = topts.framework.map(|f| { + opts.framework = topts.framework.as_deref().map(|f| { // `Framework` is bump-allocated below, so `Drop` never runs — use arena-owned slices. let projected = js_parser::options::Framework { is_built_in_react: f.is_built_in_react, diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index c87782a50f10..499be8b050fc 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -354,6 +354,16 @@ bun_dispatch::link_interface! { fn register_barrel_export(barrel_path: &[u8], alias: &[u8]); } } +impl DevServerHandle { + /// The handle for a dev server reached through a dispatch-time `ThisPtr` + /// (whose invariant — the pointee stays live for every use through it and + /// its copies — is exactly `of`'s). + #[inline] + pub fn from_this(this: bun_ptr::ThisPtr) -> Self { + // SAFETY: see doc comment. + unsafe { Self::of(this.as_ptr()) } + } +} // SAFETY: the handle is `{ kind, owner: *mut () }`; the raw pointer is what // defeats the auto-impl. `owner` is the single per-process `bake::DevServer` // (established at `unsafe fn new()`), which outlives every bundler worker diff --git a/src/bundler/options.rs b/src/bundler/options.rs index fb2eefed2506..1b47f5870bc6 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -1353,7 +1353,7 @@ pub struct BundleOptions<'a> { // directly — all access goes through crate::dispatch::DevServerVTable. pub dev_server: *const (), /// Set when Bake is bundling. Affects module resolution. - pub framework: Option<&'a crate::bake_types::Framework>, + pub framework: Option>, pub serve_plugins: Option]>>, pub bunfig_path: Box<[u8]>, @@ -1543,7 +1543,7 @@ impl<'a> BundleOptions<'a> { metafile_json_path: self.metafile_json_path.clone(), metafile_markdown_path: self.metafile_markdown_path.clone(), dev_server: self.dev_server, - framework: self.framework, + framework: self.framework.clone(), serve_plugins: self.serve_plugins.clone(), bunfig_path: self.bunfig_path.clone(), unwrap_commonjs_packages: self.unwrap_commonjs_packages, diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 1957363dd686..9c730237fd1f 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1130,7 +1130,7 @@ fn resolver_bundle_options_subset( }, external: src.external.clone(), extra_cjs_extensions: src.extra_cjs_extensions.clone(), - framework: src.framework.map(|f| { + framework: src.framework.as_deref().map(|f| { // Bundler-local `bake_types::BuiltInModule` and // `bun_options_types::BuiltInModule` are nominally distinct (the // former predates the TYPE_ONLY move-down); convert variant-wise. diff --git a/src/http_jsc/headers_jsc.rs b/src/http_jsc/headers_jsc.rs index 19822e15eb4d..ad4c058d53ba 100644 --- a/src/http_jsc/headers_jsc.rs +++ b/src/http_jsc/headers_jsc.rs @@ -143,6 +143,34 @@ pub fn to_fetch_headers( ) } +/// [`to_fetch_headers`], owning the result. +pub fn to_fetch_headers_ref( + this: &Headers, + global: &JSGlobalObject, +) -> JsResult { + use bun_http_types::ETag::HeaderEntryColumns; + use bun_jsc::JsError; + if this.entries.len() == 0 { + return Ok(bun_jsc::fetch_headers::HeadersRef::create_empty()); + } + let names: &[StringPointer] = this.entries.items_name(); + let values: &[StringPointer] = this.entries.items_value(); + // SAFETY: `names`/`values` point into live slices of `this.entries.len()` + // entries; C++ reads exactly `count_` of each and does not retain the pointers. + bun_jsc::fetch_headers::HeadersRef::create( + global, + // C++ side reads only; cast_mut() is safe (no mutation). + names.as_ptr().cast_mut(), + values.as_ptr().cast_mut(), + // `from_bytes` scans for + // non-ASCII and tags UTF-8; `init` would leave the buffer Latin-1 + // and mojibake any UTF-8 header value bytes ≥0x80. + &ZigString::from_bytes(this.buf.as_slice()), + this.entries.len() as u32, + ) + .ok_or(JsError::Thrown) +} + struct H2TestingAPIs; impl H2TestingAPIs { diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index ab443294812c..6ee8bdf0abe9 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -337,6 +337,18 @@ impl HeadersRef { Self(FetchHeaders::create_empty()) } + /// [`FetchHeaders::create`], owning the fresh allocation. + #[inline] + pub fn create( + global: &JSGlobalObject, + names: *mut StringPointer, + values: *mut StringPointer, + buf: &ZigString, + count_: u32, + ) -> Option { + FetchHeaders::create(global, names, values, buf, count_).map(Self) + } + /// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1. #[inline] pub fn create_from_uws(uws_request: *mut c_void) -> Self { diff --git a/src/jsc/VmHandle.rs b/src/jsc/VmHandle.rs index 8b767f90b8a7..30496aa5303e 100644 --- a/src/jsc/VmHandle.rs +++ b/src/jsc/VmHandle.rs @@ -344,6 +344,28 @@ impl VmHandle { }) } + /// Queue an owned `T` on the VM's `kind` loop (dispatched under `T::TAG`, + /// whose arm reclaims the box), or hand it back if the VM is closed. + pub fn post_boxed( + &self, + kind: LoopKind, + task: Box, + ) -> Result<(), Box> { + let raw: *mut T = bun_core::heap::into_raw(task); + let ct = ConcurrentTaskItem::create(bun_event_loop::Task::init(raw)); + match self.post(kind, ct) { + Posted::Queued => Ok(()), + Posted::Refused(ct) => { + // SAFETY: refused ⇒ nothing else holds the carrier `create` boxed + // or the payload we boxed into it two lines up. + unsafe { + drop(bun_core::heap::take(ct.as_ptr())); + Err(bun_core::heap::take(raw)) + } + } + } + } + /// Queue a C++ `EventLoopTask` on the VM's `kind` loop from another /// thread (WebCore's `postTaskTo` / `postTaskConcurrently`), or delete it /// unrun if the VM is closed. diff --git a/src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp b/src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp index bf06efca518b..b546b9b03008 100644 --- a/src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp +++ b/src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp @@ -127,12 +127,18 @@ void InspectorBunFrontendDevServerAgent__notifyClientDisconnected(InspectorBunFr agent->clientDisconnected(devServerId, connectionId); } -void InspectorBunFrontendDevServerAgent__notifyBundleStart(InspectorBunFrontendDevServerAgent* agent, int devServerId, BunString* triggerFiles, size_t triggerFilesLen) +// `bun_core::ffi::FfiSlice` (borrowed). +struct BunStringSlice { + const BunString* ptr; + size_t len; +}; + +void InspectorBunFrontendDevServerAgent__notifyBundleStart(InspectorBunFrontendDevServerAgent* agent, int devServerId, BunStringSlice triggerFiles) { // Create a JSON array for the triggerFiles Ref> files = JSON::ArrayOf::create(); - for (size_t i = 0; i < triggerFilesLen; i++) { - files->addItem(triggerFiles[i].transferToWTFString()); + for (size_t i = 0; i < triggerFiles.len; i++) { + files->addItem(triggerFiles.ptr[i].toWTFString()); } agent->bundleStart(devServerId, WTF::move(files)); diff --git a/src/ptr/lib.rs b/src/ptr/lib.rs index 1c47c9bcbc11..fa9919a02a40 100644 --- a/src/ptr/lib.rs +++ b/src/ptr/lib.rs @@ -704,6 +704,90 @@ unsafe impl Send for BackRef {} // sound. unsafe impl Sync for BackRef {} +// ───────────────────────────────────────────────────────────────────────────── +// ThreadBound — a back-reference other threads may carry but not follow. +// ───────────────────────────────────────────────────────────────────────────── + +/// A [`BackRef`] to a thread-affine `T` that any thread may hold, copy and +/// pass along, but that only the thread it was created on can dereference: +/// [`get`](Self::get) panics everywhere else. +/// +/// This is the typed way to hand a JS-thread object's address to machinery +/// that runs on another thread (a watcher thread's registration, a task it +/// queues back) purely so that it can be handed back to the owning thread. +/// Liveness is the usual `BackRef` holder obligation, stated where the value +/// is constructed; thread affinity is enforced here, which is what makes the +/// `Send`/`Sync` impls below hold for every `T`. +pub struct ThreadBound { + ptr: BackRef, + /// `std`'s id (a process-unique counter, never reused) rather than the OS + /// thread id, so a later thread cannot pass for the owner. + owner: std::thread::ThreadId, +} + +impl ThreadBound { + /// Bind `r`'s address to the calling thread. + #[inline] + pub fn new(r: &T) -> Self { + ThreadBound { + ptr: BackRef::new(r), + owner: std::thread::current().id(), + } + } + + /// Whether the calling thread is the one this reference was created on. + #[inline] + pub fn is_owner_thread(&self) -> bool { + std::thread::current().id() == self.owner + } + + /// Borrow the pointee. Panics when called from any thread other than the + /// one that created this reference. + #[inline] + #[track_caller] + pub fn get(&self) -> &T { + assert!( + self.is_owner_thread(), + "ThreadBound: dereferenced on a thread other than its owner" + ); + self.ptr.get() + } + + /// The pointee's address, for identity comparisons on any thread. + #[inline] + pub fn addr(&self) -> usize { + self.ptr.as_const_ptr().cast::<()>() as usize + } +} + +impl Clone for ThreadBound { + #[inline] + fn clone(&self) -> Self { + ThreadBound { + ptr: self.ptr, + owner: self.owner, + } + } +} + +impl core::fmt::Debug for ThreadBound { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ThreadBound") + .field("ptr", &self.ptr) + .field("owner", &self.owner) + .finish() + } +} + +// SAFETY: the pointee is reachable only through `get`, which refuses every +// thread but the one the reference was created on (where a `&T` already +// existed), so moving the handle to another thread exposes no `T` state there. +unsafe impl Send for ThreadBound {} +// SAFETY: as above — a shared `&ThreadBound` on a foreign thread yields +// nothing but the address; on the owner thread it yields aliased `&T`s, which +// is ordinary same-thread sharing. +unsafe impl Sync for ThreadBound {} + // ───────────────────────────────────────────────────────────────────────────── // DetachablePtr — scoped `&mut T` parked behind `&self` for re-entrant reads. // diff --git a/src/runtime/bake/BakeSourceProvider.cpp b/src/runtime/bake/BakeSourceProvider.cpp index 75995e3a6aab..29b7332808c4 100644 --- a/src/runtime/bake/BakeSourceProvider.cpp +++ b/src/runtime/bake/BakeSourceProvider.cpp @@ -80,7 +80,15 @@ extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatch(GlobalObject* global, BunS return JSC::JSValue::encode(result); } -extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, const char* sourceMapJSONPtr, size_t sourceMapJSONLength) { +// `bun_core::ffi::FfiSlice`; ownership of the buffer transfers to the provider. +struct BakeSourceMapJSON { + const char* ptr; + size_t len; +}; + +extern "C" JSC::EncodedJSValue BakeLoadServerHmrPatchWithSourceMap(GlobalObject* global, BunString source, BakeSourceMapJSON sourceMapJSON) { + const char* sourceMapJSONPtr = sourceMapJSON.ptr; + size_t sourceMapJSONLength = sourceMapJSON.len; JSC::VM&vm = global->vm(); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 3fdade611cc2..24eded8e174b 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -9,15 +9,17 @@ //! Theorized and designed over 2 years out of pure love —— paper clover <3 //! For questions about its core philosophy, email `devserver@paperclover.net` -use ::core::ffi::c_void; +use ::core::cell::Cell; + use bun_bundler::mal_prelude::*; +use bun_ptr::JsCell; use std::io::Write as _; use std::time::Instant; use bun_alloc::{AllocError, Arena}; use bun_ast::Log; use bun_bundler::options_impl::TargetExt as _; -use bun_collections::{ArrayHashMap, DynamicBitSet, HashMap, HiveArrayFallback, StringHashMap}; +use bun_collections::{ArrayHashMap, DynamicBitSet, HashMap, StringHashMap}; use bun_core::{self as str, String as BunString, ZStr, strings}; use bun_core::{Environment, Output}; use bun_jsc::bun_string_jsc; @@ -26,7 +28,7 @@ use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult}; use bun_jsc::{LogJsc as _, StringJsc as _}; use bun_paths::{self as paths, PathBuffer}; use bun_sys as sys; -use bun_uws::{self as uws, AnyResponse, Opcode, Request, WebSocketUpgradeContext}; +use bun_uws::{AnyResponse, Opcode, Request, WebSocketUpgradeContext}; use bun_watcher::WatchItemColumns as _; use bun_wyhash::{Wyhash, hash}; @@ -49,7 +51,6 @@ use crate::bake::dev_server::ResponseLike; pub(super) use crate::bake::dev_server::assets::Assets; pub(super) use crate::bake::dev_server::error_report_request::ErrorReportRequest; -pub(super) use crate::bake::dev_server::HotReloadEvent; pub(super) use crate::bake::dev_server::incremental_graph::IncrementalGraph; pub(super) use crate::bake::dev_server::memory_cost::MemoryCost; @@ -87,7 +88,6 @@ impl DevServer { /// Recover `&mut VirtualMachine` via the global singleton — `self.vm` is /// `*const`. The VM is process-unique on the JS thread, so /// `VirtualMachine::get()` returns the same instance with write provenance. - /// SAFETY: single JS thread; caller must not hold an aliasing `&mut`. #[inline] pub(crate) fn vm_mut(&self) -> &mut VirtualMachine { debug_assert!(::core::ptr::eq( @@ -97,38 +97,80 @@ impl DevServer { VirtualMachine::get_mut() } - // ── transpiler accessors ─────────────────────────────────────────────── - // The three transpilers are `MaybeUninit` until `init()` writes them via - // `Framework::init_transpiler`. Every access after `init()` returns goes - // through these helpers; the SAFETY contract is that `init()` is the only - // constructor and it always populates all three before returning `Ok`. + /// The heap cell this dev server lives in (see [`DevServerCell`]). + #[inline] + pub(crate) fn this(&self) -> bun_ptr::ThisPtr { + self.this.expect("set by init()").this_ptr() + } + #[inline] pub(crate) fn server_transpiler(&self) -> &Transpiler<'static> { - // SAFETY: written in `init()` before any access. - unsafe { self.server_transpiler.assume_init_ref() } + &self.server_transpiler } + #[inline] - pub(crate) fn server_transpiler_mut(&mut self) -> &mut Transpiler<'static> { - // SAFETY: written in `init()` before any access. - unsafe { self.server_transpiler.assume_init_mut() } + pub(crate) fn watcher(&mut self) -> &mut Watcher { + self.bun_watcher.as_deref_mut().expect("watcher is live") } + + /// Split borrow for `DirectoryWatchStore` methods that evict watch entries. #[inline] - pub(crate) fn client_transpiler(&self) -> &Transpiler<'static> { - // SAFETY: written in `init()` before any access. - unsafe { self.client_transpiler.assume_init_ref() } + pub(crate) fn directory_watchers_and_watcher( + &mut self, + ) -> (&mut DirectoryWatchStore, &mut Watcher) { + ( + &mut self.directory_watchers, + self.bun_watcher.as_deref_mut().expect("watcher is live"), + ) } + + /// `client_graph` together with the sibling state its methods reach. #[inline] - pub(crate) fn client_transpiler_mut(&mut self) -> &mut Transpiler<'static> { - // SAFETY: written in `init()` before any access. - unsafe { self.client_transpiler.assume_init_mut() } + pub(crate) fn client_graph_mut( + &mut self, + ) -> incremental_graph::GraphRef<'_, { bake::Side::Client }> { + incremental_graph::GraphRef { + g: &mut self.client_graph, + s: incremental_graph::GraphSiblings { + client_graph: None, + server_graph: Some(&mut self.server_graph), + incremental_result: &mut self.incremental_result, + bundling_failures: &mut self.bundling_failures, + assets: &mut self.assets, + directory_watchers: &mut self.directory_watchers, + bun_watcher: self.bun_watcher.as_deref_mut().expect("watcher is live"), + route_lookup: &self.route_lookup, + root: &self.root, + configuration_hash_key: &self.configuration_hash_key, + graph_safety_lock: &self.graph_safety_lock, + }, + } } + + /// `server_graph` together with the sibling state its methods reach. #[inline] - pub(crate) fn ssr_transpiler_mut(&mut self) -> &mut Transpiler<'static> { - // SAFETY: written in `init()` before any access. - unsafe { self.ssr_transpiler.assume_init_mut() } + pub(crate) fn server_graph_mut( + &mut self, + ) -> incremental_graph::GraphRef<'_, { bake::Side::Server }> { + incremental_graph::GraphRef { + g: &mut self.server_graph, + s: incremental_graph::GraphSiblings { + client_graph: Some(&mut self.client_graph), + server_graph: None, + incremental_result: &mut self.incremental_result, + bundling_failures: &mut self.bundling_failures, + assets: &mut self.assets, + directory_watchers: &mut self.directory_watchers, + bun_watcher: self.bun_watcher.as_deref_mut().expect("watcher is live"), + route_lookup: &self.route_lookup, + root: &self.root, + configuration_hash_key: &self.configuration_hash_key, + graph_safety_lock: &self.graph_safety_lock, + }, + } } } -pub(super) use crate::bake::dev_server::WatcherAtomics; +pub(super) use crate::bake::dev_server::HotReloadShared; pub(super) use crate::bake::dev_server::route_bundle::RouteBundle; pub(super) use crate::bake::dev_server::serialized_failure::SerializedFailure; pub(super) use crate::bake::dev_server::source_map_store::SourceMapStore; @@ -145,12 +187,14 @@ pub struct Options<'a> { pub arena: &'a Arena, pub root: &'a ZStr, pub vm: &'a VirtualMachine, + /// The `Bun.serve` instance that owns the dev server. + pub server: AnyServer, pub framework: bake::Framework, pub bundler_options: bake::SplitBundlerOptions, pub broadcast_console_log_from_browser_to_server: bool, } -// Note: the fields (`arena`, `root`, `vm`, `framework`, +// Note: the fields (`arena`, `root`, `vm`, `server`, `framework`, // `bundler_options`, `broadcast_console_log_from_browser_to_server`) are // required with no sensible zero value, so `Default` is intentionally NOT // implemented. Callers construct `Options` via struct-literal at the call site @@ -243,26 +287,46 @@ pub struct CurrentBundle { pub struct NextBundle { /// A list of `RouteBundle`s which have active requests to bundle it. pub(crate) route_queue: ArrayHashMap, - /// If a reload event exists and should be drained. The information - /// for this watch event is in one of the `watch_events` - pub(crate) reload_event: Option<*mut HotReloadEvent>, // BORROW_FIELD: ptr into dev.watcher_atomics.events[] + /// If a reload event exists and should be drained: the index of its slot + /// in `dev.hot_reload.events`. + pub(crate) reload_event: Option, /// The list of requests that are blocked on this bundle. pub(crate) requests: deferred_request::List, pub(crate) promise: DeferredPromise, } +/// The heap cell a [`DevServer`] lives in. `Bun.serve`'s `NewServer` owns it +/// (`OwnedThis`); the uws routes, HMR sockets, deferred +/// requests, the bundler's `DevServerHandle` and queued hot-reload tasks hold +/// `ThisPtr`/`BackRef`s to it and enter through [`with_mut`](Self::with_mut). +/// All of them are torn down by (or before) `Drop for DevServer`. +pub struct DevServerCell(JsCell); + +impl DevServerCell { + /// JS-thread only; see [`JsCell::with_mut`]. + #[inline] + pub fn with_mut(&self, f: impl FnOnce(&mut DevServer) -> R) -> R { + self.0.with_mut(f) + } + + #[inline] + pub fn get(&self) -> &DevServer { + self.0.get() + } +} + // Note: this is the **canonical** `DevServer` struct. `dev_server/mod.rs` // re-exports it (`pub use super::dev_server_body::DevServer`) so the -// `container_of` submodules (`incremental_graph`, `assets`, …) and the -// 4.8 kL of method bodies in this file all name the same type. The -// `Transpiler<'static>` / `BundleV2<'static>` lifetime is the DevServer-self -// lifetime stand-in: those borrows point at fields stored inline in the -// `Box` allocation, which is never moved post-`init()`. +// submodules (`incremental_graph`, `assets`, …) and the method bodies in this +// file all name the same type. The `Transpiler<'static>` / `BundleV2<'static>` +// borrow `bun_alloc::default_arena()`. pub struct DevServer { /// To validate the DevServer has not been collected, this can be checked. - /// When freed, this is set to `undefined`. UAF here also trips ASAN. pub(crate) magic: Magic, + /// Back-reference to the cell this lives in; set by `init()` right after + /// allocation. + pub(crate) this: Option>, /// Absolute path to project root directory. For the HMR /// runtime, its module IDs are strings relative to this. pub(crate) root: Box<[u8]>, @@ -274,19 +338,11 @@ pub struct DevServer { /// When the value mismatches the page is forcibly reloaded. pub(crate) configuration_hash_key: [u8; 16], /// The virtual machine (global object) to execute code in. - /// JSC_BORROW (LIFETIMES.tsv): passed in via `Options.vm`; deinit no-op. - /// [`BackRef`](bun_ptr::BackRef) (not `&'a`) so `DevServer` is not - /// lifetime-generic — it is `Box`-owned by `ServerInstance` which outlives - /// the VM anyway. The back-reference invariant (pointee outlives holder) - /// is the JSC_BORROW guarantee: vm is valid for DevServer's entire - /// lifetime. + /// [`BackRef`](bun_ptr::BackRef): the VM outlives the `Bun.serve` instance + /// that owns this dev server. pub(crate) vm: bun_ptr::BackRef, - /// How the file-watcher thread submits hot-reload events to the VM. - pub(crate) vm_handle: bun_jsc::VmHandle, - /// May be `None` if not attached to an HTTP server yet. When no server is - /// available, functions taking in requests and responses are unavailable. - /// However, a lot of testing in this mode is missing, so it may hit assertions. - pub(crate) server: Option, + /// The `Bun.serve` instance that owns this dev server. + pub(crate) server: AnyServer, /// Contains the tree of routes. This structure contains FileIndex pub(crate) router: FrameworkRouter, /// Every navigatable route has bundling state here. @@ -341,20 +397,12 @@ pub struct DevServer { pub(crate) server_register_update_callback: jsc::StrongOptional, // Watching - // Note: the watcher thread owns this allocation at shutdown (it frees it - // in `thread_main` once `running` flips false). Auto-dropping a - // `Box` here would free the heap block while the watcher thread - // is still blocked in `GetQueuedCompletionStatus`/`read()` holding a - // `*mut Watcher` into it (and on Windows the kernel still has a pending - // `ReadDirectoryChangesW` against the inline `DirWatcher.buf`/`overlapped`). - // `ManuallyDrop` so `Drop for DevServer` can hand the raw pointer to - // `Watcher::shutdown` instead. - pub(crate) bun_watcher: ::core::mem::ManuallyDrop>, + /// `Some` until `Drop`, which hands the watcher to `shutdown_boxed` (the + /// watcher thread may still be using the allocation, so it frees it). + pub(crate) bun_watcher: Option>, pub(crate) directory_watchers: DirectoryWatchStore, - /// Heap-owned via `heap::into_raw` so a still-queued - /// `HotReloadEvent.concurrent_task` can outlive `DevServer`; see the - /// `next_event` check in `Drop`. - pub(crate) watcher_atomics: ::core::ptr::NonNull, + /// The hot-reload event triple-buffer shared with the watcher thread. + pub(crate) hot_reload: std::sync::Arc, pub(crate) testing_batch_events: TestingBatchEvents, /// Number of bundles that have been executed. This is currently not read, but @@ -364,23 +412,19 @@ pub struct DevServer { pub(crate) bundles_since_last_error: usize, pub(crate) framework: bake::Framework, - pub(crate) bundler_framework_views: Vec<*mut bun_bundler::bake_types::Framework>, pub(crate) bundler_options: bake::SplitBundlerOptions, - // Each logical graph gets its own bundler configuration. - // Note: `'static` is the DevServer-self lifetime stand-in (see - // `CurrentBundle.bv2`). `Transpiler<'a>` borrows the global - // `Fs::FileSystem` singleton + `dot_env::Loader`, both of which outlive - // the server. - // `MaybeUninit` until `Framework::init_transpiler` populates them in place - // (in `init()` below) — `Transpiler` contains a non-nullable `&Arena`, so - // neither `Default` nor `mem::zeroed()` are sound (PORTING.md §Forbidden). - pub(crate) server_transpiler: ::core::mem::MaybeUninit>, - pub(crate) client_transpiler: ::core::mem::MaybeUninit>, - pub(crate) ssr_transpiler: ::core::mem::MaybeUninit>, + // Each logical graph gets its own bundler configuration. Boxed because the + // transpiler's linker holds pointers into it. + pub(crate) server_transpiler: Box>, + pub(crate) client_transpiler: Box>, + /// `None` unless the framework asked for a separate SSR graph (then the + /// server transpiler doubles as the SSR one). + pub(crate) ssr_transpiler: Option>>, /// The log used by all `server_transpiler`, `client_transpiler` and `ssr_transpiler`. /// Note that it is rarely correct to write messages into it. Instead, associate - /// messages with the IncrementalGraph file or Route using `SerializedFailure` - pub(crate) log: Log, + /// messages with the IncrementalGraph file or Route using `SerializedFailure`. + /// Boxed because the transpilers hold its address. + pub(crate) log: Box, pub(crate) plugin_state: PluginState, /// See `CurrentBundle` doc comment. pub(crate) current_bundle: Option, @@ -388,10 +432,9 @@ pub struct DevServer { /// those are temporaried here. When the current bundle is finished, it /// will immediately enqueue this. pub(crate) next_bundle: NextBundle, - pub(crate) deferred_request_pool: - HiveArrayFallback, - /// UWS can handle closing the websocket connections themselves - pub(crate) active_websocket_connections: HashMap<*mut HmrSocket, ()>, + /// UWS can handle closing the websocket connections themselves. Keyed by + /// the socket's address. + pub(crate) active_websocket_connections: HashMap>, // Debugging /// Reference count to number of active sockets with the incremental_visualizer enabled. @@ -429,16 +472,6 @@ pub struct DeferredPromise { } impl DeferredPromise { - pub(crate) fn set_route_bundle_state( - &mut self, - dev: &mut DevServer, - state: route_bundle::State, - ) { - for route_bundle_index in self.route_bundle_indices.keys() { - dev.route_bundle_ptr(*route_bundle_index).server_state = state; - } - } - pub fn reset(&mut self) { self.strong = jsc::JSPromiseStrong::empty(); self.route_bundle_indices.clear_retaining_capacity(); @@ -450,11 +483,8 @@ impl DeferredPromise { } } -/// DevServer is stored on the heap, storing its allocator. -pub(crate) fn init(options: Options) -> JsResult> { - // Note: `Features.dev_server +|= 1` (saturating add). AtomicUsize has - // no `saturating_inc`; on a 64-bit counter overflow is unreachable, so a - // relaxed `fetch_add(1)` is equivalent in practice. +/// DevServer is stored on the heap, in a [`DevServerCell`]. +pub(crate) fn init(options: Options) -> JsResult> { bun_core::analytics::Features::DEV_SERVER.fetch_add(1, ::core::sync::atomic::Ordering::Relaxed); let separate_ssr_graph = options @@ -464,131 +494,10 @@ pub(crate) fn init(options: Options) -> JsResult> { .map(|sc| sc.separate_ssr_graph) .unwrap_or(false); - // Note: `server_transpiler` / `client_transpiler` / `ssr_transpiler` / - // `bun_watcher` / `router` / `watcher_atomics` must be assigned after the - // heap address is stable. Rust forbids partial struct literals and - // `mem::zeroed()` here is UB (`Box` is `NonNull`-backed; - // `Transpiler<'a>` carries `&'a Arena`), so use - // `Box::new_uninit()` + per-field `addr_of_mut!().write()`, leaving those - // fields uninitialized until their real values are computed against - // the stable `*mut DevServer`, then `assume_init()` once every field is written. - use ::core::mem::MaybeUninit; - use ::core::ptr::addr_of_mut; - - let mut dev_uninit: Box> = Box::new(MaybeUninit::uninit()); - let p: *mut DevServer = dev_uninit.as_mut_ptr(); - - let root = paths::string_paths::without_trailing_slash_windows_path(options.root.as_bytes()); - - /// `addr_of_mut!((*p).$field).write($value)` — writes a single field of the - /// partially-initialized `DevServer` without materializing `&mut DevServer`. - macro_rules! w { - ($field:ident, $value:expr) => { - addr_of_mut!((*p).$field).write($value) - }; - } - - // SAFETY: `p` is a freshly-allocated, properly-aligned `*mut DevServer`; each - // `addr_of_mut!((*p).field)` computes an in-bounds field address without - // creating a reference to the (partially-uninit) whole. Every field is written - // exactly once before `assume_init()` below. - unsafe { - w!(magic, Magic::Valid); - w!(root, Box::from(root)); - w!(vm, bun_ptr::BackRef::new(options.vm)); - w!(vm_handle, options.vm.handle()); - w!(server, None); - w!(directory_watchers, DirectoryWatchStore::default()); - w!(server_fetch_function_callback, jsc::StrongOptional::empty()); - w!( - server_register_update_callback, - jsc::StrongOptional::empty() - ); - w!(generation, 0); - w!(graph_safety_lock, ThreadLock::init_unlocked()); - w!(framework, options.framework); - w!(bundler_options, options.bundler_options); - w!(emit_incremental_visualizer_events, 0); - w!(emit_memory_visualizer_events, 0); - w!( - memory_visualizer_timer, - EventLoopTimer::init_paused(EventLoopTimerTag::DevServerMemoryVisualizerTick) - ); - // `dev.frontend_only = dev.framework.file_system_router_types.len == 0` - w!( - frontend_only, - (*addr_of_mut!((*p).framework)) - .file_system_router_types - .is_empty() - ); - w!(client_graph, IncrementalGraph::default()); - w!(server_graph, IncrementalGraph::default()); - w!(barrel_files_with_deferrals, Default::default()); - w!(barrel_needed_exports, Default::default()); - w!(incremental_result, IncrementalResult::EMPTY); - w!(route_lookup, Default::default()); - w!(route_bundles, Vec::new()); - w!(html_router, HTMLRouter::empty()); - w!(active_websocket_connections, Default::default()); - w!(current_bundle, None); - w!( - next_bundle, - NextBundle { - route_queue: Default::default(), - reload_event: None, - requests: deferred_request::List::default(), - promise: DeferredPromise::default(), - } - ); - w!(inspector_server_id, DebuggerId::init(0)); // TODO paper clover: - w!( - assets, - Assets { - path_map: Default::default(), - files: Default::default(), - refs: Default::default(), - needs_reindex: false, - } - ); - w!(source_maps, SourceMapStore::empty()); - w!(plugin_state, PluginState::Unknown); - w!(bundling_failures, Default::default()); - w!( - assume_perfect_incremental_bundling, - bun_core::env_var::feature_flag::BUN_ASSUME_PERFECT_INCREMENTAL - .get() - .unwrap_or(bun_core::env::IS_DEBUG) - ); - w!(testing_batch_events, TestingBatchEvents::Disabled); - w!( - broadcast_console_log_from_browser_to_server, - options.broadcast_console_log_from_browser_to_server - ); - w!(bundles_since_last_error, 0); - w!(has_tailwind_plugin_hack, None); - w!(configuration_hash_key, [0; 16]); - w!(log, Log::init()); - w!(deferred_request_pool, HiveArrayFallback::init()); - - // `.router = undefined` — placeholder until the real router is built below - // (after transpilers + framework.resolve). Constructed empty so the field - // is valid for `assume_init()`; overwritten by `dev.router = 'router: {..}`. - w!( - router, - FrameworkRouter { - root: Box::from(root), - types: Box::new([]), - routes: Vec::new(), - static_routes: Default::default(), - dynamic_routes: Default::default(), - pattern_string_arena: Arena::new(), - } - ); - } - let global = options.vm.global(); let generic_action = "while initializing development server"; + let root = paths::string_paths::without_trailing_slash_windows_path(options.root.as_bytes()); // FileSystem is a process-lifetime singleton; `init` interns the path into // the `DirnameStore` (process-lifetime arena) so no caller-side leak is // needed for the `'static` it stores. @@ -598,557 +507,438 @@ pub(crate) fn init(options: Options) -> JsResult> { }; let top_level_dir: &'static [u8] = bun_resolver::fs::FileSystem::get().top_level_dir; - // `.bun_watcher = undefined` → `Watcher.init(DevServer, dev, fs, ...)` - // SAFETY: `Watcher::init` only stores `p` as an opaque `*mut ()` ctx; it does - // not dereference it until `start()` spawns the watcher thread, by which point - // every `DevServer` field is initialized (`assume_init` below precedes - // `bun_watcher.start()`). - let bun_watcher = match Watcher::init::(p, top_level_dir) { - Ok(w) => w, - Err(err) => { - return Err(global.throw_error( - err, - "while initializing file watcher for development server", - )); - } - }; - // SAFETY: per-field write into uninit struct; see `w!` SAFETY above. - unsafe { w!(bun_watcher, ::core::mem::ManuallyDrop::new(bun_watcher)) }; - - // `.watcher_atomics = undefined` → `WatcherAtomics.init(dev)` - // SAFETY: `WatcherAtomics::init` / `HotReloadEvent::init_empty` only store `p` - // as a BACKREF for later `concurrent_task.from(dev)` / `run`; not dereferenced - // during construction. - unsafe { w!(watcher_atomics, WatcherAtomics::init(p)) }; - - // This causes a memory leak, but the allocator is otherwise used on multiple threads. - // (allocator param dropped — global mimalloc) - - // `.server_transpiler/.client_transpiler/.ssr_transpiler = undefined` → - // `Framework.initTranspiler(..., &dev.X_transpiler, ...)`. - // - // SAFETY: `init_transpiler` writes the slot via `MaybeUninit::write` (see - // `bake_body.rs`), so the previous (uninitialized) bytes are never dropped. - // `framework`/`log`/`bundler_options` were written above; reborrowing each - // individually via `addr_of_mut!` is sound because no `&mut DevServer` exists. - // Note: `Transpiler<'static>` erases the arena lifetime — `options.arena` - // is the `UserOptions.arena` which is moved into / outlives the `DevServer` - // box. Widen `'a → 'static` here once. - // SAFETY: `options.arena` outlives every `Transpiler` field it backs (see - // `Options::arena` doc — "must live until DevServer drops"). - let arena: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(options.arena) }; - let mut bundler_framework_views: Vec<*mut bun_bundler::bake_types::Framework> = - Vec::with_capacity(4); - // SAFETY: `p` points into the boxed `MaybeUninit`; the fields - // accessed below were each written above and are reborrowed disjointly via - // `addr_of_mut!`, so no overlapping `&mut` exists. - unsafe { - let framework = &mut *addr_of_mut!((*p).framework); - let log = &mut *addr_of_mut!((*p).log); - let bundler_options = &mut *addr_of_mut!((*p).bundler_options); - - match framework.init_transpiler( + // The transpilers outlive `options.arena` (they are `'static`), so their + // long-lived allocations (define tables) come from the process arena. + let arena = bun_alloc::default_arena(); + let mut framework = options.framework; + let bundler_options = options.bundler_options; + let mut log = Box::new(Log::init()); + + let server_transpiler = framework + .init_transpiler( arena, - log, + &mut log, bake::Mode::Development, bake::Graph::Server, - &mut *addr_of_mut!((*p).server_transpiler), &bundler_options.server, - ) { - Ok(view) => bundler_framework_views.push(view), - Err(err) => return Err(global.throw_error(err, generic_action)), - } - match framework.init_transpiler( + ) + .map_err(|err| global.throw_error(err, generic_action))?; + let client_transpiler = framework + .init_transpiler( arena, - log, + &mut log, bake::Mode::Development, bake::Graph::Client, - &mut *addr_of_mut!((*p).client_transpiler), &bundler_options.client, - ) { - Ok(view) => bundler_framework_views.push(view), - Err(err) => return Err(global.throw_error(err, generic_action)), - } - if separate_ssr_graph { - match framework.init_transpiler( - arena, - log, - bake::Mode::Development, - bake::Graph::Ssr, - &mut *addr_of_mut!((*p).ssr_transpiler), - &bundler_options.ssr, - ) { - Ok(view) => bundler_framework_views.push(view), - Err(err) => return Err(global.throw_error(err, generic_action)), - } - } else { - // Note: when `!separate_ssr_graph`, `ssr_transpiler` is never - // read, but a valid value must still be written before - // `assume_init()`. Bitwise-alias the server - // transpiler. This is sound only because `Drop for DevServer` gates - // `ssr_transpiler.assume_init_drop()` behind the same - // `separate_ssr_graph` check (see Drop impl): in the `!separate` - // branch the alias is never independently dropped, so there is no - // double-free. Do not remove that gate. - ::core::ptr::copy_nonoverlapping( - addr_of_mut!((*p).server_transpiler).cast_const(), - addr_of_mut!((*p).ssr_transpiler), - 1, - ); - } - - w!(bundler_framework_views, bundler_framework_views); - } + ) + .map_err(|err| global.throw_error(err, generic_action))?; + let ssr_transpiler = if separate_ssr_graph { + Some( + framework + .init_transpiler( + arena, + &mut log, + bake::Mode::Development, + bake::Graph::Ssr, + &bundler_options.ssr, + ) + .map_err(|err| global.throw_error(err, generic_action))?, + ) + } else { + None + }; - // ── every field is now written ─────────────────────────────────────────── - // SAFETY: all fields of `*p` were written exactly once above via - // `addr_of_mut!().write()` / `copy_nonoverlapping`; no field remains uninit. - let mut dev: Box = unsafe { dev_uninit.assume_init() }; - let dev_ptr: *mut DevServer = &raw mut *dev; + let hot_reload = HotReloadShared::new(None, options.vm.handle()); + let bun_watcher = Watcher::init_with_handler( + Box::new(crate::bake::dev_server::DevWatcherHandler { + shared: std::sync::Arc::clone(&hot_reload), + }), + top_level_dir, + ) + .map_err(|err| { + global.throw_error( + err, + "while initializing file watcher for development server", + ) + })?; - // Note: the graphs are stored by value; `owner()` is provided on - // `IncrementalGraph` via `offset_of!` so the parent-pointer invariant is - // structural. Retain the field touches so the addresses are stable for - // `container_of` consumers below. - let _ = ( - &dev.server_graph, - &dev.client_graph, - &dev.directory_watchers, - ); + let owned = bun_ptr::OwnedThis::new(DevServerCell(JsCell::new(DevServer { + magic: Magic::Valid, + this: None, + root: Box::from(root), + vm: bun_ptr::BackRef::new(options.vm), + server: options.server, + directory_watchers: DirectoryWatchStore::default(), + server_fetch_function_callback: jsc::StrongOptional::empty(), + server_register_update_callback: jsc::StrongOptional::empty(), + generation: 0, + graph_safety_lock: ThreadLock::init_unlocked(), + frontend_only: framework.file_system_router_types.is_empty(), + framework, + bundler_options, + emit_incremental_visualizer_events: 0, + emit_memory_visualizer_events: 0, + memory_visualizer_timer: EventLoopTimer::init_paused( + EventLoopTimerTag::DevServerMemoryVisualizerTick, + ), + client_graph: IncrementalGraph::default(), + server_graph: IncrementalGraph::default(), + barrel_files_with_deferrals: Default::default(), + barrel_needed_exports: Default::default(), + incremental_result: IncrementalResult::EMPTY, + route_lookup: Default::default(), + route_bundles: Vec::new(), + html_router: HTMLRouter::empty(), + active_websocket_connections: Default::default(), + current_bundle: None, + next_bundle: NextBundle { + route_queue: Default::default(), + reload_event: None, + requests: deferred_request::List::default(), + promise: DeferredPromise::default(), + }, + inspector_server_id: DebuggerId::init(0), + assets: Assets { + path_map: Default::default(), + files: Default::default(), + refs: Default::default(), + needs_reindex: false, + }, + source_maps: SourceMapStore::empty(), + plugin_state: PluginState::Unknown, + bundling_failures: Default::default(), + assume_perfect_incremental_bundling: + bun_core::env_var::feature_flag::BUN_ASSUME_PERFECT_INCREMENTAL + .get() + .unwrap_or(bun_core::env::IS_DEBUG), + testing_batch_events: TestingBatchEvents::Disabled, + broadcast_console_log_from_browser_to_server: options + .broadcast_console_log_from_browser_to_server, + bundles_since_last_error: 0, + has_tailwind_plugin_hack: None, + configuration_hash_key: [0; 16], + log, + server_transpiler, + client_transpiler, + ssr_transpiler, + bun_watcher: Some(bun_watcher), + hot_reload, + // Placeholder until the real router is built below (after the + // transpilers + `framework.resolve`). + router: FrameworkRouter { + root: Box::from(root), + types: Box::new([]), + routes: Vec::new(), + static_routes: Default::default(), + dynamic_routes: Default::default(), + pattern_string_arena: Arena::new(), + }, + }))); + let this_ref: bun_ptr::BackRef = owned.this_ptr().into(); + let scratch_arena = options.arena; + + owned.with_mut(|dev| -> JsResult<()> { + dev.this = Some(this_ref); + dev.hot_reload.main.lock().dev = Some(bun_ptr::ThreadBound::new(this_ref.get())); + + let _unlock = dev.graph_safety_lock.guard(); + + if let Err(err) = dev.watcher().start() { + return Err(global.throw_error( + err, + "while initializing file watcher thread for development server", + )); + } - let _unlock = dev.graph_safety_lock.guard(); + // `bun_resolver::AnyResolveWatcher` is now a re-export of + // `bun_watcher::AnyResolveWatcher` (LAYERING: same type), so the watcher's + // vtable flows directly into the resolver without conversion. + let resolve_watcher = dev.watcher().get_resolve_watcher(); + // The bundler only null-checks `options.dev_server`; the dispatch + // handle proper is handed to each `BundleV2` in `start_async_bundle`. + let handle_marker = dev.bundler_handle().owner.cast_const(); + dev.server_transpiler.options.dev_server = handle_marker; + dev.server_transpiler.resolver.watcher = Some(resolve_watcher); + dev.client_transpiler.options.dev_server = handle_marker; + dev.client_transpiler.resolver.watcher = Some(resolve_watcher); + if let Some(ssr) = &mut dev.ssr_transpiler { + ssr.options.dev_server = handle_marker; + ssr.resolver.watcher = Some(resolve_watcher); + } - if let Err(err) = dev.bun_watcher.start() { - return Err(global.throw_error( - err, - "while initializing file watcher thread for development server", - )); - } + debug_assert!(dev.server_transpiler.resolver.opts.target != bun_ast::Target::Browser); + debug_assert!(dev.client_transpiler.resolver.opts.target == bun_ast::Target::Browser); - // `bun_resolver::AnyResolveWatcher` is now a re-export of - // `bun_watcher::AnyResolveWatcher` (LAYERING: same type), so the watcher's - // vtable flows directly into the resolver without conversion. - let resolve_watcher = dev.bun_watcher.get_resolve_watcher(); - dev.server_transpiler_mut().options.dev_server = dev_ptr as *const (); - dev.server_transpiler_mut().resolver.watcher = Some(resolve_watcher); - dev.client_transpiler_mut().options.dev_server = dev_ptr as *const (); - dev.client_transpiler_mut().resolver.watcher = Some(resolve_watcher); - - if separate_ssr_graph { - dev.ssr_transpiler_mut().options.dev_server = dev_ptr as *const (); - dev.ssr_transpiler_mut().resolver.watcher = Some(resolve_watcher); - } - - debug_assert!(dev.server_transpiler().resolver.opts.target != bun_ast::Target::Browser); - debug_assert!(dev.client_transpiler().resolver.opts.target == bun_ast::Target::Browser); - - // Note: reborrow `framework` and the two resolvers via `dev_ptr` so - // borrowck doesn't see three overlapping `&mut dev`. - // SAFETY: `dev_ptr` is the live `Box` heap address; the three - // fields are disjoint. - if unsafe { &mut (*dev_ptr).framework } - .resolve( - // SAFETY: see above; `server_transpiler` was initialized and is disjoint from `framework`. - unsafe { &mut (*(*dev_ptr).server_transpiler.as_mut_ptr()).resolver }, - // SAFETY: see above; `client_transpiler` was initialized and is disjoint from `framework`. - unsafe { &mut (*(*dev_ptr).client_transpiler.as_mut_ptr()).resolver }, - options.arena, - ) - .is_err() - { - if dev.framework.is_built_in_react { - bake::Framework::add_react_install_command_note(&mut dev.log); + if dev + .framework + .resolve( + &mut dev.server_transpiler.resolver, + &mut dev.client_transpiler.resolver, + scratch_arena, + ) + .is_err() + { + if dev.framework.is_built_in_react { + bake::Framework::add_react_install_command_note(&mut dev.log); + } + return Err(global.throw_value(dev.log.to_js_aggregate_error( + global, + format_args!("Framework is missing required files!"), + )?)); } - return Err(global.throw_value(dev.log.to_js_aggregate_error( - global, - format_args!("Framework is missing required files!"), - )?)); - } - // `init_transpiler` arena-snapshots the framework projection, so the - // transpilers do not see the resolved - // `server_runtime_import` / `react_fast_refresh.import_source`; - // re-project after resolve so parser-generated imports (e.g. - // `serverRuntimeImportSource` in `wrap_exports_for_client_reference`) see - // absolute paths instead of the user's relative `"./framework/server.ts"`. - { - let resolved_ptr: *mut bun_bundler::bake_types::Framework = - arena.alloc(dev.framework.as_bundler_view()); - dev.bundler_framework_views.push(resolved_ptr); - // SAFETY: `resolved_ptr` is a fresh arena slot, no aliasing borrows. - let resolved_view: &'static bun_bundler::bake_types::Framework = unsafe { &*resolved_ptr }; - dev.server_transpiler_mut().options.framework = Some(resolved_view); - dev.client_transpiler_mut().options.framework = Some(resolved_view); - if separate_ssr_graph { - dev.ssr_transpiler_mut().options.framework = Some(resolved_view); + // `init_transpiler` snapshots the framework projection, so the + // transpilers do not see the resolved + // `server_runtime_import` / `react_fast_refresh.import_source`; + // re-project after resolve so parser-generated imports (e.g. + // `serverRuntimeImportSource` in `wrap_exports_for_client_reference`) see + // absolute paths instead of the user's relative `"./framework/server.ts"`. + { + let resolved_view = std::sync::Arc::new(dev.framework.as_bundler_view()); + dev.server_transpiler.options.framework = Some(std::sync::Arc::clone(&resolved_view)); + dev.client_transpiler.options.framework = Some(std::sync::Arc::clone(&resolved_view)); + if let Some(ssr) = &mut dev.ssr_transpiler { + ssr.options.framework = Some(resolved_view); + } } - } - // errdefer dev.route_lookup.clearAndFree() / client_graph.deinit() / server_graph.deinit() + dev.configuration_hash_key = 'hash_key: { + let mut h = Wyhash::init(128); - dev.configuration_hash_key = 'hash_key: { - let mut h = Wyhash::init(128); + if bun_core::env::IS_DEBUG { + let stat = match sys::stat( + bun_core::self_exe_path() + .unwrap_or_else(|e| Output::panic(format_args!("unhandled {}", e))), + ) { + Ok(s) => s, + Err(e) => Output::panic(format_args!("unhandled {}", e)), + }; + // Note: `sys::Stat` is `libc::stat` on POSIX / `uv_stat_t` on + // Windows (where mtime is `mtim.sec`). Debug-only cache-bust key. + #[cfg(not(windows))] + bun_core::write_any_to_hasher(&mut h, stat.st_mtime as i64); + #[cfg(windows)] + bun_core::write_any_to_hasher(&mut h, &(stat.mtim.sec as i64)); + h.update(crate::bake::bake_body::get_hmr_runtime(bake::Side::Client).code); + h.update(crate::bake::bake_body::get_hmr_runtime(bake::Side::Server).code); + } else { + h.update(Environment::GIT_SHA_SHORT.as_bytes()); + } - if bun_core::env::IS_DEBUG { - let stat = match sys::stat( - bun_core::self_exe_path() - .unwrap_or_else(|e| Output::panic(format_args!("unhandled {}", e))), - ) { - Ok(s) => s, - Err(e) => Output::panic(format_args!("unhandled {}", e)), - }; - // Note: `sys::Stat` is `libc::stat` on POSIX / `uv_stat_t` on - // Windows (where mtime is `mtim.sec`). Debug-only cache-bust key. - #[cfg(not(windows))] - bun_core::write_any_to_hasher(&mut h, stat.st_mtime as i64); - #[cfg(windows)] - bun_core::write_any_to_hasher(&mut h, &(stat.mtim.sec as i64)); - h.update(crate::bake::bake_body::get_hmr_runtime(bake::Side::Client).code); - h.update(crate::bake::bake_body::get_hmr_runtime(bake::Side::Server).code); - } else { - h.update(Environment::GIT_SHA_SHORT.as_bytes()); - } + for fsr in &dev.framework.file_system_router_types { + bun_core::write_any_to_hasher(&mut h, fsr.allow_layouts as u8); + bun_core::write_any_to_hasher(&mut h, fsr.ignore_underscores as u8); + h.update(&fsr.entry_server); + h.update(&[0]); + h.update(fsr.entry_client.as_deref().unwrap_or(b"")); + h.update(&[0]); + h.update(&fsr.prefix); + h.update(&[0]); + h.update(&fsr.root); + h.update(&[0]); + for ext in &fsr.extensions { + h.update(ext); + h.update(&[0]); + } + h.update(&[0]); + for dir in &fsr.ignore_dirs { + h.update(dir); + h.update(&[0]); + } + h.update(&[0]); + } - for fsr in &dev.framework.file_system_router_types { - bun_core::write_any_to_hasher(&mut h, fsr.allow_layouts as u8); - bun_core::write_any_to_hasher(&mut h, fsr.ignore_underscores as u8); - h.update(&fsr.entry_server); - h.update(&[0]); - h.update(fsr.entry_client.as_deref().unwrap_or(b"")); - h.update(&[0]); - h.update(&fsr.prefix); - h.update(&[0]); - h.update(&fsr.root); - h.update(&[0]); - for ext in &fsr.extensions { - h.update(ext); + if let Some(sc) = &dev.framework.server_components { + bun_core::write_any_to_hasher(&mut h, 1u8); + bun_core::write_any_to_hasher(&mut h, sc.separate_ssr_graph as u8); + h.update(&sc.client_register_server_reference); + h.update(&[0]); + h.update(&sc.server_register_client_reference); + h.update(&[0]); + h.update(&sc.server_register_server_reference); + h.update(&[0]); + h.update(&sc.server_runtime_import); h.update(&[0]); + } else { + bun_core::write_any_to_hasher(&mut h, 0u8); } - h.update(&[0]); - for dir in &fsr.ignore_dirs { - h.update(dir); + + if let Some(rfr) = &dev.framework.react_fast_refresh { + bun_core::write_any_to_hasher(&mut h, 1u8); + h.update(&rfr.import_source); + } else { + bun_core::write_any_to_hasher(&mut h, 0u8); + } + + for (k, v) in dev + .framework + .built_in_modules + .keys() + .iter() + .zip(dev.framework.built_in_modules.values()) + { + h.update(k); + h.update(&[0]); + // Hash the active tag, then the payload. + let (tag, data): (u8, &[u8]) = match v { + bun_bundler::bake_types::BuiltInModule::Import(d) => (0, &d[..]), + bun_bundler::bake_types::BuiltInModule::Code(d) => (1, &d[..]), + }; + bun_core::write_any_to_hasher(&mut h, tag); + h.update(data); h.update(&[0]); } h.update(&[0]); - } - if let Some(sc) = &dev.framework.server_components { - bun_core::write_any_to_hasher(&mut h, 1u8); - bun_core::write_any_to_hasher(&mut h, sc.separate_ssr_graph as u8); - h.update(&sc.client_register_server_reference); - h.update(&[0]); - h.update(&sc.server_register_client_reference); - h.update(&[0]); - h.update(&sc.server_register_server_reference); - h.update(&[0]); - h.update(&sc.server_runtime_import); - h.update(&[0]); - } else { - bun_core::write_any_to_hasher(&mut h, 0u8); - } + let mut out = [0u8; 16]; + bun_core::fmt::bytes_to_hex_lower(&h.final_().to_ne_bytes(), &mut out); + break 'hash_key out; + }; + // Add react fast refresh if needed. This is the first file on the client side, + // as it will be referred to by index. if let Some(rfr) = &dev.framework.react_fast_refresh { - bun_core::write_any_to_hasher(&mut h, 1u8); - h.update(&rfr.import_source); - } else { - bun_core::write_any_to_hasher(&mut h, 0u8); + let idx = dev.client_graph.insert_stale( + &mut dev.assets, + &rfr.import_source, + bake::Graph::Client, + )?; + debug_assert!(idx == incremental_graph::FileIndex::<{ bake::Side::Client }>::init(0)); } - for (k, v) in dev - .framework - .built_in_modules - .keys() - .iter() - .zip(dev.framework.built_in_modules.values()) - { - h.update(k); - h.update(&[0]); - // Hash the active tag, then the payload. - // `bun_bundler::bake_types::BuiltInModule` has no `.tag()`/`.data_slice()`; - // shim locally with a match (upstream type — cannot add inherent methods). - let (tag, data): (u8, &[u8]) = match v { - bun_bundler::bake_types::BuiltInModule::Import(d) => (0, &d[..]), - bun_bundler::bake_types::BuiltInModule::Code(d) => (1, &d[..]), - }; - bun_core::write_any_to_hasher(&mut h, tag); - h.update(data); - h.update(&[0]); + if !dev.frontend_only { + dev.init_server_runtime(); } - h.update(&[0]); - let mut out = [0u8; 16]; - bun_core::fmt::bytes_to_hex_lower(&h.final_().to_ne_bytes(), &mut out); - break 'hash_key out; - }; + // Initialize FrameworkRouter + dev.router = 'router: { + let mut types: Vec = + Vec::with_capacity(dev.framework.file_system_router_types.len()); - // Add react fast refresh if needed. This is the first file on the client side, - // as it will be referred to by index. - if let Some(rfr) = &dev.framework.react_fast_refresh { - // `debug_assert!` does not evaluate its argument in release. Hoist - // the side-effecting `insert_stale` - // out so the refresh runtime is registered at index 0 in all builds. - let idx = dev - .client_graph - .insert_stale(&rfr.import_source, bake::Graph::Client)?; - debug_assert!(idx == incremental_graph::FileIndex::<{ bake::Side::Client }>::init(0)); - } - - if !dev.frontend_only { - dev.init_server_runtime(); - } - - // Initialize FrameworkRouter - dev.router = 'router: { - let mut types: Vec = - Vec::with_capacity(dev.framework.file_system_router_types.len()); - - // Note: the loop body mutates `server_transpiler.resolver`, - // `server_graph`, `client_graph`, and `route_lookup` while iterating - // `framework.file_system_router_types` by shared ref. All five fields - // are disjoint; reborrow them through `dev_ptr` so the shared - // `framework` iter does not lock `*dev`. - // SAFETY: `dev_ptr` is the live `Box` heap address; `framework` - // is read-only here and disjoint from the fields mutated in the loop body. - for (i, fsr) in unsafe { &(*dev_ptr).framework } - .file_system_router_types - .iter() - .enumerate() - { - let mut buf = paths::path_buffer_pool::get(); - let joined_root = paths::resolve_path::join_abs_string_buf::( - &dev.root, - &mut buf[..], - &[&fsr.root], - ); - // SAFETY: `server_transpiler` was fully initialized by `init_transpiler` - // above; `.resolver` is disjoint from `framework`. - let Some(entry) = unsafe { (*dev_ptr).server_transpiler.assume_init_mut() } - .resolver - .read_dir_info_ignore_error(joined_root) - else { - continue; - }; + for i in 0..dev.framework.file_system_router_types.len() { + let fsr = &dev.framework.file_system_router_types[i]; + let mut buf = paths::path_buffer_pool::get(); + let joined_root = paths::resolve_path::join_abs_string_buf::( + &dev.root, + &mut buf[..], + &[&fsr.root], + ); + let Some(entry) = dev + .server_transpiler + .resolver + .read_dir_info_ignore_error(joined_root) + else { + continue; + }; + let abs_root: Box<[u8]> = strings::without_trailing_slash(entry.abs_path).into(); - // SAFETY: `server_graph` is disjoint from `framework`. - let server_file = unsafe { &mut (*dev_ptr).server_graph }.insert_stale_extra( - &fsr.entry_server, - bake::Graph::Server, - incremental_graph::RouteKind::Route, - )?; + let server_file = dev.server_graph.insert_stale_extra( + &mut dev.assets, + &fsr.entry_server, + bake::Graph::Server, + incremental_graph::RouteKind::Route, + )?; + let client_file = match &fsr.entry_client { + Some(client) => Some(to_opaque_file_id::<{ bake::Side::Client }>( + dev.client_graph.insert_stale( + &mut dev.assets, + client, + bake::Graph::Client, + )?, + )), + None => None, + }; - types.push(framework_router::Type { - abs_root: strings::without_trailing_slash(entry.abs_path).into(), - ignore_underscores: fsr.ignore_underscores, - ignore_dirs: fsr - .ignore_dirs - .iter() - .map(|d| Box::<[u8]>::from(d.as_ref())) - .collect(), - extensions: fsr - .extensions - .iter() - .map(|e| Box::<[u8]>::from(e.as_ref())) - .collect(), - style: fsr.style.clone(), - allow_layouts: fsr.allow_layouts, - server_file: to_opaque_file_id::<{ bake::Side::Server }>(server_file), - client_file: if let Some(client) = &fsr.entry_client { - Some(to_opaque_file_id::<{ bake::Side::Client }>( - // SAFETY: `client_graph` is disjoint from `framework`. - unsafe { &mut (*dev_ptr).client_graph } - .insert_stale(client, bake::Graph::Client)?, - )) - } else { - None - }, - server_file_string: jsc::StrongOptional::empty(), - }); + types.push(framework_router::Type { + abs_root, + ignore_underscores: fsr.ignore_underscores, + ignore_dirs: fsr + .ignore_dirs + .iter() + .map(|d| Box::<[u8]>::from(d.as_ref())) + .collect(), + extensions: fsr + .extensions + .iter() + .map(|e| Box::<[u8]>::from(e.as_ref())) + .collect(), + style: fsr.style.clone(), + allow_layouts: fsr.allow_layouts, + server_file: to_opaque_file_id::<{ bake::Side::Server }>(server_file), + client_file, + server_file_string: jsc::StrongOptional::empty(), + }); - // SAFETY: `route_lookup` is disjoint from `framework`. - unsafe { &mut (*dev_ptr).route_lookup }.put( - server_file, - RouteIndexAndRecurseFlag::new( - framework_router::RouteIndex::init(u32::try_from(i).expect("int cast")), - true, - ), - )?; - } + dev.route_lookup.put( + server_file, + RouteIndexAndRecurseFlag::new( + framework_router::RouteIndex::init(u32::try_from(i).expect("int cast")), + true, + ), + )?; + } - break 'router FrameworkRouter::init_empty(&dev.root, types.into_boxed_slice())?; - }; + break 'router FrameworkRouter::init_empty(&dev.root, types.into_boxed_slice())?; + }; - // TODO: move scanning to be one tick after server startup. this way the - // line saying the server is ready shows quicker, and route errors show up - // after that line. - dev.scan_initial_routes()?; + // TODO: move scanning to be one tick after server startup. this way the + // line saying the server is ready shows quicker, and route errors show up + // after that line. + dev.scan_initial_routes()?; - debug_assert!(dev.magic == Magic::Valid); + debug_assert!(dev.magic == Magic::Valid); + Ok(()) + })?; - Ok(dev) + Ok(owned) } impl Drop for DevServer { fn drop(&mut self) { debug_log!("deinit"); - // For a usize test counter, wrapping at usize::MAX is unreachable in - // practice, so a plain fetch_add is fine. DEV_SERVER_DEINIT_COUNT_FOR_TESTING.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - // Exhaustiveness check: destructuring without `..` fails to compile when a field is added, - // removed, or renamed, forcing this Drop to be reviewed. All bindings - // are `_` so nothing is moved; cleanup not done explicitly below - // happens via the implicit field drops after this body returns. - { - let DevServer { - magic: _, - root: _, - inspector_server_id: _, - configuration_hash_key: _, - vm: _, - vm_handle: _, - server: _, - router: _, - route_bundles: _, - graph_safety_lock: _, - client_graph: _, - server_graph: _, - barrel_files_with_deferrals: _, - barrel_needed_exports: _, - incremental_result: _, - route_lookup: _, - html_router: _, - assets: _, - source_maps: _, - bundling_failures: _, - frontend_only: _, - has_tailwind_plugin_hack: _, - server_fetch_function_callback: _, - server_register_update_callback: _, - bun_watcher: _, - directory_watchers: _, - watcher_atomics: _, - testing_batch_events: _, - generation: _, - bundles_since_last_error: _, - framework: _, - bundler_framework_views: _, - bundler_options: _, - server_transpiler: _, - client_transpiler: _, - ssr_transpiler: _, - log: _, - plugin_state: _, - current_bundle: _, - next_bundle: _, - deferred_request_pool: _, - active_websocket_connections: _, - emit_incremental_visualizer_events: _, - emit_memory_visualizer_events: _, - memory_visualizer_timer: _, - assume_perfect_incremental_bundling: _, - broadcast_console_log_from_browser_to_server: _, - } = &*self; - } - - // WebSockets should be deinitialized before other parts. - // `websocket.close()` synchronously dispatches `HmrSocket.onClose`, - // which calls `dev.active_websocket_connections.remove(s)` and - // destroys the `HmrSocket`. Snapshot the keys first. - { - let count = self.active_websocket_connections.len(); - if count > 0 { - let sockets: Vec<*mut HmrSocket> = - self.active_websocket_connections.keys().copied().collect(); - for s in sockets { - // SAFETY: s is a valid HmrSocket ptr owned by the connection map - if let Some(websocket) = unsafe { (*s).underlying } { - websocket.close(); - } - } + // No route, socket, task or bundler callback may re-enter the cell + // from here on. + self.hot_reload.main.lock().dev = None; + + // WebSockets should be deinitialized before other parts. Each socket is + // detached from this dev server first (its `on_close`, dispatched + // synchronously by `close()`, then only releases the socket itself). + for socket in ::core::mem::take(&mut self.active_websocket_connections).values() { + socket.detach_from_dev_server(self); + if let Some(websocket) = socket.underlying.get() { + websocket.close(); } - debug_assert!(self.active_websocket_connections.is_empty()); + socket.deref(); } if self.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { - let timer_ptr: *mut EventLoopTimer = &raw mut self.memory_visualizer_timer; - self.timer_heap().remove(timer_ptr); + Self::timer_heap().remove(&raw mut self.memory_visualizer_timer); } self.graph_safety_lock.lock(); - // Hand ownership of the heap allocation to the watcher thread (which frees it in - // `thread_main` once `running` flips false). Auto-dropping the `Box` - // here would free the allocation out from under the still-running - // thread; on Windows the kernel additionally retains a pending - // `ReadDirectoryChangesW` against the inline 64 KiB `DirWatcher.buf` + - // `overlapped`, so the freed block being recycled by mimalloc for a - // later allocation is a kernel write into live unrelated heap data. - // SAFETY: `bun_watcher` was written exactly once in `init()` and is - // never taken elsewhere; this is `Drop`, so the field is not read again. - let watcher = unsafe { ::core::mem::ManuallyDrop::take(&mut self.bun_watcher) }; - // SAFETY: `Box::into_raw` yields the unique heap pointer; ownership - // transfers to `shutdown`, which reclaims or hands off to the thread. - unsafe { Watcher::shutdown(Box::into_raw(watcher), true) }; - - // The map's `Drop` runs `SerializedFailure::drop` for each value. + // Hand the watcher to the watcher thread (which frees it in `thread_main` + // once `running` flips false) or free it if the thread never started. + if let Some(watcher) = self.bun_watcher.take() { + watcher.shutdown_boxed(true); + } if self.current_bundle.is_some() { debug_assert!(false); // impossible to de-initialize this state correctly. } - { - let mut r = self.next_bundle.requests.first; - while !r.is_null() { - // SAFETY: `r` is a live intrusive-list node linked by `defer_request`; - // read the link before `deref_` may reclaim the node. - let next = unsafe { (*r).next }; - // SAFETY: `data` was initialized by `defer_request` before being - // linked; this exclusive borrow ends at `deref_` below. - let data = unsafe { (*r).data.assume_init_mut() }; - debug_assert!(!matches!(data.handler, Handler::ServerHandler(_))); - data.deref_(); - r = next; - } - self.next_bundle.promise.deinit_idempotently(); + for request in ::core::mem::take(&mut self.next_bundle.requests) { + debug_assert!(!matches!(*request.handler.get(), Handler::ServerHandler(_))); + DeferredRequest::release(request); } + self.next_bundle.promise.deinit_idempotently(); for value in self.source_maps.entries.values_mut() { debug_assert!(value.ref_count > 0); value.ref_count = 0; } if self.source_maps.weak_ref_sweep_timer.state == EventLoopTimerState::ACTIVE { - let timer_ptr: *mut EventLoopTimer = &raw mut self.source_maps.weak_ref_sweep_timer; - self.timer_heap().remove(timer_ptr); - } - - // `Watcher::shutdown` serialises with `dispatch_file_updates` on - // `Watcher.mutex`, so `next_event` is stable from the watcher side. - let atomics = self.watcher_atomics.as_ptr(); - // SAFETY: `atomics` is the `heap::into_raw` pointer from `init()`. - let next = unsafe { - (*atomics) - .next_event - .load(::core::sync::atomic::Ordering::Acquire) - }; - if next == crate::bake::dev_server::NextEvent::DONE.0 { - // SAFETY: `atomics` is the unique owner; no event is queued. - unsafe { - for event in &mut (*atomics).events { - event.dirs.clear_and_free(); - event.files.clear_and_free(); - event.extra_files.clear(); - } - bun_core::heap::destroy(atomics); - } - } else { - // A `HotReloadEvent.concurrent_task` is still linked in the - // concurrent queue; `HotReloadEvent::run` will see the nulled - // `owner` and reclaim this allocation. - // SAFETY: `atomics` is live; the main thread is the sole writer - // of `owner` here (watcher thread is serialised out). - unsafe { - for event in &mut (*atomics).events { - event.owner = ::core::ptr::null_mut(); - } - } + Self::timer_heap().remove(&raw mut self.source_maps.weak_ref_sweep_timer); } if let TestingBatchEvents::Enabled(batch) = &mut self.testing_batch_events { @@ -1158,33 +948,10 @@ impl Drop for DevServer { )); } - let separate_ssr_graph = self - .framework - .server_components - .as_ref() - .is_some_and(|sc| sc.separate_ssr_graph); - // SAFETY: each transpiler is initialized exactly once in `init()`. - unsafe { - self.server_transpiler.assume_init_drop(); - self.client_transpiler.assume_init_drop(); - if separate_ssr_graph { - self.ssr_transpiler.assume_init_drop(); - } - } - - for &ptr in &self.bundler_framework_views { - // SAFETY: each ptr is a unique arena slot written once in `init()`. - unsafe { ::core::ptr::drop_in_place(ptr) }; - } - self.bundler_framework_views.clear(); - debug_assert!(self.magic == Magic::Valid); - // self.magic = undefined — no Rust equivalent; freed memory. } } -// re-exports from memory_cost module already declared at top - impl DevServer { fn init_server_runtime(&mut self) { let runtime = BunString::create_static_external( @@ -1208,9 +975,6 @@ impl DevServer { ) { Ok(v) => v, Err(err) => { - // SAFETY: vm is JSC_BORROW — valid for DevServer lifetime; - // `print_error_like_object_to_console` needs `&mut VM`, so cast - // the shared borrow through a raw pointer (single-threaded JS). self.vm_mut() .print_error_like_object_to_console(global.take_exception(err)); panic!("Server runtime failed to start. The above error is always a bug in Bun"); @@ -1246,18 +1010,16 @@ impl DevServer { /// Deferred one tick so that the server can be up faster fn scan_initial_routes(&mut self) -> crate::Result<()> { - // Reborrow `self`, `self.router`, and `self.server_transpiler.resolver` - // via the raw self-ptr (the three are disjoint fields of the heap-stable `*self`). - let self_ptr = std::ptr::from_mut::(self); - // SAFETY: `router`, `server_transpiler.resolver`, and the - // `InsertionHandler` callbacks (touch `server_graph`/`route_lookup`) - // are disjoint fields of `*self_ptr`. - unsafe { - (*self_ptr).router.scan_all( - &mut (*(*self_ptr).server_transpiler.as_mut_ptr()).resolver, - framework_router::InsertionContext::wrap(&mut *self_ptr), - )?; - } + let mut ctx = RouterInsertionCtx { + server_graph: &mut self.server_graph, + assets: &mut self.assets, + route_lookup: &mut self.route_lookup, + root: &self.root, + }; + self.router.scan_all( + &mut self.server_transpiler.resolver, + framework_router::InsertionContext::wrap(&mut ctx), + )?; self.server_graph.ensure_stale_bit_capacity(true)?; self.client_graph.ensure_stale_bit_capacity(true)?; @@ -1270,66 +1032,57 @@ impl DevServer { server: &crate::server::NewServer, ) -> crate::Result { // TODO: all paths here must be prefixed with publicPath if set. - self.server = Some(AnyServer::from(server)); - let app = server.app_ptr().expect("server app is live"); - let dev = std::ptr::from_mut::(self).cast::(); - - // The ZST-fn-item trampoline pattern used by - // `server::server_body::AppRouteExt`: a generic `extern "C"` - // shim parameterized on the handler type `H` (which is zero-sized for - // a fn-item), so each `tramp::` lowers to a distinct C - // function pointer with the handler baked in. - macro_rules! route { - ($method:ident, $pattern:expr, $id:expr) => {{ - // SAFETY: `app` is set before `set_routes` (server init path) - // and outlives it; the borrow is statement-scoped. - unsafe { &mut *app }.$method($pattern, Some(dev_route_tramp::), dev); - }}; - } - - route!( - get, + debug_assert!(self.server == AnyServer::from(server)); + let this = self.this(); + let app = server.app_mut().expect("server app is live"); + use bun_http_types::Method::Method; + app.method_this( + Method::GET, const_format::concatcp!(CLIENT_PREFIX, "/:route").as_bytes(), - DevHandlerId::JsRequest + dev_route::<{ DevHandlerId::JsRequest }>, + this, ); - route!( - get, + app.method_this( + Method::GET, const_format::concatcp!(ASSET_PREFIX, "/:asset").as_bytes(), - DevHandlerId::AssetRequest + dev_route::<{ DevHandlerId::AssetRequest }>, + this, ); - route!( - get, + app.method_this( + Method::GET, const_format::concatcp!(INTERNAL_PREFIX, "/src/*").as_bytes(), - DevHandlerId::SrcRequest + dev_route::<{ DevHandlerId::SrcRequest }>, + this, ); - route!( - post, + app.method_this( + Method::POST, const_format::concatcp!(INTERNAL_PREFIX, "/report_error").as_bytes(), - DevHandlerId::ReportError + dev_route::<{ DevHandlerId::ReportError }>, + this, ); - route!( - post, + app.method_this( + Method::POST, const_format::concatcp!(INTERNAL_PREFIX, "/unref").as_bytes(), - DevHandlerId::UnrefSourceMap + dev_route::<{ DevHandlerId::UnrefSourceMap }>, + this, + ); + app.any_this( + INTERNAL_PREFIX.as_bytes(), + dev_route::<{ DevHandlerId::NotFound }>, + this, ); - route!(any, INTERNAL_PREFIX.as_bytes(), DevHandlerId::NotFound); - // SAFETY: see `route!` — statement-scoped reborrow of the live app; - // `dev` is this `DevServer`, the `hmr_socket_behavior` upgrade server, - // which outlives the route (routes are cleared before it drops). - unsafe { - (*app).ws( - const_format::concatcp!(INTERNAL_PREFIX, "/hmr").as_bytes(), - dev, - 0, - hmr_socket_behavior::(), - ) - }; + app.ws_this::( + const_format::concatcp!(INTERNAL_PREFIX, "/hmr").as_bytes(), + this, + 0, + &Default::default(), + ); // Only attach a catch-all handler if the framework has filesystem // router types. Otherwise, this can just be Bun.serve's default handler. if !self.framework.file_system_router_types.is_empty() { - route!(any, b"/*", DevHandlerId::Request); + app.any_this(b"/*", dev_route::<{ DevHandlerId::Request }>, this); Ok(true) } else { Ok(false) @@ -1337,7 +1090,7 @@ impl DevServer { } } -/// Handler dispatch for `dev_route_tramp`. A fn pointer cannot be a const +/// Handler dispatch for `dev_route`. A fn pointer cannot be a const /// generic, so use a `ConstParamTy` enum instead and /// `match` inside the trampoline (the optimizer folds the constant `match`). #[derive(Copy, Clone, Eq, PartialEq, ::core::marker::ConstParamTy)] @@ -1357,10 +1110,7 @@ pub(super) enum DevHandlerId { /// non-loopback / non-IP / non-configured hostnames prevents the attacker's /// page from reading bundled source via same-origin fetch. pub(crate) fn is_allowed_dev_host(dev: &DevServer, req: &Request) -> bool { - is_allowed_host_header( - req, - dev.server.as_ref().map(|server| &server.config().address), - ) + is_allowed_host_header(req, Some(&dev.server.config().address)) } pub(crate) fn is_allowed_host_header( @@ -1479,63 +1229,94 @@ fn origin_forbidden(resp: AnyResponse) { }); } -/// `extern "C"` trampoline: recovers `&mut DevServer` from user-data and wraps -/// the raw `uws_res` as `AnyResponse`, then calls the handler for `ID`. -#[allow( - clippy::undocumented_unsafe_blocks, - reason = "the SAFETY note above the match covers the eight identical call-scoped reborrows" -)] -extern "C" fn dev_route_tramp( - res: *mut bun_uws_sys::uws_res, - req: *mut bun_uws_sys::Request, - ud: *mut c_void, +/// Route handler registered with `App::method_this` / `any_this` for each +/// `/_bun/*` route (and the catch-all): applies the Host/Origin guards, then +/// runs the handler for `ID` on the dev server. +fn dev_route( + this: bun_ptr::ThisPtr, + req: bun_uws::AnyRequest, + resp: AnyResponse, ) { - // SAFETY: `ud`/`req`/`res` were registered by `set_routes` and outlive the - // route; uWS guarantees they are non-null in handler callbacks. `dev` is - // re-derived per statement so no `&mut DevServer` spans the guard reads. - let dev = ud.cast::(); - let req = req.cast::(); - let resp = if SSL { - AnyResponse::SSL(res.cast::()) - } else { - AnyResponse::TCP(res.cast::()) + let bun_uws::AnyRequest::H1(req) = req else { + return not_found(resp); }; - // SAFETY: uWS passes a non-null `Request*` valid for the callback; shared, - // call-scoped reborrow. - if !is_allowed_dev_host(unsafe { &*dev }, unsafe { &*req }) { + let req = bun_opaque::opaque_deref_mut(req); + if !is_allowed_dev_host(this.get().get(), req) { return host_forbidden(resp); } - // SAFETY: as above. if matches!(ID, DevHandlerId::ReportError | DevHandlerId::UnrefSourceMap) - && !is_allowed_dev_origin(unsafe { &*req }) + && !is_allowed_dev_origin(req) { return origin_forbidden(resp); } - // SAFETY: as above — the exclusive borrow is consumed by the single handler - // call; nothing else stores or re-derives this `Request` pointer. + let cell = this.get(); match ID { - DevHandlerId::JsRequest => on_js_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp), - DevHandlerId::AssetRequest => { - on_asset_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp) - } - DevHandlerId::SrcRequest => { - on_src_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp) - } - DevHandlerId::ReportError => { - on_report_error_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp) - } + DevHandlerId::JsRequest => cell.with_mut(|dev| on_js_request(dev, req, resp)), + DevHandlerId::AssetRequest => cell.with_mut(|dev| on_asset_request(dev, req, resp)), + DevHandlerId::SrcRequest => cell.with_mut(|dev| on_src_request(dev, req, resp)), + DevHandlerId::ReportError => cell.with_mut(|dev| on_report_error_request(dev, req, resp)), DevHandlerId::UnrefSourceMap => { - on_unref_source_map_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp) + cell.with_mut(|dev| on_unref_source_map_request(dev, req, resp)) } - DevHandlerId::NotFound => on_not_found(unsafe { &mut *dev }, unsafe { &mut *req }, resp), + DevHandlerId::NotFound => cell.with_mut(|dev| on_not_found(dev, req, resp)), DevHandlerId::Request => { // The catch-all route can reach a framework request handler; this - // trampoline folds what bundling for it left pending. - crate::dispatch::fold(on_request(unsafe { &mut *dev }, unsafe { &mut *req }, resp)); + // is where what bundling for it left pending is folded. The user's + // JS runs after the dev server borrow ends. + let server = cell.get().server; + let result = match cell.with_mut(|dev| on_request(dev, req, resp)) { + Ok(RequestAction::Done) => Ok(()), + Ok(RequestAction::UserHandler) => { + server.on_request(req, resp); + Ok(()) + } + Ok(RequestAction::Framework(call)) => { + call.run(server, SavedRequestUnion::Stack(req), resp); + Ok(()) + } + Err(err) => Err(err), + }; + crate::dispatch::fold(result); } } } +/// What the catch-all route handler does once the dev server borrow ends. +enum RequestAction { + Done, + /// Fall through to `Bun.serve`'s `fetch` handler. + UserHandler, + /// Call the framework's request handler. + Framework(FrameworkCall), +} + +/// A ready-to-run call into the framework's `handleRequest` (server JS). +pub(crate) struct FrameworkCall { + callback: JSValue, + args: FrameworkRequestArgs, +} + +impl FrameworkCall { + fn run(self, server: AnyServer, req: SavedRequestUnion<'_>, resp: AnyResponse) { + let args = self.args; + server.on_saved_request( + req, + resp, + self.callback, + [ + args.router_type_main, + args.route_modules, + args.client_id, + args.styles, + args.params, + args.set_async_local_storage, + args.bundle_new_route, + args.new_route_params, + ], + ); + } +} + fn on_report_error_request(dev: &mut DevServer, req: &mut Request, resp: AnyResponse) { use bun_uws_sys::thunk::OpaqueHandle as _; match resp { @@ -1562,52 +1343,10 @@ fn on_unref_source_map_request(dev: &mut DevServer, req: &mut Request, resp: Any } } -/// `WebSocketBehavior.Wrap(DevServer, HmrSocket, ssl).apply(.{})`. -fn hmr_socket_behavior() -> bun_uws_sys::WebSocketBehavior { - bun_uws_sys::web_socket::Wrap::::apply(&Default::default()) -} - -// `WebSocketBehavior.Wrap(ServerType, Type, ssl)` requires `Type` (= `HmrSocket`) -// to be a `WebSocketHandler` and `ServerType` (= `DevServer`) to be a -// `WebSocketUpgradeServer`. The trait is wired explicitly and forward to the inherent method bodies in -// `dev_server::hmr_socket`. -impl bun_uws_sys::web_socket::WebSocketHandler for HmrSocket { - // `Wrap.apply` leaves the drain/ping/pong C callbacks `null` when - // `HAS_ON_* == false`. - const HAS_ON_DRAIN: bool = false; - const HAS_ON_PING: bool = false; - const HAS_ON_PONG: bool = false; - - // Note (noalias re-entrancy): the trait now hands the handler a raw - // `*mut Self` (see `WebSocketHandler` doc). `HmrSocket::on_close` already - // takes `*mut Self`; `on_open`/`on_message` still take `&mut self` so - // re-derive a fresh `&mut *this` here (not carried in from a `noalias` - // dispatch-frame borrow). - #[inline] - unsafe fn on_open(this: *mut Self, ws: bun_uws_sys::AnyWebSocket) { - // SAFETY: `this` is the live user-data pointer (per trait contract). - HmrSocket::on_open(unsafe { &mut *this }, ws) - } - #[inline] - unsafe fn on_message( - this: *mut Self, - ws: bun_uws_sys::AnyWebSocket, - message: &[u8], - opcode: bun_uws_sys::Opcode, - ) { - // SAFETY: see `on_open`. - HmrSocket::on_message(unsafe { &mut *this }, ws, message, opcode) - } - #[inline] - unsafe fn on_close(this: *mut Self, ws: bun_uws_sys::AnyWebSocket, code: i32, message: &[u8]) { - // SAFETY: see `on_open`. - HmrSocket::on_close(this, ws, code, message) - } - unsafe fn on_drain(_this: *mut Self, _ws: bun_uws_sys::AnyWebSocket) {} - unsafe fn on_ping(_this: *mut Self, _ws: bun_uws_sys::AnyWebSocket, _message: &[u8]) {} - unsafe fn on_pong(_this: *mut Self, _ws: bun_uws_sys::AnyWebSocket, _message: &[u8]) {} -} -impl bun_uws_sys::web_socket::WebSocketUpgradeServer for DevServer { +// `App::ws_this::` requires `HmrSocket` to be a +// `WebSocketHandlerRef` (see `dev_server::hmr_socket`) and `DevServerCell` to +// be a `WebSocketUpgradeServer`. +impl bun_uws_sys::web_socket::WebSocketUpgradeServer for DevServerCell { fn on_websocket_upgrade( this: bun_ptr::ThisPtr, res: &mut bun_uws_sys::NewAppResponse, @@ -1616,16 +1355,7 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D id: usize, ) { debug_assert_eq!(id, 0); - let this: *mut Self = this.as_ptr(); - // Note: DevServer always registers `*mut Self` with `id == 0` - // (`set_routes` → `app.ws(prefix, this, 0, ..)`); live for the upgrade - // callback's duration. `res.upgrade(..)` synchronously runs `on_open` - // → `HmrSocket::dev()`, which materializes a second `&mut DevServer` - // from the socket's backref, so no `&mut DevServer` may span it — the - // pre-upgrade borrows below are scoped to their statements. - // - // SAFETY: `this` is the live DevServer registered for the upgrade callback. - if !is_allowed_dev_host(unsafe { &*this }, req) { + if !is_allowed_dev_host(this.get().get(), req) { host_forbidden(res.as_any_response()); return; } @@ -1633,13 +1363,15 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D origin_forbidden(res.as_any_response()); return; } - // SAFETY: as above; the borrow is statement-scoped, ending before `upgrade`. - let dw = bun_core::heap::into_raw(HmrSocket::new(unsafe { &mut *this })); - // SAFETY: `this` is live (see above). - let _ = unsafe { (*this).active_websocket_connections.insert(dw, ()) }; - // `on_open` (run synchronously inside `upgrade`) re-derives DevServer, not `res`. - let _ = res.upgrade( - dw, + let socket = bun_ptr::RefPtr::new(HmrSocket::new(bun_ptr::BackRef::new(this.get()))); + this.get().with_mut(|dev| { + dev.active_websocket_connections + .insert(socket.as_ptr() as usize, socket.dupe_ref()) + }); + // `res.upgrade_ref(..)` synchronously runs `HmrSocket::on_open`, which + // enters the dev server itself, so no `with_mut` may span it. + let _ = res.upgrade_ref( + socket, req.header(b"sec-websocket-key").unwrap_or(b""), req.header(b"sec-websocket-protocol").unwrap_or(b""), req.header(b"sec-websocket-extension").unwrap_or(b""), @@ -1708,29 +1440,21 @@ fn on_js_request(dev: &mut DevServer, req: &mut Request, resp: AnyResponse) { if is_map { let source_id = source_map_store::SourceId(id); - // Note: `render_json` needs `&mut DevServer` while `entry` borrows - // `dev.source_maps.entries[_]`. The entry is read-only (`&self`) and - // `render_json` does not touch `source_maps.entries`, so erase the - // entry borrow to a raw pointer and reborrow `dev` for the call. let Some(entry) = dev .source_maps .entries - .get_mut(&source_map_store::Key::init(id)) + .get(&source_map_store::Key::init(id)) else { return not_found(resp); }; - let entry_ptr: *const source_map_store::Entry = &raw const *entry; - // SAFETY: `entry_ptr` points into `dev.source_maps.entries` storage, - // which is not reallocated by `render_json`. - let json_bytes = - match unsafe { &*entry_ptr }.render_json(dev, source_id.kind(), bake::Side::Client) { - Ok(b) => b, - Err(e) => bun_core::handle_oom(Err(e)), - }; + let json_bytes = match entry.render_json(source_id.kind(), bake::Side::Client) { + Ok(b) => b, + Err(e) => bun_core::handle_oom(Err(e)), + }; let response = StaticRoute::init_from_any_blob( crate::webcore::blob::Any::from_array_list(json_bytes), crate::server::static_route::InitFromBytesOptions { - server: dev.server, + server: Some(dev.server), mime_type: Some(&MimeType::JSON), ..Default::default() }, @@ -1796,108 +1520,60 @@ fn on_src_request(_dev: &mut DevServer, req: &mut Request, resp: AnyResponse) { } struct RequestEnsureRouteBundledCtx { - // Note: erased to raw pointer — a `&mut DevServer` field would alias the - // caller's borrow. - dev: *mut DevServer, req: ReqOrSaved, resp: AnyResponse, kind: deferred_request::HandlerKind, route_bundle_index: route_bundle::Index, + /// Set by `on_loaded` for a framework route: the caller runs it once the + /// dev server borrow ends. + framework_call: Option, } -impl RequestEnsureRouteBundledCtx { - /// Reborrow the erased `dev` pointer. - /// # Safety - /// `self.dev` is set from a live `&mut DevServer` at ctx construction and - /// outlives the ctx (the ctx is stack-local in the request handler scope). - #[inline] - fn dev_mut(&mut self) -> &mut DevServer { - // SAFETY: `self.dev` was set from a live `&mut DevServer` at ctx - // construction and outlives this stack-local ctx. - unsafe { &mut *self.dev } - } - - fn on_defer(&mut self, bundle_field: BundleQueueType) -> JsResult<()> { - // Note: reshaped for borrowck — captured args before re-borrowing dev - let route_bundle_index = self.route_bundle_index; - let kind = self.kind; +impl EnsureRouteCtx for RequestEnsureRouteBundledCtx { + fn on_defer(&mut self, dev: &mut DevServer, bundle_field: BundleQueueType) -> JsResult<()> { let req = ::core::mem::replace(&mut self.req, ReqOrSaved::Aborted); - let resp = self.resp; - let dev = self.dev_mut(); - let requests_array: *mut deferred_request::List = match bundle_field { - BundleQueueType::CurrentBundle => { - &raw mut dev - .current_bundle - .as_mut() - .expect("infallible: bundle active") - .requests - } - BundleQueueType::NextBundle => &raw mut dev.next_bundle.requests, - }; - self.dev_mut().defer_request( - // SAFETY: `requests_array` points into `*self.dev`, which outlives this call. - unsafe { &mut *requests_array }, - route_bundle_index, - kind, + dev.defer_request( + bundle_field, + self.route_bundle_index, + self.kind, req, - resp, + self.resp, )?; Ok(()) } - fn on_loaded(&mut self) -> JsResult<()> { + fn on_loaded(&mut self, dev: &mut DevServer) -> JsResult<()> { match self.kind { deferred_request::HandlerKind::ServerHandler => { - let route_bundle_index = self.route_bundle_index; - let resp = self.resp; - // The `Strong` field is move-only. Take ownership out of `self.req` - // (it is consumed by `on_framework_request_with_bundle`). - let req = match ::core::mem::replace(&mut self.req, ReqOrSaved::Aborted) { - ReqOrSaved::Req(r) => { - // SAFETY: `r` is a uWS `Request*` valid for the handler callback's duration. - SavedRequestUnion::Stack(unsafe { &mut *r }) - } - ReqOrSaved::Aborted => unreachable!(), + let ReqOrSaved::Req(r) = self.req else { + unreachable!() }; - self.dev_mut() - .on_framework_request_with_bundle(route_bundle_index, req, resp) + let url = bun_opaque::opaque_deref_mut(r).url(); + self.framework_call = + Some(dev.prepare_framework_request(self.route_bundle_index, url)?); + Ok(()) } deferred_request::HandlerKind::BundledHtmlPage => { - let route_bundle_index = self.route_bundle_index; - let resp = self.resp; - let method = self.req.method(); - self.dev_mut() - .on_html_request_with_bundle(route_bundle_index, resp, method); + dev.on_html_request_with_bundle( + self.route_bundle_index, + self.resp, + self.req.method(), + ); Ok(()) } } } - fn on_plugin_error(&mut self) -> JsResult<()> { + fn on_plugin_error(&mut self, _dev: &mut DevServer) -> JsResult<()> { self.resp.end(b"Plugin Error", false); Ok(()) } - fn to_dev_response(&mut self) -> DevResponse<'_> { + fn to_dev_response(&mut self, _dev: &mut DevServer) -> DevResponse<'_> { DevResponse::Http(self.resp) } } -impl EnsureRouteCtx for RequestEnsureRouteBundledCtx { - fn on_defer(&mut self, b: BundleQueueType) -> JsResult<()> { - Self::on_defer(self, b) - } - fn on_loaded(&mut self) -> JsResult<()> { - Self::on_loaded(self) - } - fn on_plugin_error(&mut self) -> JsResult<()> { - Self::on_plugin_error(self) - } - fn to_dev_response(&mut self) -> DevResponse<'_> { - Self::to_dev_response(self) - } -} - #[derive(Copy, Clone)] enum BundleQueueType { NextBundle, @@ -1905,10 +1581,10 @@ enum BundleQueueType { } trait EnsureRouteCtx { - fn on_defer(&mut self, bundle_field: BundleQueueType) -> JsResult<()>; - fn on_loaded(&mut self) -> JsResult<()>; - fn on_plugin_error(&mut self) -> JsResult<()>; - fn to_dev_response(&mut self) -> DevResponse<'_>; + fn on_defer(&mut self, dev: &mut DevServer, bundle_field: BundleQueueType) -> JsResult<()>; + fn on_loaded(&mut self, dev: &mut DevServer) -> JsResult<()>; + fn on_plugin_error(&mut self, dev: &mut DevServer) -> JsResult<()>; + fn to_dev_response(&mut self, dev: &mut DevServer) -> DevResponse<'_>; } fn ensure_route_is_bundled( @@ -1917,7 +1593,6 @@ fn ensure_route_is_bundled( ctx: &mut Ctx, ) -> JsResult<()> { debug_assert!(dev.magic == Magic::Valid); - debug_assert!(dev.server.is_some()); let mut state = dev.route_bundle_ptr(route_bundle_index).server_state; 'sw: loop { match state { @@ -1925,7 +1600,7 @@ fn ensure_route_is_bundled( // We already are bundling something, defer the request if dev.current_bundle.is_some() { dev.next_bundle.route_queue.put(route_bundle_index, ())?; - ctx.on_defer(BundleQueueType::NextBundle)?; + ctx.on_defer(dev, BundleQueueType::NextBundle)?; dev.route_bundle_ptr(route_bundle_index).server_state = route_bundle::State::DeferredToNextBundle; return Ok(()); @@ -1942,9 +1617,6 @@ fn ensure_route_is_bundled( dev.plugin_state = PluginState::Loaded; } else { // TODO: implement a proper solution here - // `vm()` is the centralized JSC_BORROW accessor (one - // `unsafe` site for the field); the borrow is dropped - // before the assignment so no overlap with `&mut dev`. let has_tailwind = if let Some(serve_plugins) = &dev.vm().transpiler.options.serve_plugins { @@ -1957,11 +1629,8 @@ fn ensure_route_is_bundled( }; dev.has_tailwind_plugin_hack = has_tailwind; - let load_result: crate::server::GetOrStartLoadResult = dev - .server - .as_ref() - .expect("infallible: server bound") - .get_or_load_plugins( + let load_result: crate::server::GetOrStartLoadResult = + dev.server.get_or_load_plugins( crate::server::ServePluginsCallback::DevServer, ); match load_result { @@ -1986,14 +1655,14 @@ fn ensure_route_is_bundled( } PluginState::Pending => { dev.next_bundle.route_queue.put(route_bundle_index, ())?; - ctx.on_defer(BundleQueueType::NextBundle)?; + ctx.on_defer(dev, BundleQueueType::NextBundle)?; dev.route_bundle_ptr(route_bundle_index).server_state = route_bundle::State::DeferredToNextBundle; return Ok(()); } PluginState::Err => { // TODO: render plugin error page - ctx.on_plugin_error()?; + ctx.on_plugin_error(dev)?; return Ok(()); } PluginState::Loaded => break 'plugin, @@ -2022,7 +1691,7 @@ fn ensure_route_is_bundled( } dev.next_bundle.route_queue.put(route_bundle_index, ())?; - ctx.on_defer(BundleQueueType::NextBundle)?; + ctx.on_defer(dev, BundleQueueType::NextBundle)?; dev.route_bundle_ptr(route_bundle_index).server_state = route_bundle::State::Bundling; @@ -2037,19 +1706,20 @@ fn ensure_route_is_bundled( .get(&route_bundle_index) .is_some() ); - ctx.on_defer(BundleQueueType::NextBundle)?; + ctx.on_defer(dev, BundleQueueType::NextBundle)?; return Ok(()); } route_bundle::State::Bundling => { debug_assert!(dev.current_bundle.is_some()); - ctx.on_defer(BundleQueueType::CurrentBundle)?; + ctx.on_defer(dev, BundleQueueType::CurrentBundle)?; return Ok(()); } route_bundle::State::PossibleBundlingFailures => { if !dev.bundling_failures.is_empty() { // Trace the graph to see if there are any failures that are // reachable by this route. - match check_route_failures(dev, route_bundle_index, ctx.to_dev_response())? { + let resp = ctx.to_dev_response(dev); + match check_route_failures(dev, route_bundle_index, resp)? { CheckResult::Stop => return Ok(()), CheckResult::Ok => {} // Errors were cleared or not in the way. CheckResult::Rebuild => { @@ -2064,7 +1734,7 @@ fn ensure_route_is_bundled( continue 'sw; } route_bundle::State::Loaded => { - ctx.on_loaded()?; + ctx.on_loaded(dev)?; return Ok(()); } } @@ -2081,8 +1751,7 @@ impl ReqOrSaved { fn method(&self) -> Method { match self { ReqOrSaved::Req(req) => { - // SAFETY: `req` is a uWS `Request*` valid for the handler callback's duration. - Method::which(unsafe { &**req }.method()).unwrap_or(Method::POST) + Method::which(bun_opaque::opaque_deref_mut(*req).method()).unwrap_or(Method::POST) } ReqOrSaved::Aborted => unreachable!(), } @@ -2092,116 +1761,82 @@ impl ReqOrSaved { impl DevServer { fn defer_request( &mut self, - requests_array: &mut deferred_request::List, + bundle_field: BundleQueueType, route_bundle_index: route_bundle::Index, kind: deferred_request::HandlerKind, req: ReqOrSaved, resp: AnyResponse, ) -> crate::Result<()> { - let deferred_slot = self.deferred_request_pool.claim(); - let deferred_node_ptr: *mut deferred_request::Node = deferred_slot.addr().as_ptr(); - // Precompute the data slot pointer (used inside the initializer for - // abort-callback registration) without forming a reference to the - // still-uninitialized slot. - // SAFETY: `deferred_node_ptr` is a valid (allocated, aligned) hive-pool - // slot; `addr_of_mut!` projects to the field without a reference, so - // the uninit `data` is never read. - let deferred_data_ptr: *mut c_void = - unsafe { ::core::ptr::addr_of_mut!((*deferred_node_ptr).data) }.cast::(); - debug_log!("DeferredRequest(0x{:x}).init", deferred_data_ptr as usize); - let method = match &req { - // SAFETY: r is a uws Request ptr valid for the duration of the handler callback - ReqOrSaved::Req(r) => Method::which(unsafe { &**r }.method()).unwrap_or(Method::GET), + ReqOrSaved::Req(r) => { + Method::which(bun_opaque::opaque_deref_mut(*r).method()).unwrap_or(Method::GET) + } _ => unreachable!(), }; - let mut deferred_data = DeferredRequest { - route_bundle_index, - dev: std::ptr::from_ref(self), - referenced_by_devserver: true, - weakly_referenced_by_requestcontext: false, - handler: match kind { - deferred_request::HandlerKind::BundledHtmlPage => 'brk: { - // Note: `on_aborted` rejects `DeferredRequest`; - // erase to `c_void` and cast back inside the trampoline. - resp.on_aborted( - |p: *mut c_void, r: AnyResponse| { - // SAFETY: p is the &mut deferred.data registered below; lifetime erased - unsafe { &mut *p.cast::() }.on_abort(r) - }, - deferred_data_ptr, - ); - break 'brk Handler::BundledHtmlPage(ResponseAndMethod { - response: resp, - method, - }); - } - deferred_request::HandlerKind::ServerHandler => 'brk: { - let server_handler: SavedRequest = match req { - ReqOrSaved::Req(r) => { - let global = self.vm().global(); - match self - .server - .as_ref() - .unwrap() - .prepare_and_save_js_request_context( - // SAFETY: r is the live µWS request for this handler frame. - unsafe { &mut *r }, - resp, - global, - Some(method), - )? { - Some(saved) => saved, - // Abort the deferral on failure. - // `deferred_slot` drops here, releasing the - // still-uninitialized slot without `drop_in_place`. - None => return Ok(()), - } - } - _ => unreachable!(), - }; - server_handler.ctx.ref_(); - server_handler.ctx.set_additional_on_abort_callback(Some( - crate::server::any_request_context::AdditionalOnAbortCallback { - cb: { - fn cb(ptr: *mut c_void) { - DeferredRequest::on_abort_wrapper(ptr) - } - cb - }, - // SAFETY: deferred.data is a live field of a HiveArray-owned node - data: unsafe { ::core::ptr::NonNull::new_unchecked(deferred_data_ptr) }, - deref_fn: { - fn deref_fn(ptr: *mut c_void) { - // SAFETY: ptr is &mut DeferredRequest from above - let self_: &mut DeferredRequest = - unsafe { &mut *ptr.cast::() }; - self_.weak_deref(); - } - deref_fn - }, - }, - )); - break 'brk Handler::ServerHandler(server_handler); + let handler = match kind { + deferred_request::HandlerKind::BundledHtmlPage => { + Handler::BundledHtmlPage(ResponseAndMethod { + response: resp, + method, + }) + } + deferred_request::HandlerKind::ServerHandler => { + let ReqOrSaved::Req(r) = req else { + unreachable!() + }; + let global = self.vm().global(); + match self.server.prepare_and_save_js_request_context( + bun_opaque::opaque_deref_mut(r), + resp, + global, + Some(method), + )? { + Some(saved) => Handler::ServerHandler(saved), + // Abort the deferral on failure. + None => return Ok(()), } - }, + } + }; + let deferred = bun_ptr::RefPtr::new(DeferredRequest { + ref_count: Cell::new(1), + route_bundle_index, + handled: Cell::new(false), + handler: JsCell::new(handler), + }); + debug_log!("DeferredRequest(0x{:x}).init", deferred.as_ptr() as usize); + + match deferred.handler.get() { + Handler::BundledHtmlPage(_) => { + // Cleared by whatever answers the response (`StaticRoute`, + // `abort()`), which happens before the lists release their ref. + resp.on_aborted_this( + |this: bun_ptr::ThisPtr, _: AnyResponse| this.on_abort(), + deferred.this_ptr(), + ); + } + Handler::ServerHandler(saved) => { + saved.ctx.ref_(); + saved.ctx.set_additional_on_abort_callback(Some( + crate::server::any_request_context::AdditionalOnAbortCallback( + deferred.dupe_ref(), + ), + )); + } + Handler::Aborted => unreachable!(), + } + + let requests = match bundle_field { + BundleQueueType::CurrentBundle => { + &mut self + .current_bundle + .as_mut() + .expect("infallible: bundle active") + .requests + } + BundleQueueType::NextBundle => &mut self.next_bundle.requests, }; - - if matches!(deferred_data.handler, Handler::ServerHandler(_)) { - deferred_data.weak_ref(); - } - - // `next` is a placeholder: `prepend` overwrites it before any read. - let deferred_ptr = deferred_slot - .write(deferred_request::Node { - next: ::core::ptr::null_mut(), - data: ::core::mem::MaybeUninit::new(deferred_data), - }) - .as_ptr(); - - // SAFETY: `deferred_ptr` was just initialized via `deferred_slot.write()`. - unsafe { requests_array.prepend(&mut *deferred_ptr) }; + requests.push(deferred); Ok(()) } } @@ -2218,24 +1853,12 @@ fn check_route_failures( resp: DevResponse, ) -> crate::Result { let mut gts = dev.init_graph_trace_state(0)?; - // Note: erase to a raw pointer so the deferred cleanup only fires on - // scope exit when no other borrow of `dev` is live. - let dev_ptr = std::ptr::from_mut::(dev); - scopeguard::defer! { - // SAFETY: see Note above. - unsafe { (*dev_ptr).incremental_result.failures_added.clear() } - }; + let mut dev = scopeguard::guard(dev, |dev| { + dev.incremental_result.failures_added.clear(); + }); let _lock_guard = dev.graph_safety_lock.guard(); - let route_bundle = std::ptr::from_mut::(dev.route_bundle_ptr(route_bundle_index)); - // SAFETY: `trace_all_route_imports` reads `route_bundle.data` but never - // mutates `route_bundles`; the raw-pointer reborrow sidesteps the - // overlapping `&mut self`. - dev.trace_all_route_imports( - // SAFETY: see above; `route_bundle` points into `dev.route_bundles`, not resized here. - unsafe { &*route_bundle }, - &mut gts, - TraceImportGoal::FindErrors, - )?; + let key = dev.route_bundles[route_bundle_index.get() as usize].trace_key(); + dev.trace_all_route_imports(key, &mut gts, TraceImportGoal::FindErrors)?; if !dev.incremental_result.failures_added.is_empty() { // See comment on this field for information if !dev.assume_perfect_incremental_bundling { @@ -2255,11 +1878,7 @@ fn check_route_failures( return Ok(CheckResult::Rebuild); } - // SAFETY: `send_serialized_failures` does not mutate - // `incremental_result.failures_added`; reborrow through raw ptr to - // satisfy borrowck. - let failures = unsafe { &(*dev_ptr).incremental_result.failures_added }; - dev.send_serialized_failures(resp, failures, None)?; + dev.send_serialized_failures(resp, &dev.incremental_result.failures_added, None)?; Ok(CheckResult::Stop) } else { // Failures are unreachable by this route, so it is OK to load. @@ -2277,8 +1896,6 @@ impl DevServer { let client_file_names = self.client_graph.bundled_files.keys(); // Build a list of all files that have not yet been bundled. - // Note: split borrow — `route_bundle_ptr`/`type_ptr` are `&mut self`; - // index `route_bundles`/`types` immutably so the SoA key slices above stay live. match &self.route_bundles[rbi.get() as usize].data { route_bundle::Data::Framework(bundle) => { let mut route = self.router.route_ptr(bundle.route_index); @@ -2352,113 +1969,82 @@ struct FrameworkRequestArgs { } impl DevServer { - /// Note: raw-pointer receiver. A previous version of this body bound - /// long-lived `&mut RouteBundle` / `&mut Type` from - /// `&mut *self_ptr` and then reborrowed `&mut *self_ptr` again — overlapping - /// `&mut` UB. Per docs/PORTING.md §Global mutable state we instead stay in - /// raw-ptr land: hold `*mut Self` / `*mut Type` and deref per-access via - /// place projection (`(*this).field`), never materializing a whole-struct - /// `&mut DevServer` while a sub-borrow is live. - /// - /// SAFETY: `this` must point to a live `DevServer`; `framework_bundle` must - /// point into `(*this).route_bundles[route_bundle_index].data`. Neither - /// `route_bundles` nor `router.types` is reallocated during this call. - // `&(...)` is deliberate — sidesteps dangerous_implicit_autorefs. - #[allow(clippy::needless_borrow)] - unsafe fn compute_arguments_for_framework_request( - this: *mut Self, + /// The `Framework` payload of `route_bundles[rbi]`. + fn framework_bundle_mut(&mut self, rbi: route_bundle::Index) -> &mut route_bundle::Framework { + match &mut self.route_bundles[rbi.get() as usize].data { + route_bundle::Data::Framework(f) => f, + route_bundle::Data::Html(_) => unreachable!("expected .framework"), + } + } + + fn compute_arguments_for_framework_request( + &mut self, route_bundle_index: route_bundle::Index, - framework_bundle: &mut route_bundle::Framework, params_js_value: JSValue, first_request: bool, ) -> JsResult { - // SAFETY: `this` is live; `vm` is a `BackRef` (safe Deref); `vm.global` - // is valid for VM lifetime. - let global = unsafe { &*(&(*this).vm).global }; - // SAFETY: place projections off `*this` — `router` / `server_graph` / - // `route_bundles` are disjoint fields; each reborrow is scoped to its - // expression so no two `&mut` overlap. `framework_bundle` lives in - // `route_bundles[_].data`, disjoint from every other field touched here. - let route_type_idx = unsafe { - (*this) - .router - .route_ptr(framework_bundle.route_index) - .r#type - }; - // SAFETY: held raw; deref per-access. `router.types` is a `Box<[Type]>` — - // never reallocated for the lifetime of `DevServer`. - let router_type: *mut framework_router::Type = - unsafe { (*this).router.type_ptr(route_type_idx) }; - // SAFETY: scalar copy — `route_bundles[i]` is otherwise only read (via - // `generate_css_js_array`, which takes `&RouteBundle`). - let client_script_generation: u32 = unsafe { - (&(*this).route_bundles)[route_bundle_index.get() as usize].client_script_generation + let global = self.global(); + let (route_index, cached_module_list, cached_client_bundle_url, cached_css_file_array) = { + let fw = self.framework_bundle_mut(route_bundle_index); + ( + fw.route_index, + fw.cached_module_list.get(), + fw.cached_client_bundle_url.get(), + fw.cached_css_file_array.get(), + ) }; + let route_type_idx = self.router.route_ptr(route_index).r#type; + let client_script_generation: u32 = + self.route_bundles[route_bundle_index.get() as usize].client_script_generation; Ok(FrameworkRequestArgs { // routerTypeMain - // SAFETY: `router_type` points into `router.types`, never reallocated for `DevServer`'s lifetime. - router_type_main: match unsafe { (*router_type).server_file_string.get() } { + router_type_main: match self + .router + .type_ptr_const(route_type_idx) + .server_file_string + .get() + { Some(s) => s, None => 'str: { - // SAFETY: `server_graph` is disjoint from `router` / `route_bundles`. - let name = unsafe { - &(*this).server_graph.bundled_files.keys()[from_opaque_file_id::< - { bake::Side::Server }, - >( - (*router_type).server_file - ) - .get() - as usize] - }; + let server_file = self.router.type_ptr_const(route_type_idx).server_file; + let name = &self.server_graph.bundled_files.keys() + [from_opaque_file_id::<{ bake::Side::Server }>(server_file).get() as usize]; let mut buf = paths::path_buffer_pool::get(); let s = bun_string_jsc::create_utf8_for_js( global, - // SAFETY: `relative_path(&self)` only reads `self.root`; - // no `&mut` derived from `*this` is live across this call. - unsafe { (*this).relative_path(&mut *buf, name) }, + self.relative_path(&mut buf, name), )?; - // SAFETY: per-access raw deref; `router.types` not reallocated. - unsafe { - (*router_type).server_file_string = jsc::StrongOptional::create(s, global) - }; + self.router.type_ptr(route_type_idx).server_file_string = + jsc::StrongOptional::create(s, global); break 'str s; } }, // routeModules - route_modules: match framework_bundle.cached_module_list.get() { + route_modules: match cached_module_list { Some(a) => a, None => 'arr: { - // SAFETY: `server_graph` / `router` are disjoint from - // `route_bundles`; `route` is a `&Route` reborrowed per step. - let keys = unsafe { (*this).server_graph.bundled_files.keys() }; + let keys = self.server_graph.bundled_files.keys(); let mut n: usize = 1; - // SAFETY: `this` is live; `router.routes` is not reallocated during this fn. - let mut route = - unsafe { (*this).router.route_ptr(framework_bundle.route_index) }; + let mut route = self.router.route_ptr(route_index); loop { if route.file_layout.is_some() { n += 1; } let Some(p) = route.parent else { break }; - // SAFETY: `this` is live; `router.routes` is not reallocated during this fn. - route = unsafe { (*this).router.route_ptr(p) }; + route = self.router.route_ptr(p); } let arr = JSValue::create_empty_array(global, n)?; - // SAFETY: `this` is live; `router.routes` is not reallocated during this fn. - route = unsafe { (*this).router.route_ptr(framework_bundle.route_index) }; + route = self.router.route_ptr(route_index); { let mut buf = paths::path_buffer_pool::get(); - // SAFETY: `relative_path(&self)` only reads `self.root`; no `&mut *this` is live. - let route_name = unsafe { - (*this).relative_path( - &mut *buf, - &keys[from_opaque_file_id::<{ bake::Side::Server }>( - route.file_page.unwrap(), - ) - .get() as usize], + let route_name = self.relative_path( + &mut buf, + &keys[from_opaque_file_id::<{ bake::Side::Server }>( + route.file_page.unwrap(), ) - }; + .get() as usize], + ); arr.put_index( global, 0, @@ -2469,14 +2055,11 @@ impl DevServer { loop { if let Some(layout) = route.file_layout { let mut buf = paths::path_buffer_pool::get(); - // SAFETY: `relative_path(&self)` only reads `self.root`; no `&mut *this` is live. - let layout_name = unsafe { - (*this).relative_path( - &mut *buf, - &keys[from_opaque_file_id::<{ bake::Side::Server }>(layout) - .get() as usize], - ) - }; + let layout_name = self.relative_path( + &mut buf, + &keys[from_opaque_file_id::<{ bake::Side::Server }>(layout).get() + as usize], + ); arr.put_index( global, u32::try_from(n).expect("int cast"), @@ -2485,15 +2068,15 @@ impl DevServer { n += 1; } let Some(p) = route.parent else { break }; - // SAFETY: `this` is live; `router.routes` is not reallocated during this fn. - route = unsafe { (*this).router.route_ptr(p) }; + route = self.router.route_ptr(p); } - framework_bundle.cached_module_list = jsc::StrongOptional::create(arr, global); + self.framework_bundle_mut(route_bundle_index) + .cached_module_list = jsc::StrongOptional::create(arr, global); break 'arr arr; } }, // clientId - client_id: match framework_bundle.cached_client_bundle_url.get() { + client_id: match cached_client_bundle_url { Some(s) => s, None => 'str: { let bundle_index: u32 = route_bundle_index.get(); @@ -2503,34 +2086,23 @@ impl DevServer { let mut hex = [0u8; 16]; bun_core::fmt::bytes_to_hex_lower(&bundle_index.to_ne_bytes(), &mut hex[..8]); bun_core::fmt::bytes_to_hex_lower(&generation.to_ne_bytes(), &mut hex[8..]); - // SAFETY: `bytes_to_hex_lower` writes ASCII [0-9a-f] only. - let hex_str = unsafe { ::core::str::from_utf8_unchecked(&hex) }; let js = BunString::create_format(format_args!( - "{CLIENT_PREFIX}/route-{hex_str}.js", + "{CLIENT_PREFIX}/route-{}.js", + bstr::BStr::new(&hex), )) .into_js(global)?; - framework_bundle.cached_client_bundle_url = - jsc::StrongOptional::create(js, global); + self.framework_bundle_mut(route_bundle_index) + .cached_client_bundle_url = jsc::StrongOptional::create(js, global); break 'str js; } }, // styles - styles: match framework_bundle.cached_css_file_array.get() { + styles: match cached_css_file_array { Some(a) => a, None => 'arr: { - // SAFETY: `generate_css_js_array` reads `client_graph` / - // `router` and the `&RouteBundle`; it never reallocates - // `route_bundles`. The `&mut *this` whole-struct reborrow - // here is the *only* one in this fn — no other `&`/`&mut` - // derived from `*this` is live across it (`router_type` / - // `keys` / `route` were all consumed in earlier arms). - let js = unsafe { - (*this).generate_css_js_array( - &(&(*this).route_bundles)[route_bundle_index.get() as usize], - ) - }?; - framework_bundle.cached_css_file_array = - jsc::StrongOptional::create(js, global); + let js = self.generate_css_js_array(route_bundle_index)?; + self.framework_bundle_mut(route_bundle_index) + .cached_css_file_array = jsc::StrongOptional::create(js, global); break 'arr js; } }, @@ -2556,49 +2128,31 @@ impl DevServer { }) } - fn on_framework_request_with_bundle( + /// Compute everything the framework's `handleRequest` needs for the route + /// at `route_bundle_index`; the caller makes the call ([`FrameworkCall::run`]). + fn prepare_framework_request( &mut self, route_bundle_index: route_bundle::Index, - req: SavedRequestUnion, - resp: AnyResponse, - ) -> JsResult<()> { - // Note: erase the `&mut RouteBundle` to a raw pointer so subsequent - // `&self` accesses (vm/router/callback) don't trip borrowck. - let route_bundle: *mut RouteBundle = self.route_bundle_ptr(route_bundle_index); + url: &[u8], + ) -> JsResult { debug_assert!(matches!( - // SAFETY: `route_bundle` points into `self.route_bundles`, not resized in this fn. - unsafe { &(*route_bundle).data }, + self.route_bundles[route_bundle_index.get() as usize].data, route_bundle::Data::Framework(_) )); - // SAFETY: `route_bundle` points into `self.route_bundles`, which is not - // resized or dropped for the duration of this fn. - let framework_bundle = match unsafe { &mut (*route_bundle).data } { - route_bundle::Data::Framework(f) => f, - _ => unreachable!(), - }; - // Extract route params by re-matching the URL + let mut params: framework_router::MatchedParams = Default::default(); + + // Extract pathname from URL (remove protocol, host, query, hash) + let pathname = extract_pathname_from_url(url); + + // Create params JSValue // TODO: lazy structure caching since we are making these objects a lot let global = self.vm().global(); - let params_js_value = { - let mut params: framework_router::MatchedParams = Default::default(); - let url_bunstr = match &req { - // SAFETY: r is a uws Request ptr valid for the duration of the handler callback - SavedRequestUnion::Stack(r) => bun_core::StringView::borrow_utf8((**r).url()), - SavedRequestUnion::Saved(data) => match data.request() { - Some(r) => bun_core::StringView::new(r.url.get()), - None => bun_core::StringView::EMPTY, - }, - }; - let url = url_bunstr.to_utf8(); - // Extract pathname from URL (remove protocol, host, query, hash) - let pathname = extract_pathname_from_url(url.slice()); - if self.router.match_slow(pathname, &mut params).is_some() { - params.to_js(global) - } else { - JSValue::NULL - } + let params_js_value = if self.router.match_slow(pathname, &mut params).is_some() { + params.to_js(global) + } else { + JSValue::NULL }; let server_request_callback = self @@ -2606,38 +2160,16 @@ impl DevServer { .get() .expect("did not initialize server code"); - // SAFETY: `self` is live; `framework_bundle` points into - // `self.route_bundles[route_bundle_index].data`. Raw-ptr receiver — see - // Note on `compute_arguments_for_framework_request`. - let args = unsafe { - Self::compute_arguments_for_framework_request( - self, - route_bundle_index, - framework_bundle, - params_js_value, - true, - ) - }?; + let args = self.compute_arguments_for_framework_request( + route_bundle_index, + params_js_value, + true, + )?; - self.server - .as_ref() - .expect("infallible: server bound") - .on_saved_request( - req, - resp, - server_request_callback, - [ - args.router_type_main, - args.route_modules, - args.client_id, - args.styles, - args.params, - args.set_async_local_storage, - args.bundle_new_route, - args.new_route_params, - ], - ); - Ok(()) + Ok(FrameworkCall { + callback: server_request_callback, + args, + }) } fn on_html_request_with_bundle( @@ -2646,48 +2178,36 @@ impl DevServer { resp: AnyResponse, method: Method, ) { - // Note: erase `self` to a raw pointer so the `route_bundle` borrow - // doesn't conflict with the `&mut self` calls below. Per docs/PORTING.md §Global mutable state: hold - // `*mut T` and deref per-access; do not bind a long-lived `&mut`. - let self_ptr = std::ptr::from_mut::(self); - // SAFETY: `route_bundles` is not reallocated for the duration of this fn. - let route_bundle: *mut RouteBundle = - &raw mut unsafe { &mut *self_ptr }.route_bundles[route_bundle_index.get() as usize]; debug_assert!(matches!( - // SAFETY: `route_bundle` points into `self.route_bundles`, not resized in this fn. - unsafe { &(*route_bundle).data }, + self.route_bundles[route_bundle_index.get() as usize].data, route_bundle::Data::Html(_) )); - // SAFETY: `route_bundle` points into `self.route_bundles`, not resized in - // this fn; the shared reborrow ends with this statement. - let cached: Option> = - unsafe { (*route_bundle).data.html().cached_response.as_ref() } - .map(bun_ptr::RefPtr::this_ptr); + let cached: Option> = self.route_bundles + [route_bundle_index.get() as usize] + .data + .html() + .cached_response + .as_ref() + .map(bun_ptr::RefPtr::this_ptr); let blob: bun_ptr::ThisPtr = match cached { Some(blob) => blob, None => 'generate: { - // SAFETY: `generate_html_payload` reads `route_bundle.data` / - // `client_graph` and never reallocates `route_bundles`. No - // `&mut` into `*route_bundle` is live across this call; both - // reborrows end when it returns. - let payload = unsafe { - Self::generate_html_payload(&mut *self_ptr, route_bundle_index, &*route_bundle) - } - .expect("oom"); + let payload = self.generate_html_payload(route_bundle_index).expect("oom"); let response = StaticRoute::init_from_any_blob( crate::webcore::AnyBlob::from_owned_slice(payload), crate::server::static_route::InitFromBytesOptions { mime_type: Some(&MimeType::HTML), - // SAFETY: `self_ptr` is `&mut self` erased; live for this fn body. - server: unsafe { &*self_ptr }.server, + server: Some(self.server), ..Default::default() }, ); let blob = response.this_ptr(); - // SAFETY: per-access reborrow; no other `&` into `*route_bundle` live. - unsafe { (*route_bundle).data.html_mut().cached_response = Some(response) }; + self.route_bundles[route_bundle_index.get() as usize] + .data + .html_mut() + .cached_response = Some(response); break 'generate blob; } }; @@ -2714,35 +2234,66 @@ impl DevServer { fn generate_html_payload( &mut self, route_bundle_index: route_bundle::Index, - route_bundle: &RouteBundle, ) -> crate::Result> { - // Re-derive `html` here from `route_bundle.data` so the - // caller never holds two `&mut` into the same allocation. `route_bundle` - // is read-only in this fn — `&RouteBundle` suffices. - let html = route_bundle.data.html(); - debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); - debug_assert!(html.html_bundle.dev_server_id.get() == Some(route_bundle_index)); - debug_assert!(html.cached_response.is_none()); - // `report_html_routes_without_html` keeps a route without these out of the loaded state. - let script_injection_offset = html - .script_injection_offset - .expect("loaded html route has no script injection offset") - .get_usize(); - let bundled_html = html - .bundled_html_text - .as_ref() - .expect("loaded html route has no bundled html"); + let (trace_key, client_script_generation, script_injection_offset, bundled_html, html_path) = { + let route_bundle = &mut self.route_bundles[route_bundle_index.get() as usize]; + debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); + let trace_key = route_bundle.trace_key(); + let generation = route_bundle.client_script_generation; + let html = route_bundle.data.html_mut(); + debug_assert!(html.html_bundle.dev_server_id.get() == Some(route_bundle_index)); + debug_assert!(html.cached_response.is_none()); + // `report_html_routes_without_html` keeps a route without these out of the loaded state. + let script_injection_offset = html + .script_injection_offset + .expect("loaded html route has no script injection offset") + .get_usize(); + // Moved out for the duration of this function (put back below) so + // the graph can be traced while it is read. + let bundled_html = html + .bundled_html_text + .take() + .expect("loaded html route has no bundled html"); + ( + trace_key, + generation, + script_injection_offset, + bundled_html, + html.html_bundle.bundle.path.clone(), + ) + }; + let result = self.generate_html_payload_from( + route_bundle_index, + trace_key, + client_script_generation, + script_injection_offset, + &bundled_html, + &html_path, + ); + self.route_bundles[route_bundle_index.get() as usize] + .data + .html_mut() + .bundled_html_text = Some(bundled_html); + result + } + fn generate_html_payload_from( + &mut self, + route_bundle_index: route_bundle::Index, + trace_key: route_bundle::TraceKey, + client_script_generation: u32, + script_injection_offset: usize, + bundled_html: &[u8], + html_path: &[u8], + ) -> crate::Result> { // The bundler records an offsets in development mode, splitting the HTML // file into two chunks. DevServer is able to insert style/script tags // using the information available in IncrementalGraph. let before_head_end = &bundled_html[..script_injection_offset]; let after_head_end = &bundled_html[script_injection_offset..]; - let mut display_name = strings::without_suffix_comptime( - paths::basename(&html.html_bundle.bundle.path), - b".html", - ); + let mut display_name = + strings::without_suffix_comptime(paths::basename(html_path), b".html"); // TODO: function for URL safe chars if !strings::is_all_ascii(display_name) || strings::contains_char(display_name, b'"') { display_name = b"page"; @@ -2754,7 +2305,7 @@ impl DevServer { let mut gts = self.init_graph_trace_state(0)?; // Run tracing self.client_graph.reset(); - self.trace_all_route_imports(route_bundle, &mut gts, TraceImportGoal::FindCss)?; + self.trace_all_route_imports(trace_key, &mut gts, TraceImportGoal::FindCss)?; let css_ids: &[u64] = &self.client_graph.current_css_files; @@ -2796,7 +2347,7 @@ impl DevServer { ); array.extend_from_slice(&hex_buf[..n]); let n = bun_core::fmt::bytes_to_hex_lower( - &route_bundle.client_script_generation.to_ne_bytes(), + &client_script_generation.to_ne_bytes(), &mut hex_buf, ); array.extend_from_slice(&hex_buf[..n]); @@ -2866,43 +2417,30 @@ impl DevServer { resp: AnyResponse, method: Method, ) { - let self_ptr = std::ptr::from_mut::(self); - // SAFETY: statement-scoped reborrow; the returned `&mut RouteBundle` - // immediately decays to a raw pointer. - let route_bundle: *mut RouteBundle = - unsafe { &mut *self_ptr }.route_bundle_ptr(bundle_index); - // SAFETY: `route_bundle` points into `self.route_bundles`, not resized in - // this fn; the shared reborrow ends with this statement. - let cached: Option> = - unsafe { (*route_bundle).client_bundle.as_ref() }.map(bun_ptr::RefPtr::this_ptr); + let cached: Option> = self + .route_bundle_ptr(bundle_index) + .client_bundle + .as_ref() + .map(bun_ptr::RefPtr::this_ptr); let client_bundle: bun_ptr::ThisPtr = match cached { Some(client_bundle) => client_bundle, None => 'generate: { - // SAFETY: `generate_client_bundle` reads `*route_bundle` and - // never reallocates `route_bundles`; no `&mut` into - // `*route_bundle` is live across this call. - let payload = - unsafe { Self::generate_client_bundle(&mut *self_ptr, &*route_bundle) } - .expect("oom"); + let payload = self.generate_client_bundle(bundle_index).expect("oom"); let bundle = StaticRoute::init_from_any_blob( crate::webcore::AnyBlob::from_owned_slice(payload), crate::server::static_route::InitFromBytesOptions { mime_type: Some(&MimeType::JAVASCRIPT), - // SAFETY: `self_ptr` is `&mut self` erased; live for this fn body. - server: unsafe { &*self_ptr }.server, + server: Some(self.server), ..Default::default() }, ); let client_bundle = bundle.this_ptr(); - // SAFETY: per-access reborrow; no other `&` into `*route_bundle` live. - unsafe { (*route_bundle).client_bundle = Some(bundle) }; + self.route_bundle_ptr(bundle_index).client_bundle = Some(bundle); break 'generate client_bundle; } }; - // SAFETY: shared, statement-scoped read of `*route_bundle`. - let source_map_id = unsafe { &*route_bundle }.source_map_id(); - // SAFETY: `source_maps` is disjoint from `route_bundles`. - unsafe { &mut (*self_ptr).source_maps }.add_weak_ref(source_map_id); + let source_map_id = self.route_bundle_ptr(bundle_index).source_map_id(); + self.source_maps.add_weak_ref(source_map_id); StaticRoute::on_with_method(client_bundle, method, resp); } } @@ -2913,27 +2451,24 @@ enum DevResponse<'a> { } /// When requests are waiting on a bundle, the relevant request information is -/// prepared and stored in a linked list. +/// prepared and stored in a `DeferredRequest`. Ref holders: the bundle's +/// request list (`CurrentBundle.requests` / `NextBundle.requests`) until the +/// dev server answers it, and — for `Handler::ServerHandler` — the request +/// context's `AdditionalOnAbortCallback` until the request aborts or finishes. +#[derive(bun_ptr::CellRefCounted)] pub struct DeferredRequest { + ref_count: Cell, pub(crate) route_bundle_index: route_bundle::Index, - pub(crate) handler: Handler, - pub(crate) dev: *const DevServer, // BACKREF: owned by dev.deferred_request_pool - - /// This struct can be referenced by the dev server (`dev.current_bundle.requests`) - pub(crate) referenced_by_devserver: bool, - pub(crate) weakly_referenced_by_requestcontext: bool, + pub(crate) handler: JsCell, + /// Set once the dev server has answered (or dropped) this request; an + /// abort arriving afterwards is ignored. + handled: Cell, } pub mod deferred_request { use super::*; - /// A small maximum is set because development servers are unlikely to - /// acquire much load, so allocating a ton at the start for no reason - /// is very silly. This contributes to ~6kb of the initial DevServer allocation. - pub const MAX_PREALLOCATED: usize = 16; - - pub type List = bun_collections::pool::SinglyLinkedList; - pub type Node = bun_collections::pool::Node; + pub type List = Vec>; bun_output::define_scoped_log!(debug_log_dr, DlogeferredRequest, hidden); pub(super) use debug_log_dr; @@ -2972,100 +2507,50 @@ use deferred_request::{DlogeferredRequest, Handler, PromiseResponse}; pub(super) use crate::server::SavedRequestUnion; impl DeferredRequest { - pub const MAX_PREALLOCATED: usize = deferred_request::MAX_PREALLOCATED; - - pub(crate) fn is_alive(&self) -> bool { - self.referenced_by_devserver + /// Release the request list's ref: the dev server is done with this + /// request, so tear down whatever the handler still holds first. + #[allow(clippy::needless_pass_by_value)] // consumes the list's ref + pub(crate) fn release(this: bun_ptr::RefPtr) { + this.retire(); + this.deref(); } - // NOTE: This should only be called from the DevServer which is the only - // place that can hold a strong reference - pub(crate) fn deref_(&mut self) { - self.referenced_by_devserver = false; - let should_free = !self.weakly_referenced_by_requestcontext; - self.__deinit(); - if should_free { - self.__free(); - } - } - - pub(crate) fn weak_ref(&mut self) { - debug_assert!(!self.weakly_referenced_by_requestcontext); - self.weakly_referenced_by_requestcontext = true; - } - - pub(crate) fn weak_deref(&mut self) { - self.weakly_referenced_by_requestcontext = false; - if !self.referenced_by_devserver { - self.__free(); + fn retire(&self) { + self.handled.set(true); + deferred_request::debug_log_dr!( + "DeferredRequest(0x{:x}) deinitImpl", + std::ptr::from_ref(self) as usize + ); + if let Handler::ServerHandler(mut saved) = self.handler.replace(Handler::Aborted) { + saved.deinit(); + // `saved` (incl. `js_request: jsc::Strong`) drops at scope exit. } } - fn on_abort_wrapper(this: *mut c_void) { - // SAFETY: this is &mut DeferredRequest registered in defer_request - let self_ = unsafe { bun_ptr::callback_ctx::(this) }; - if !self_.is_alive() { + /// The request context this was deferred for aborted. + pub(crate) fn on_abort_from_request_context(&self) { + if self.handled.get() { return; } - self_.on_abort_impl(); - } - - fn on_abort(&mut self, _: AnyResponse) { - self.on_abort_impl(); + self.on_abort(); } - fn on_abort_impl(&mut self) { + fn on_abort(&self) { deferred_request::debug_log_dr!( "DeferredRequest(0x{:x}) onAbort", std::ptr::from_ref(self) as usize ); self.abort(); - debug_assert!(matches!(self.handler, Handler::Aborted)); - } - - /// Actually free the underlying allocation for the node, does not deinitialize children - fn __free(&mut self) { - // SAFETY: self is the .data field of a Node in deferred_request_pool - let node = unsafe { - bun_core::from_field_ptr!(deferred_request::Node, data, std::ptr::from_mut(self)) - }; - // SAFETY: dev backref is valid while the pool entry exists; `node` is a - // fully-initialized hive slot. `pool::Node` has no drop glue (`data` - // is `MaybeUninit`), so `put`'s in-place drop is a no-op — `__deinit` - // already tore down the payload. - unsafe { - (*self.dev.cast_mut()).deferred_request_pool.put(node); - } - } - - /// *WARNING*: Do not call this directly, instead call `.deref_()` - fn __deinit(&mut self) { - deferred_request::debug_log_dr!( - "DeferredRequest(0x{:x}) deinitImpl", - std::ptr::from_ref(self) as usize - ); - // Note: the pool stores `MaybeUninit` (no `Drop`), - // so the `Handler` payload must be torn down explicitly here. Swap to - // `Aborted` (zero-payload) and let the moved-out value drop — for - // `ServerHandler` this releases `saved.js_request: Strong`. - let handler = ::core::mem::replace(&mut self.handler, Handler::Aborted); - match handler { - Handler::ServerHandler(mut saved) => { - saved.deinit(); - // `saved` (incl. `js_request: jsc::Strong`) drops at scope exit. - } - Handler::BundledHtmlPage(_) | Handler::Aborted => {} - } + debug_assert!(matches!(*self.handler.get(), Handler::Aborted)); } /// Deinitializes state by aborting the connection. - fn abort(&mut self) { + pub(crate) fn abort(&self) { deferred_request::debug_log_dr!( "DeferredRequest(0x{:x}) abort", std::ptr::from_ref(self) as usize ); - let handler = ::core::mem::replace(&mut self.handler, Handler::Aborted); - match handler { + match self.handler.replace(Handler::Aborted) { Handler::ServerHandler(saved) => { deferred_request::debug_log_dr!( " request url: {}", @@ -3118,88 +2603,57 @@ impl DevServer { for key in entry_points.set.keys() { trigger_files.push(BunString::clone_utf8(key)); } - agent.notify_bundle_start(self.inspector_server_id, &mut trigger_files); + agent.notify_bundle_start(self.inspector_server_id, &trigger_files); } self.incremental_result.reset(); // Ref server to keep it from closing. - if let Some(server) = self.server.as_mut() { - server.on_pending_request(); - } + self.server.on_pending_request(); - // Boxed so its address is stable: `bv2.graph.heap: &'static Arena` - // borrows it (self-ref via `CurrentBundle`, see Note on - // `CurrentBundle.bv2`). + // Owned by the `CurrentBundle` alongside `bv2`, which borrows it as + // `graph.heap`. let heap: Box = Box::new(bun_alloc::MimallocArena::new()); - // Borrows `heap`, so AST nodes built during bundle setup - // live exactly as long as the bundle. The arena-allocated allocator - // never runs `Drop`; the `AstAllocState` is taken into `CurrentBundle` - // on success and recycled by the guard below on error paths. - let ast_memory_store: *mut bun_ast::ASTMemoryAllocator = - heap.alloc(bun_ast::ASTMemoryAllocator::borrowing(&heap)); - struct ReleaseAstState(*mut bun_ast::ASTMemoryAllocator); - impl Drop for ReleaseAstState { - fn drop(&mut self) { - // SAFETY: points at the arena-allocated allocator in `heap`, - // which outlives this guard; the `Scope` has already exited. - unsafe { (*self.0).release_ast_state() }; - } - } - let _release_ast_state = ReleaseAstState(ast_memory_store); - // SAFETY: the `ASTMemoryAllocator` lives in a bumpalo chunk owned by - // `heap` → `bv2.graph.heap`; address is stable for the bv2 lifetime. - // The exclusive reborrow is owned by the `ast_scope` guard, which is - // dropped before `bv2` is moved into `current_bundle` below. - let ast_scope = unsafe { bun_ast::ASTMemoryAllocator::enter(&mut *ast_memory_store) }; - - // The bundler stores - // `Option>`. Park the value in `heap` - // — bumpalo chunks are heap-allocated, so the address is stable across - // the move of `heap` into `bv2.graph.heap` and lives exactly as long - // as `bv2`. + let (server_transpiler, heap_ref) = self.bundle_borrows(&heap); + let client_transpiler = ::core::ptr::NonNull::from(&mut *self.client_transpiler); + let ssr_transpiler = match &mut self.ssr_transpiler { + Some(t) => ::core::ptr::NonNull::from(&mut **t), + None => ::core::ptr::NonNull::from(&mut *server_transpiler), + }; + + // AST nodes built during bundle setup live exactly as long as the + // bundle: the `AstAllocState` is taken into `CurrentBundle` on success + // and released by the guard on every path. + let mut ast_memory_store = scopeguard::guard( + heap_ref.alloc(bun_ast::ASTMemoryAllocator::borrowing(heap_ref)), + |store| store.release_ast_state(), + ); + let ast_scope = ast_memory_store.enter(); + + // The bundler stores `Option>`; park the value + // in `heap` so it lives exactly as long as `bv2`. let event_loop: bun_bundler::linker_context_mod::EventLoop = - // SAFETY: `self.vm().event_loop()` is the live per-thread `jsc::EventLoop`. - Some(::core::ptr::NonNull::from(heap.alloc( + Some(::core::ptr::NonNull::from(heap_ref.alloc( bun_event_loop::AnyEventLoop::js(self.vm().event_loop().cast()), ))); - // SAFETY: `heap` is `Box`-allocated above and moved into - // `CurrentBundle` (which also owns `bv2`); the boxed arena's address - // is stable for the lifetime of `bv2.graph.heap`'s borrow. - let heap_ptr: *const bun_alloc::Arena = &raw const *heap; - // Note: split `&mut self` into disjoint field reborrows so - // `server_transpiler` (`&'a mut`) and `client/ssr_transpiler` - // (NonNull) don't trip the single-`&mut self` rule. - let self_ptr = std::ptr::from_mut::(self); let mut bv2: Box> = BundleV2::init( - // SAFETY: `server_transpiler` outlives `bv2` (held by `self`). - unsafe { (*self_ptr).server_transpiler.assume_init_mut() }, + server_transpiler, Some(bundler::bundle_v2::BakeOptions { framework: self.framework.as_bundler_view(), - // SAFETY: sibling fields of `*self`; `BundleV2` stores them as - // raw pointers and never moves them. - client_transpiler: unsafe { - ::core::ptr::NonNull::from((*self_ptr).client_transpiler.assume_init_mut()) - }, - // SAFETY: sibling field of `*self`; `BundleV2` stores it as a raw - // pointer and never moves it. - ssr_transpiler: unsafe { - ::core::ptr::NonNull::from((*self_ptr).ssr_transpiler.assume_init_mut()) - }, + client_transpiler, + ssr_transpiler, plugins: self.bundler_options.plugin, }), - // SAFETY: see `heap_ptr` note above. - unsafe { &*heap_ptr }, + heap_ref, event_loop, false, // watching is handled separately Some(::core::ptr::NonNull::from( bun_threading::work_pool::WorkPool::get(), )), - // SAFETY: see `heap_ptr` note above. - unsafe { &*heap_ptr }, + heap_ref, )?; - bv2.bun_watcher = Some(::core::ptr::NonNull::from(&mut **self.bun_watcher)); + bv2.bun_watcher = Some(::core::ptr::NonNull::from(self.watcher())); bv2.asynchronous = true; let dev_handle = self.bundler_handle(); bv2.dev_server = Some(dev_handle); @@ -3230,9 +2684,8 @@ impl DevServer { // End the AST scope and move its state into the bundle so the small // `AstVec`s built during setup stay alive until the bundle completes. drop(ast_scope); - // SAFETY: `ast_memory_store` lives in `heap`; the scope above has - // exited, so no `&mut` to the allocator is live. - let ast_alloc_state = unsafe { (*ast_memory_store).take_ast_state() }; + let ast_alloc_state = ast_memory_store.take_ast_state(); + drop(ast_memory_store); self.current_bundle = Some(CurrentBundle { bv2, heap, @@ -3251,38 +2704,70 @@ impl DevServer { Ok(()) } + /// The two borrows a `BundleV2<'static>` holds for the lifetime of a + /// bundle: the server transpiler (`bv2.transpiler`) and the bundle's heap + /// (`bv2.graph.heap`). `BundleV2<'a>` ties both to one `'a`, which for the + /// dev server's `Transpiler<'static>` must be `'static`; the transpiler is + /// boxed in `self` and the heap is boxed in the `CurrentBundle` next to + /// `bv2`, and `finalize_bundle_cleanup` drops `bv2` before either. + fn bundle_borrows( + &mut self, + heap: &bun_alloc::MimallocArena, + ) -> ( + &'static mut Transpiler<'static>, + &'static bun_alloc::MimallocArena, + ) { + // SAFETY: see doc comment — both pointees outlive the `BundleV2` that + // receives these borrows; `BundleV2` needs an owning-handle API to say so. + unsafe { + ( + bun_ptr::detach_lifetime_mut(&mut *self.server_transpiler), + bun_ptr::detach_lifetime_ref(heap), + ) + } + } + pub(crate) fn prepare_and_log_resolution_failures(&mut self) -> crate::Result<()> { // Since resolution failures can be asynchronous, their logs are not inserted // until the very end. - let resolution_failures = &self - .current_bundle - .as_ref() - .expect("infallible: bundle active") - .resolution_failure_entries; - if !resolution_failures.is_empty() { - for (owner, log) in resolution_failures - .keys() - .iter() - .zip(resolution_failures.values()) - { - if log.has_errors() { - // `resolution_failure_entries` keys are `OwnerPacked` (1-bit side + file). - let index = owner.file(); - match owner.side() { - bake::Side::Client => self.client_graph.insert_failure( - incremental_graph::InsertFailureKey::Index(index), - log, - false, - )?, - bake::Side::Server => self.server_graph.insert_failure( - incremental_graph::InsertFailureKey::Index(index), - log, - true, - )?, - } + let resolution_failures = ::core::mem::take( + &mut self + .current_bundle + .as_mut() + .expect("infallible: bundle active") + .resolution_failure_entries, + ); + let mut inserted = Ok(()); + for (owner, log) in resolution_failures + .keys() + .iter() + .zip(resolution_failures.values()) + { + if log.has_errors() { + // `resolution_failure_entries` keys are `OwnerPacked` (1-bit side + file). + let index = owner.file(); + inserted = match owner.side() { + bake::Side::Client => self.client_graph_mut().insert_failure( + incremental_graph::InsertFailureKey::Index(index), + log, + false, + ), + bake::Side::Server => self.server_graph_mut().insert_failure( + incremental_graph::InsertFailureKey::Index(index), + log, + true, + ), + }; + if inserted.is_err() { + break; } } } + self.current_bundle + .as_mut() + .expect("infallible: bundle active") + .resolution_failure_entries = resolution_failures; + inserted?; // Theoretically, it shouldn't be possible for errors to leak into dev.log if self.log.has_errors() && !self.log.msgs.is_empty() { @@ -3296,7 +2781,8 @@ impl DevServer { /// A route that a bundle left without html must not reach the loaded state: fail its file instead. fn report_html_routes_without_html(&mut self) -> crate::Result<()> { - for route_bundle in &self.route_bundles { + for i in 0..self.route_bundles.len() { + let route_bundle = &self.route_bundles[i]; let route_bundle::Data::Html(html) = &route_bundle.data else { continue; }; @@ -3330,7 +2816,7 @@ impl DevServer { } let log = html.html_bundle.bundle.no_html_page_log(); - self.client_graph.insert_failure( + self.client_graph_mut().insert_failure( incremental_graph::InsertFailureKey::Index(file_index.get()), &log, false, @@ -3369,15 +2855,18 @@ impl DevServer { removed.deinit(self); } + let mut owners = Vec::with_capacity(self.incremental_result.failures_added.len()); for added in &self.incremental_result.failures_added { payload.extend_from_slice(&added.data); - - match added.get_owner() { + owners.push(added.get_owner()); + } + for owner in owners { + match owner { serialized_failure::Owner::None | serialized_failure::Owner::Route(_) => { unreachable!() } serialized_failure::Owner::Server(index) => { - self.server_graph.trace_dependencies( + self.server_graph_mut().trace_dependencies( index, &mut gts, incremental_graph::TraceDependencyGoal::NoStop, @@ -3385,7 +2874,7 @@ impl DevServer { )? } serialized_failure::Owner::Client(index) => { - self.client_graph.trace_dependencies( + self.client_graph_mut().trace_dependencies( index, &mut gts, incremental_graph::TraceDependencyGoal::NoStop, @@ -3451,113 +2940,111 @@ impl DevServer { /// Used to generate the entry point. Unlike incremental patches, this always /// contains all needed files for a route. - fn generate_client_bundle(&mut self, route_bundle: &RouteBundle) -> crate::Result> { - debug_assert!(route_bundle.client_bundle.is_none()); - debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); + fn generate_client_bundle(&mut self, rbi: route_bundle::Index) -> crate::Result> { + let (trace_key, script_id) = { + let route_bundle = &self.route_bundles[rbi.get() as usize]; + debug_assert!(route_bundle.client_bundle.is_none()); + debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); + (route_bundle.trace_key(), route_bundle.source_map_id()) + }; let _lock = self.graph_safety_lock.guard(); - // Note: scopeguard closures capture `self` by ref, wedging borrowck for - // the rest of the fn. Erase to a raw pointer. - let self_ptr: *mut Self = self; + // `current_chunk_parts`/`current_chunk_len` are scratch buffers shared with + // the HMR pipeline. We must leave them cleared on every exit path. + let mut this = scopeguard::guard(&mut *self, |dev| dev.client_graph.reset()); + let dev = &mut **this; // Prepare bitsets - let mut gts = self.init_graph_trace_state(0)?; + let mut gts = dev.init_graph_trace_state(0)?; // Run tracing - self.client_graph.reset(); - // `current_chunk_parts`/`current_chunk_len` are scratch buffers shared with - // the HMR pipeline. We must leave them cleared on every exit path. - // SAFETY: see `self_ptr` SAFETY above. - // Note: copy `self_ptr` so the `defer!` closure captures a distinct - // local — otherwise borrowck treats `*self_ptr` as held for the guard's - // lifetime and rejects later `&mut (*self_ptr).…` reborrows. - let self_ptr_defer: *mut Self = self_ptr; - scopeguard::defer! { - // SAFETY: `self_ptr_defer` is `&mut self` erased; live for this fn body. - unsafe { (*self_ptr_defer).client_graph.reset() } - }; - self.trace_all_route_imports(route_bundle, &mut gts, TraceImportGoal::FindClientModules)?; + dev.client_graph.reset(); + dev.trace_all_route_imports(trace_key, &mut gts, TraceImportGoal::FindClientModules)?; - let mut react_fast_refresh_id: &[u8] = b""; - if let Some(rfr) = &self.framework.react_fast_refresh { + let mut react_fast_refresh_id: Vec = Vec::new(); + if let Some(import_source) = dev + .framework + .react_fast_refresh + .as_ref() + .map(|rfr| rfr.import_source.to_vec()) + { 'brk: { - let Some(rfr_index) = self.client_graph.get_file_index(&rfr.import_source) else { + let Some(rfr_index) = dev.client_graph.get_file_index(&import_source) else { break 'brk; }; - if !self + if !dev .client_graph .stale_files .is_set(rfr_index.get() as usize) { - self.client_graph.trace_imports( + dev.client_graph_mut().trace_imports( rfr_index, &mut gts, TraceImportGoal::FindClientModules, )?; - react_fast_refresh_id = &rfr.import_source; + react_fast_refresh_id = import_source; } } } - let client_file: Option = match &route_bundle.data { - route_bundle::Data::Framework(fw) => { - let type_idx = self.router.route_ptr(fw.route_index).r#type; - self.router + let client_file: Option = match trace_key { + route_bundle::TraceKey::Framework(route_index) => { + let type_idx = dev.router.route_ptr(route_index).r#type; + dev.router .type_ptr(type_idx) .client_file .map(from_opaque_file_id::<{ bake::Side::Client }>) } - route_bundle::Data::Html(html) => Some(html.bundled_file), + route_bundle::TraceKey::Html(bundled_file) => Some(bundled_file), }; // Insert the source map - let script_id = route_bundle.source_map_id(); map_log!("inc {:x}, 1 for generateClientBundle", script_id.get()); - match self.source_maps.put_or_increment_ref_count(script_id, 1)? { + match dev.source_maps.put_or_increment_ref_count(script_id, 1)? { source_map_store::PutOrIncrementRefCount::Uninitialized(entry) => { - // SAFETY: `self_ptr` is live for the entire fn body; guard runs at scope exit. - // errdefer: unref on early-`?`; disarmed via `into_inner` on success. - let guard = scopeguard::guard(script_id, move |id| unsafe { - (*self_ptr).source_maps.unref(id) - }); gts.clear_and_free(); - // allocates internally with the global allocator in the port. - // SAFETY: see `self_ptr` SAFETY above; `client_graph` is a - // disjoint field from `source_maps`. - unsafe { &mut (*self_ptr).client_graph }.take_source_map(entry)?; - scopeguard::ScopeGuard::into_inner(guard); + let filled = dev.client_graph.take_source_map(entry); + if filled.is_err() { + dev.source_maps.unref(script_id); + } + filled?; } source_map_store::PutOrIncrementRefCount::Shared(_) => {} } - // Note: `take_js_bundle` mutably borrows `client_graph` while - // `initial_entry` would alias `client_graph.bundled_files.keys()[idx]`. - // Clone the key (a short path string) so the borrow ends before the - // `&mut client_graph` call; cold path (per-route bundle finalize). + // `take_js_bundle` mutably borrows `client_graph` while `initial_entry` + // would alias `client_graph.bundled_files.keys()[idx]`. Clone the key + // (a short path string) so the borrow ends before the `&mut client_graph` + // call; cold path (per-route bundle finalize). let initial_entry: Vec = if let Some(idx) = client_file { - self.client_graph.bundled_files.keys()[idx.get() as usize].to_vec() + dev.client_graph.bundled_files.keys()[idx.get() as usize].to_vec() } else { Vec::new() }; - let client_bundle = - self.client_graph - .take_js_bundle(&incremental_graph::TakeJSBundleOptionsClient { - kind: crate::bake::dev_server::ChunkKind::InitialResponse, - initial_response_entry_point: &initial_entry, - react_refresh_entry_point: react_fast_refresh_id, - script_id, - console_log: self.should_receive_console_log_from_browser(), - })?; + let console_log = dev.should_receive_console_log_from_browser(); + let client_bundle = dev.client_graph_mut().take_js_bundle( + &incremental_graph::TakeJSBundleOptionsClient { + kind: crate::bake::dev_server::ChunkKind::InitialResponse, + initial_response_entry_point: &initial_entry, + react_refresh_entry_point: &react_fast_refresh_id, + script_id, + console_log, + }, + )?; Ok(client_bundle) } - fn generate_css_js_array(&mut self, route_bundle: &RouteBundle) -> JsResult { - debug_assert!(matches!( - route_bundle.data, - route_bundle::Data::Framework(_) - )); - debug_assert!(!route_bundle.data.framework().cached_css_file_array.has()); - debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); + fn generate_css_js_array(&mut self, rbi: route_bundle::Index) -> JsResult { + let trace_key = { + let route_bundle = &self.route_bundles[rbi.get() as usize]; + debug_assert!(matches!( + route_bundle.data, + route_bundle::Data::Framework(_) + )); + debug_assert!(!route_bundle.data.framework().cached_css_file_array.has()); + debug_assert!(route_bundle.server_state == route_bundle::State::Loaded); + route_bundle.trace_key() + }; let _lock = self.graph_safety_lock.guard(); @@ -3566,7 +3053,7 @@ impl DevServer { // Run tracing self.client_graph.reset(); - self.trace_all_route_imports(route_bundle, &mut gts, TraceImportGoal::FindCss)?; + self.trace_all_route_imports(trace_key, &mut gts, TraceImportGoal::FindCss)?; let names: &[u64] = &self.client_graph.current_css_files; let global = self.vm().global(); @@ -3600,28 +3087,31 @@ impl DevServer { fn trace_all_route_imports( &mut self, - route_bundle: &RouteBundle, + route_bundle: route_bundle::TraceKey, gts: &mut GraphTraceState, goal: TraceImportGoal, ) -> crate::Result<()> { - match &route_bundle.data { - route_bundle::Data::Framework(fw) => { - let mut route = self.router.route_ptr(fw.route_index); - // Note: copy the two `Type` fields up front so the - // `&self.router` borrow doesn't overlap `&mut self.*_graph`. + match route_bundle { + route_bundle::TraceKey::Framework(route_index) => { + let route_fields = |router: &FrameworkRouter, i: framework_router::RouteIndex| { + let r = router.route_ptr(i); + (r.r#type, r.file_page, r.file_layout, r.parent) + }; + let (route_type, file_page, mut file_layout, mut parent) = + route_fields(&self.router, route_index); let (rt_server_file, rt_client_file) = { - let rt = self.router.type_ptr_const(route.r#type); + let rt = self.router.type_ptr_const(route_type); (rt.server_file, rt.client_file) }; // Both framework entry points are considered - self.server_graph.trace_imports( + self.server_graph_mut().trace_imports( from_opaque_file_id::<{ bake::Side::Server }>(rt_server_file), gts, TraceImportGoal::FindCss, )?; if let Some(id) = rt_client_file { - self.client_graph.trace_imports( + self.client_graph_mut().trace_imports( from_opaque_file_id::<{ bake::Side::Client }>(id), gts, goal, @@ -3629,8 +3119,8 @@ impl DevServer { } // The route file is considered - if let Some(id) = route.file_page { - self.server_graph.trace_imports( + if let Some(id) = file_page { + self.server_graph_mut().trace_imports( from_opaque_file_id::<{ bake::Side::Server }>(id), gts, goal, @@ -3639,27 +3129,27 @@ impl DevServer { // For all parents, the layout is considered loop { - if let Some(id) = route.file_layout { - self.server_graph.trace_imports( + if let Some(id) = file_layout { + self.server_graph_mut().trace_imports( from_opaque_file_id::<{ bake::Side::Server }>(id), gts, goal, )?; } - let Some(p) = route.parent else { break }; - route = self.router.route_ptr(p); + let Some(p) = parent else { break }; + (_, _, file_layout, parent) = route_fields(&self.router, p); } } - route_bundle::Data::Html(html) => { - self.client_graph - .trace_imports(html.bundled_file, gts, goal)?; + route_bundle::TraceKey::Html(bundled_file) => { + self.client_graph_mut() + .trace_imports(bundled_file, gts, goal)?; } } Ok(()) } fn make_array_for_server_components_patch( - &mut self, + &self, global: &JSGlobalObject, items: &[incremental_graph::ServerFileIndex], ) -> JsResult { @@ -3779,24 +3269,19 @@ fn finalize_bundle_cleanup(dev: &mut DevServer, bv2: &mut BundleV2, had_sent_hmr dev.start_next_bundle_if_present(); // Unref the ref added in `start_async_bundle` - if let Some(server) = dev.server.as_mut() { - server.on_static_request_complete(); - } + dev.server.on_static_request_complete(); } fn drain_current_bundle_requests(current_bundle: &mut CurrentBundle) { - if !current_bundle.requests.first.is_null() { + if !current_bundle.requests.is_empty() { // cannot be an assertion because in the case of OOM, the request list was not drained. bun_core::debug!( "current_bundle.requests.first != null. this leaves pending requests without an error page!", ); } - while let Some(node) = current_bundle.requests.pop_first() { - // SAFETY: pop_first returns a live `*mut Node`; `data` was - // initialized by `defer_request`. - let req = unsafe { (*node).data.assume_init_mut() }; + while let Some(req) = current_bundle.requests.pop() { req.abort(); - req.deref_(); + DeferredRequest::release(req); } } @@ -3808,88 +3293,41 @@ pub(super) fn finalize_bundle( result: &mut bundler::bundle_v2::DevServerOutput, ) -> JsResult<()> { debug_assert!(dev.magic == Magic::Valid); - // Note: `had_sent_hmr_event` is read inside the outer-defer scopeguard - // and mutated later in the body. Use a `Cell` so the closure capture is `&Cell` - // (shared), letting the body keep writing through `.set()`. - let had_sent_hmr_event = ::core::cell::Cell::new(false); - - // Both `defer!`s below (outer cleanup, then current_bundle request - // drain) are registered before the first fallible operation — the only - // statements in between are infallible (raw-pointer copies and the - // `expect` on an invariant). Guards run LIFO (drain → outer), and the - // `graph_safety_lock` guard declared after them drops first. - // Note: erase `dev`/`bv2` to raw pointers inside the guard so the - // long-lived closure capture doesn't lock borrowck for the entire fn body. - let dev_ptr = std::ptr::from_mut::(dev); - let bv2_ptr = std::ptr::from_mut::(bv2); - // Note: copy the raw ptrs into closure-only locals so `defer!`'s by-ref - // capture does not hold `*dev_ptr`/`*bv2_ptr` borrowed for the entire fn body. - let dev_ptr_outer: *mut DevServer = dev_ptr; - let bv2_ptr_outer: *mut BundleV2 = bv2_ptr; - scopeguard::defer! { - // SAFETY: `dev`/`bv2` are `&mut` params erased above; both outlive - // this fn-scoped guard, and no borrow of either is live when it runs. - unsafe { - finalize_bundle_cleanup( - &mut *dev_ptr_outer, - &mut *bv2_ptr_outer, - had_sent_hmr_event.get(), - ) - }; - }; + let mut had_sent_hmr_event = false; + let finalized = finalize_bundle_inner(dev, bv2, result, &mut had_sent_hmr_event); + // The chunks live in the bundle's arena, which does not run their destructors. + for chunk in result.chunks.iter_mut() { + drop(::core::mem::take(chunk)); + } + drain_current_bundle_requests( + dev.current_bundle + .as_mut() + .expect("infallible: bundle active"), + ); + finalize_bundle_cleanup(dev, bv2, had_sent_hmr_event); + finalized +} - // Note: holding `&mut CurrentBundle` for the rest of the fn locks `*dev` - // mutably. Erase to a raw pointer and reborrow at each use site via the - // `current_bundle!()` macro. - let current_bundle_ptr: *mut CurrentBundle = dev - .current_bundle - .as_mut() - .expect("infallible: bundle active"); +fn finalize_bundle_inner( + dev: &mut DevServer, + bv2: &mut BundleV2, + result: &mut bundler::bundle_v2::DevServerOutput, + had_sent_hmr_event: &mut bool, +) -> JsResult<()> { macro_rules! current_bundle { () => { - // SAFETY: `dev.current_bundle` is `Some` for the entire fn body — - // it is only set to `None` inside the outer `finalize_bundle_cleanup` defer, which runs after - // every use of this macro. `dev.current_bundle` is never reassigned - // (so the inner `CurrentBundle` is not moved) between here and the - // outer-defer. - unsafe { &mut *current_bundle_ptr } + dev.current_bundle + .as_mut() + .expect("infallible: bundle active") }; } - // Note: see `dev_ptr_outer` rationale above — separate copy for the - // `defer!` capture so `current_bundle!()` keeps reborrowing freely. - let current_bundle_ptr_defer: *mut CurrentBundle = current_bundle_ptr; - scopeguard::defer! { - // SAFETY: see `current_bundle!` SAFETY above; this `defer!` runs - // before the outer cleanup guard (LIFO), so `current_bundle_ptr` is - // still live and no borrow of `*current_bundle_ptr` remains. - unsafe { drain_current_bundle_requests(&mut *current_bundle_ptr_defer) }; - }; let _lock = dev.graph_safety_lock.guard(); - // Note: `js_pseudo_chunk()`/`css_chunks()`/`html_chunks()` each take - // `&mut DevServerOutput`, so calling more than one wedges borrowck. Split - // `result.chunks` once up front (the three regions are disjoint: - // `[0] | [1..1+n_css] | [1+n_css..1+n_css+n_html]`). + // The three regions of `result.chunks` are disjoint: + // `[0] | [1..1+n_css] | [1+n_css..1+n_css+n_html]`. let n_css = result.css_file_list.count(); let n_html = result.html_files.count(); - // Note: snapshot `result.chunks` ptr/len before `split_at_mut` so the - // CSS-chunk loop can re-form the full slice for `intermediate_output.code()` - // without re-borrowing `result.chunks` (already split). - let chunks_ptr: *mut bundler::chunk::Chunk = result.chunks.as_mut_ptr(); - let chunks_len = result.chunks.len(); - scopeguard::defer! { - // SAFETY: `chunks_ptr/len` snapshot the arena slice before any `split_at_mut`. - unsafe { - ::core::ptr::drop_in_place(::core::ptr::slice_from_raw_parts_mut( - chunks_ptr, chunks_len, - )); - } - }; - let (js_chunk_slice, rest_chunks) = result.chunks.split_at_mut(1); - let js_chunk = &mut js_chunk_slice[0]; - let (css_chunks_mut, html_rest) = rest_chunks.split_at_mut(n_css); - let html_chunks_mut = &mut html_rest[..n_html]; let input_file_sources = bv2.graph.input_files.items_source(); let input_file_loaders = bv2.graph.input_files.items_loader(); let import_records = bv2.graph.ast.items_import_records(); @@ -3933,6 +3371,7 @@ pub(super) fn finalize_bundle( let quoted_source_contents = bv2.linker.graph.files.items_quoted_source_contents(); // Pass 1, update the graph's nodes, resolving every bundler source // index into its `IncrementalGraph(...).FileIndex` + let js_chunk = &result.chunks[0]; debug_assert_eq!( js_chunk.content.javascript().parts_in_chunk_in_order.len(), js_chunk.compile_results_for_chunk.len() @@ -3966,7 +3405,7 @@ pub(super) fn finalize_bundle( }; let quoted_contents = "ed_source_contents[part_range.source_index.get() as usize]; match targets[part_range.source_index.get() as usize].bake_graph() { - bake::Graph::Client => dev.client_graph.receive_chunk( + bake::Graph::Client => dev.client_graph_mut().receive_chunk( &mut ctx, index, incremental_graph::ReceiveChunkContent::Js { @@ -3984,59 +3423,54 @@ pub(super) fn finalize_bundle( }, false, )?, - graph @ (bake::Graph::Server | bake::Graph::Ssr) => dev.server_graph.receive_chunk( - &mut ctx, - index, - incremental_graph::ReceiveChunkContent::Js { - code: compile_result.code().to_vec().into_boxed_slice(), - source_map: Some(incremental_graph::ReceiveChunkSourceMap { - chunk: source_map, - // `quoted_contents` lives in the per-bundle AST heap, - // which is destroyed at bundle end; copy onto the - // global heap so the dev-server can hold it across - // rebuilds for stack-trace remapping. - escaped_source: quoted_contents - .as_ref() - .map(|v| v.as_slice().to_vec().into_boxed_slice()), - }), - }, - graph == bake::Graph::Ssr, - )?, + graph @ (bake::Graph::Server | bake::Graph::Ssr) => { + dev.server_graph_mut().receive_chunk( + &mut ctx, + index, + incremental_graph::ReceiveChunkContent::Js { + code: compile_result.code().to_vec().into_boxed_slice(), + source_map: Some(incremental_graph::ReceiveChunkSourceMap { + chunk: source_map, + // `quoted_contents` lives in the per-bundle AST heap, + // which is destroyed at bundle end; copy onto the + // global heap so the dev-server can hold it across + // rebuilds for stack-trace remapping. + escaped_source: quoted_contents + .as_ref() + .map(|v| v.as_slice().to_vec().into_boxed_slice()), + }), + }, + graph == bake::Graph::Ssr, + )? + } } } - for (chunk, metadata) in css_chunks_mut.iter_mut().zip(result.css_file_list.values()) { - debug_assert!(matches!(chunk.content, bundler::chunk::Content::Css(_))); + for (i, metadata) in (1..1 + n_css).zip(result.css_file_list.values()) { + debug_assert!(matches!( + result.chunks[i].content, + bundler::chunk::Content::Css(_) + )); - let index = bun_ast::Index::init(chunk.entry_point.source_index()); + let index = bun_ast::Index::init(result.chunks[i].entry_point.source_index()); - // Note: `IntermediateOutput::code` takes `&mut self` (a field of - // `*chunk`) plus `chunk: &Chunk` and `chunks: &[Chunk]`; borrowck still - // rejects the overlap (`&mut chunk.intermediate_output` aliases the - // shared `&chunk` / `&chunks[..]`). Split via raw - // pointers — the callee never reborrows `chunk.intermediate_output` - // through the `chunk`/`chunks` params (it reads metadata only). + // `IntermediateOutput::code` takes `&mut self` plus `&Chunk`/`&[Chunk]` + // views of the same storage; take the output out for the call. let code = { - let chunk_ptr: *mut bundler::chunk::Chunk = chunk; - // SAFETY: `intermediate_output` is a disjoint field of `*chunk`; - // `code()` does not access it via the `chunk`/`chunks` arguments. - let io = unsafe { &mut (*chunk_ptr).intermediate_output }; - io.code( + let mut io = ::core::mem::take(&mut result.chunks[i].intermediate_output); + let code = io.code( None, &bv2.graph, &bv2.linker.graph, b"THIS_SHOULD_NEVER_BE_EMITTED_IN_DEV_MODE", - // SAFETY: see above; shared reborrow of the same allocation, - // disjoint from the `&mut intermediate_output` receiver. - unsafe { &*chunk_ptr }, - // SAFETY: `result.chunks` outlives this loop body; - // `chunks_ptr/len` were snapshotted before `split_at_mut`, - // and `code()` only reads. - unsafe { ::core::slice::from_raw_parts(chunks_ptr, chunks_len) }, + &result.chunks[i], + result.chunks, None, bundler::chunk::ReferencePathStyle::ImporterRelative, bundler::chunk::SourceMapShiftTracking::Disabled, - )? + ); + result.chunks[i].intermediate_output = io; + code? }; // Create an entry for this file. @@ -4053,6 +3487,8 @@ pub(super) fn finalize_bundle( strings::index_of(first_1024, b"tailwind").is_some() }; dev.assets.replace_path( + &mut dev.client_graph, + dev.server, key, crate::webcore::blob::Any::from_owned_slice(code.buffer.into()), &MimeType::CSS, @@ -4069,7 +3505,7 @@ pub(super) fn finalize_bundle( } } - dev.client_graph.receive_chunk( + dev.client_graph_mut().receive_chunk( &mut ctx, index, incremental_graph::ReceiveChunkContent::Css(h), @@ -4084,24 +3520,15 @@ pub(super) fn finalize_bundle( } } - for chunk in html_chunks_mut.iter_mut() { + for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter_mut() { let index = bun_ast::Index::init(chunk.entry_point.source_index()); - let bundler::CompileResult::Html { - code: compile_result_code, - script_injection_offset: compile_result_offset, - .. - } = chunk.compile_results_for_chunk.get_mut(0) - else { - unreachable!() - }; - let compile_result_offset = *compile_result_offset; let generated_js = dev.generate_javascript_code_for_html_file( index, import_records, input_file_sources, bv2.graph.input_files.items_loader(), )?; - dev.client_graph.receive_chunk( + dev.client_graph_mut().receive_chunk( &mut ctx, index, incremental_graph::ReceiveChunkContent::Js { @@ -4119,10 +3546,8 @@ pub(super) fn finalize_bundle( else { continue; }; - let route_bundle = dev.route_bundle_ptr(route_bundle_index); + let route_bundle = &mut dev.route_bundles[route_bundle_index.get() as usize]; debug_assert!(route_bundle.data.html().bundled_file == client_index); - // Note: split borrow — `invalidate_client_bundle` needs `&mut RouteBundle` - // so the `cached_response` take is done before the long-lived `html` borrow. if route_bundle .data .html_mut() @@ -4130,19 +3555,25 @@ pub(super) fn finalize_bundle( .take() .is_some() { - // SAFETY: `route_bundle` borrows `dev.route_bundles[_]`; `source_maps` - // is a disjoint field reborrowed through the raw `dev_ptr`. - route_bundle.invalidate_client_bundle(unsafe { &mut (*dev_ptr).source_maps }); + route_bundle.invalidate_client_bundle(&mut dev.source_maps); } let html = match &mut route_bundle.data { route_bundle::Data::Html(h) => h, _ => unreachable!(), }; + let bundler::CompileResult::Html { + code: compile_result_code, + script_injection_offset: compile_result_offset, + .. + } = chunk.compile_results_for_chunk.get_mut(0) + else { + unreachable!() + }; // Drops the previous text and transfers ownership of the chunk's // compile-result code, leaving an empty // boxed slice behind in the chunk — nothing reads it after this. html.bundled_html_text = Some(std::mem::take(compile_result_code)); - html.script_injection_offset = Some(route_bundle::ByteOffset::init(compile_result_offset)); + html.script_injection_offset = Some(route_bundle::ByteOffset::init(*compile_result_offset)); chunk .entry_point @@ -4166,31 +3597,38 @@ pub(super) fn finalize_bundle( // Pass 2, update the graph's edges by performing import diffing on each // changed file, removing dependencies. This pass also flags what routes // have been modified. - for part_range in js_chunk.content.javascript().parts_in_chunk_in_order.iter() { + for part_range in result.chunks[0] + .content + .javascript() + .parts_in_chunk_in_order + .iter() + { match targets[part_range.source_index.get() as usize].bake_graph() { - bake::Graph::Server | bake::Graph::Ssr => dev.server_graph.process_chunk_dependencies( - &mut ctx, - incremental_graph::ProcessMode::Normal, - part_range.source_index, - )?, - bake::Graph::Client => dev.client_graph.process_chunk_dependencies( + bake::Graph::Server | bake::Graph::Ssr => { + dev.server_graph_mut().process_chunk_dependencies( + &mut ctx, + incremental_graph::ProcessMode::Normal, + part_range.source_index, + )? + } + bake::Graph::Client => dev.client_graph_mut().process_chunk_dependencies( &mut ctx, incremental_graph::ProcessMode::Normal, part_range.source_index, )?, } } - for chunk in html_chunks_mut.iter() { + for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter() { let index = bun_ast::Index::init(chunk.entry_point.source_index()); - dev.client_graph.process_chunk_dependencies( + dev.client_graph_mut().process_chunk_dependencies( &mut ctx, incremental_graph::ProcessMode::Normal, index, )?; } - for chunk in css_chunks_mut.iter() { + for chunk in result.chunks[1..1 + n_css].iter() { let entry_index = bun_ast::Index::init(chunk.entry_point.source_index()); - dev.client_graph.process_chunk_dependencies( + dev.client_graph_mut().process_chunk_dependencies( &mut ctx, incremental_graph::ProcessMode::Css, entry_index, @@ -4201,7 +3639,7 @@ pub(super) fn finalize_bundle( if !dev.incremental_result.failures_removed.is_empty() || !dev.incremental_result.failures_added.is_empty() { - had_sent_hmr_event.set(true); + *had_sent_hmr_event = true; } dev.index_failures()?; @@ -4234,19 +3672,13 @@ pub(super) fn finalize_bundle( // Fill the source map entry dev.server_graph.take_source_map(&mut source_map_entry)?; - // Note: erase to raw ptr so `render_json` can borrow the entry - // while the cleanup guard is armed. - let entry_ptr: *mut source_map_store::Entry = &raw mut source_map_entry; - scopeguard::defer! { - // SAFETY: `source_map_entry` is a stack local that outlives this guard. - unsafe { - (*entry_ptr).ref_count = 0; - (*entry_ptr).deinit(); - } - }; + let source_map_entry = scopeguard::guard(source_map_entry, |mut entry| { + entry.ref_count = 0; + entry.deinit(); + }); let json_data = - source_map_entry.render_json(dev, ChunkKind::HmrChunk, bake::Side::Server)?; + source_map_entry.render_json(ChunkKind::HmrChunk, bake::Side::Server)?; break 'json Some(json_data); } } else { @@ -4262,18 +3694,13 @@ pub(super) fn finalize_bundle( let global = dev.global(); let server_modules = if let Some(json) = source_map_json { // This memory will be owned by the `DevServerSourceProvider` in C++ - let json: ::core::mem::ManuallyDrop> = ::core::mem::ManuallyDrop::new(json); - match c::bake_load_server_hmr_patch_with_source_map( global, BunString::clone_utf8(&server_bundle), - json.as_ptr(), - json.len(), + json.into_boxed_slice(), ) { Ok(v) => v, Err(err) => { - // SAFETY: vm is JSC_BORROW — valid for DevServer lifetime; - // `print_error_like_object_to_console` needs `&mut`. dev.vm_mut() .print_error_like_object_to_console(global.take_exception(err)); // Note: `panic!()` would unwind through the @@ -4287,8 +3714,6 @@ pub(super) fn finalize_bundle( match c::bake_load_server_hmr_patch(global, BunString::clone_latin1(&server_bundle)) { Ok(v) => v, Err(err) => { - // SAFETY: vm is JSC_BORROW — valid for DevServer lifetime; - // `print_error_like_object_to_console` needs `&mut`. dev.vm_mut() .print_error_like_object_to_console(global.take_exception(err)); // Note: `panic!()` would unwind through the @@ -4299,31 +3724,23 @@ pub(super) fn finalize_bundle( } } }; - // Note: `make_array_for_server_components_patch` needs `&mut self` - // while reading `self.incremental_result.client_components_{added,removed}`. - // It only touches `server_graph.bundled_files.keys()`, so reborrow the - // index slices via `dev_ptr` (disjoint fields). - // SAFETY: `dev_ptr == dev`; `incremental_result` is not mutated by - // `make_array_for_server_components_patch`. - let (added, removed) = unsafe { - let ir = &(*dev_ptr).incremental_result; - ( - ir.client_components_added.as_slice(), - ir.client_components_removed.as_slice(), - ) - }; let errors = match dev.server_register_update_callback.get().unwrap().call( global, global.to_js_value(), &[ server_modules, - dev.make_array_for_server_components_patch(global, added)?, - dev.make_array_for_server_components_patch(global, removed)?, + dev.make_array_for_server_components_patch( + global, + &dev.incremental_result.client_components_added, + )?, + dev.make_array_for_server_components_patch( + global, + &dev.incremental_result.client_components_removed, + )?, ], ) { Ok(v) => v, Err(err) => { - // SAFETY: vm is JSC_BORROW — valid for DevServer lifetime dev.vm_mut() .print_error_like_object_to_console(global.take_exception(err)); panic!( @@ -4405,12 +3822,12 @@ pub(super) fn finalize_bundle( dev.incremental_result.html_routes_soft_affected.clear(); ctx.gts.clear(); - for index in &dev.incremental_result.client_components_affected { - dev.server_graph.trace_dependencies( - *index, + for index in dev.incremental_result.client_components_affected.clone() { + dev.server_graph_mut().trace_dependencies( + index, ctx.gts, incremental_graph::TraceDependencyGoal::NoStop, - *index, + index, )?; } @@ -4459,26 +3876,19 @@ pub(super) fn finalize_bundle( let mut it = route_bits.iterator::(); // List 2 while let Some(i) = it.next() { - // Note: erase to raw ptr — `trace_all_route_imports` below needs - // `&mut *dev` while `route_bundle` (a sub-borrow of `dev.route_bundles`) - // is still live; the two do not actually alias. - let route_bundle: *mut RouteBundle = dev.route_bundle_ptr(route_bundle::Index::init( - u32::try_from(i).expect("int cast"), - )); + let route_bundle = &mut dev.route_bundles[i]; if had_adjusted_edges { - // SAFETY: `route_bundle` points into `dev.route_bundles` (not - // resized in this loop); the exclusive borrow is scoped to this match. - match unsafe { &mut (*route_bundle).data } { + match &mut route_bundle.data { route_bundle::Data::Framework(fw_bundle) => { fw_bundle.cached_css_file_array.clear_without_deallocation() } route_bundle::Data::Html(html) => html.cached_response = None, } } - // SAFETY: statement-scoped read; no `&mut` into `*route_bundle` is live. - if unsafe { (*route_bundle).active_viewers } == 0 || !will_hear_hot_update { + if route_bundle.active_viewers == 0 || !will_hear_hot_update { continue; } + let trace_key = route_bundle.trace_key(); w_int!(i32, i32::try_from(i).expect("int cast")); // If no edges were changed, then it is impossible to @@ -4486,10 +3896,7 @@ pub(super) fn finalize_bundle( if had_adjusted_edges { ctx.gts.clear(); dev.client_graph.current_css_files.clear(); - // SAFETY: `trace_all_route_imports` does not mutate `route_bundles`; - // shared, call-scoped reborrow. - let route_bundle_ref = unsafe { &*route_bundle }; - dev.trace_all_route_imports(route_bundle_ref, ctx.gts, TraceImportGoal::FindCss)?; + dev.trace_all_route_imports(trace_key, ctx.gts, TraceImportGoal::FindCss)?; let css_ids = &dev.client_graph.current_css_files; w_int!(i32, i32::try_from(css_ids.len()).expect("int cast")); @@ -4505,7 +3912,7 @@ pub(super) fn finalize_bundle( } w_int!(i32, -1); - let css_chunks = &*css_chunks_mut; + let css_chunks = &result.chunks[1..1 + n_css]; if will_hear_hot_update { if dev.client_graph.current_chunk_len > 0 || !css_chunks.is_empty() { // Send CSS mutations @@ -4546,19 +3953,14 @@ pub(super) fn finalize_bundle( break 'h source_map_store::Key::init(source_map_hash.digest() | 1); }; let mut sockets: u32 = 0; - for socket_ptr in dev.active_websocket_connections.keys() { - // SAFETY: `*socket_ptr` is a live HmrSocket owned by the - // connection map; shared, statement-scoped. - if unsafe { &**socket_ptr }.is_subscribed(HmrTopic::HotUpdate) { - // SAFETY: as above; the exclusive borrow of the socket's - // map ends when `entry` dies at the end of this block. - let entry = unsafe { &mut (**socket_ptr).referenced_source_maps } - .get_or_put(script_id) - .expect("oom"); - if !entry.found_existing { + for socket in dev.active_websocket_connections.values() { + if socket.is_subscribed(HmrTopic::HotUpdate) { + let is_new = socket.referenced_source_maps.with_mut(|maps| { + !maps.get_or_put(script_id).expect("oom").found_existing + }); + if is_new { sockets += 1; } - *entry.value_ptr = (); } } map_log!("inc {:x}, for {} sockets", script_id.get(), sockets); @@ -4567,10 +3969,7 @@ pub(super) fn finalize_bundle( .put_or_increment_ref_count(script_id, sockets)? { source_map_store::PutOrIncrementRefCount::Uninitialized(entry) => 'brk: { - // Note: reborrow `client_graph` via `dev_ptr` so the - // `&mut Entry` borrow inside `source_maps` does not alias. - // SAFETY: `dev_ptr` is live for this fn; disjoint fields. - unsafe { &mut (*dev_ptr).client_graph }.take_source_map(entry)?; + dev.client_graph.take_source_map(entry)?; break 'brk entry; } source_map_store::PutOrIncrementRefCount::Shared(entry) => entry, @@ -4578,12 +3977,13 @@ pub(super) fn finalize_bundle( w_int!(u32, entry.overlapping_memory_cost); // Build and send the source chunk - dev.client_graph.take_js_bundle_to_list( + let console_log = dev.should_receive_console_log_from_browser(); + dev.client_graph_mut().take_js_bundle_to_list( &mut hot_update_payload, &incremental_graph::TakeJSBundleOptionsClient { kind: crate::bake::dev_server::ChunkKind::HmrChunk, script_id, - console_log: dev.should_receive_console_log_from_browser(), + console_log, ..Default::default() }, )?; @@ -4593,88 +3993,61 @@ pub(super) fn finalize_bundle( } dev.publish(HmrTopic::HotUpdate, &hot_update_payload, Opcode::BINARY); - had_sent_hmr_event.set(true); + *had_sent_hmr_event = true; } if !dev.incremental_result.failures_added.is_empty() { dev.bundles_since_last_error = 0; - // Note: erase the agent borrow to a raw pointer so it can be passed - // through `send_serialized_failures` (which takes `&mut *dev` while - // the borrow derives from `&*dev`) and then re-used below. The agent - // lives in the VM's `Debugger`, which outlives this scope. - let mut inspector_agent_ptr: Option<*const BunFrontendDevServerAgent> = - dev.inspector().map(std::ptr::from_ref); + // Only the first failure payload sent notifies the inspector. + let mut notify_inspector = dev.inspector().is_some(); if current_bundle!().promise.strong.has_value() { - // SAFETY: see `current_bundle!` SAFETY; guard runs before the outer `finalize_bundle_cleanup` defer. - // Note: copy the raw ptr so `defer!`'s by-ref capture does not - // hold `*current_bundle_ptr` borrowed across `current_bundle!()` uses. - let cb_ptr_defer: *mut CurrentBundle = current_bundle_ptr; - scopeguard::defer! { - // SAFETY: `cb_ptr_defer` points into `dev.current_bundle`, live until the outer `finalize_bundle_cleanup` defer runs. - unsafe { (*cb_ptr_defer).promise.reset() } - }; - current_bundle!() - .promise - .set_route_bundle_state(dev, route_bundle::State::PossibleBundlingFailures); + dev.set_current_bundle_route_states(route_bundle::State::PossibleBundlingFailures); let global = dev.global(); - // Note: `bundling_failures` lives on `*dev` but - // `send_serialized_failures` needs `&mut self`; reborrow the keys - // through `dev_ptr`. The callee never touches - // `bundling_failures`. - // SAFETY: `dev_ptr` is live for the entire fn body (see line 3133). - let failures = unsafe { (*dev_ptr).bundling_failures.values() }; + let promise = current_bundle!().promise.strong.take(); + current_bundle!().promise.reset(); dev.send_serialized_failures( - DevResponse::Promise(PromiseResponse { - promise: current_bundle!().promise.strong.take(), - global, - }), - failures, - // SAFETY: agent ptr is from `dev.inspector()` just above; live for this scope. - // `take()` so the queued-request loop and the fallback below - // do not notify the inspector a second time for this bundle. - inspector_agent_ptr.take().map(|p| unsafe { &*p }), + DevResponse::Promise(PromiseResponse { promise, global }), + dev.bundling_failures.values(), + dev.inspector() + .filter(|_| ::core::mem::take(&mut notify_inspector)), )?; } - while let Some(node) = current_bundle!().requests.pop_first() { - // SAFETY: `pop_first` hands back ownership of the intrusive node; - // `data` was initialized by `defer_request`. - let req = unsafe { (*node).data.assume_init_mut() }; - let req_ptr = std::ptr::from_mut::(req); - // SAFETY: the node stays alive until `deref_()` releases it below. - scopeguard::defer! { unsafe { (*req_ptr).deref_() } }; + while let Some(req) = current_bundle!().requests.pop() { + let req = scopeguard::guard(req, DeferredRequest::release); - let rb = dev.route_bundle_ptr(req.route_bundle_index); - rb.server_state = route_bundle::State::PossibleBundlingFailures; + dev.route_bundle_ptr(req.route_bundle_index).server_state = + route_bundle::State::PossibleBundlingFailures; - let resp: DevResponse = match &mut req.handler { + let resp: DevResponse = match req.handler.replace(Handler::Aborted) { Handler::Aborted => continue, - Handler::ServerHandler(saved) => 'brk: { + Handler::ServerHandler(mut saved) => { let resp = saved.response; + // Releases the ref taken in `defer_request`... saved.deinit(); - break 'brk DevResponse::Http(resp); + // ...and the request context's own: the error page is + // written to the raw response below, so nothing hands the + // context to a JS handler that would finish it. + saved.ctx.deref(); + DevResponse::Http(resp) } Handler::BundledHtmlPage(ram) => DevResponse::Http(ram.response), }; - // SAFETY: see Note on `failures` above; `dev_ptr` is live and - // `send_serialized_failures` does not mutate `bundling_failures`. - let failures = unsafe { (*dev_ptr).bundling_failures.values() }; dev.send_serialized_failures( resp, - failures, - // SAFETY: agent ptr is from `dev.inspector()` above; live for this scope. - inspector_agent_ptr.take().map(|p| unsafe { &*p }), + dev.bundling_failures.values(), + dev.inspector() + .filter(|_| ::core::mem::take(&mut notify_inspector)), )?; } - if let Some(agent_ptr) = inspector_agent_ptr { + if notify_inspector { let mut buf: Vec = Vec::new(); dev.encode_serialized_failures( dev.bundling_failures.values(), &mut buf, - // SAFETY: `agent_ptr` is from `dev.inspector()` above; live for this scope. - Some(unsafe { &*agent_ptr }), + dev.inspector(), )?; } @@ -4738,11 +4111,8 @@ pub(super) fn finalize_bundle( } else { 'brk: { let route_bundle_index = 'rbi: { - let first = current_bundle!().requests.first; - if !first.is_null() { - // SAFETY: `first` is a live intrusive node held by current_bundle.requests; `data` was initialized by `defer_request`. - break 'rbi unsafe { (*first).data.assume_init_ref() } - .route_bundle_index; + if let Some(first) = current_bundle!().requests.last() { + break 'rbi first.route_bundle_index; } let route_bundle_indices = current_bundle!().promise.route_bundle_indices.keys(); @@ -4790,61 +4160,48 @@ pub(super) fn finalize_bundle( } } - // Release the lock because the underlying handler may acquire one. - dev.graph_safety_lock.unlock(); - // SAFETY: `dev_ptr` is live for the entire fn body; runs before `_lock` (LIFO), - // so the outer unlock guard sees a locked state again. - scopeguard::defer! { unsafe { (*dev_ptr).graph_safety_lock.lock() } }; - + // Release the lock because the underlying handler may acquire one. + dev.graph_safety_lock.unlock(); + let served = finalize_bundle_serve_requests(dev); + dev.graph_safety_lock.lock(); + served +} + +/// The tail of `finalize_bundle`: hand the finished bundle to every request +/// that was waiting on it. Runs with `graph_safety_lock` released. +fn finalize_bundle_serve_requests(dev: &mut DevServer) -> JsResult<()> { + macro_rules! current_bundle { + () => { + dev.current_bundle + .as_mut() + .expect("infallible: bundle active") + }; + } + // Set all the deferred routes to the .loaded state up front - { - let mut node = current_bundle!().requests.first; - while !node.is_null() { - // SAFETY: node is an intrusive list node valid while current_bundle.requests holds it; - // `data` was initialized by `defer_request`. - let n = unsafe { &*node }; - // SAFETY: `data` was initialized by `defer_request` before being linked. - let rb = dev.route_bundle_ptr(unsafe { n.data.assume_init_ref() }.route_bundle_index); - rb.server_state = route_bundle::State::Loaded; - node = n.next; - } + for i in 0..current_bundle!().requests.len() { + let rbi = current_bundle!().requests[i].route_bundle_index; + dev.route_bundle_ptr(rbi).server_state = route_bundle::State::Loaded; } if current_bundle!().promise.strong.has_value() { - // SAFETY: see `current_bundle!` SAFETY; guard runs before the outer `finalize_bundle_cleanup` defer. - // Note: copy the raw ptr so `defer!`'s by-ref capture does not - // hold `*current_bundle_ptr` borrowed across `current_bundle!()` uses. - let cb_ptr_defer: *mut CurrentBundle = current_bundle_ptr; - scopeguard::defer! { - // SAFETY: `cb_ptr_defer` points into `dev.current_bundle`, live until the outer `finalize_bundle_cleanup` defer runs. - unsafe { (*cb_ptr_defer).promise.deinit_idempotently() } - }; - current_bundle!() - .promise - .set_route_bundle_state(dev, route_bundle::State::Loaded); - let vm = dev.vm(); + dev.set_current_bundle_route_states(route_bundle::State::Loaded); + let vm = dev.vm; let _exit = vm.enter_event_loop_scope(); - current_bundle!() - .promise - .strong - .resolve(vm.global(), JSValue::TRUE)?; + let mut promise = + scopeguard::guard(&mut current_bundle!().promise, |p| p.deinit_idempotently()); + promise.strong.resolve(vm.global(), JSValue::TRUE)?; } - while let Some(node) = current_bundle!().requests.pop_first() { - // SAFETY: `pop_first` hands back ownership of the intrusive node; - // `data` was initialized by `defer_request`. - let req = unsafe { (*node).data.assume_init_mut() }; - let req_ptr = std::ptr::from_mut::(req); - // SAFETY: the node stays alive until `deref_()` releases it below. - scopeguard::defer! { unsafe { (*req_ptr).deref_() } }; + while let Some(req) = current_bundle!().requests.pop() { + let req = scopeguard::guard(req, DeferredRequest::release); - let rb = dev.route_bundle_ptr(req.route_bundle_index); - rb.server_state = route_bundle::State::Loaded; + dev.route_bundle_ptr(req.route_bundle_index).server_state = route_bundle::State::Loaded; // Note: `SavedRequest` is move-only (`Strong` field). Take the // handler by value so the `Saved` payload moves into the union; the // node is being torn down via `_deref` regardless. - match ::core::mem::replace(&mut req.handler, Handler::Aborted) { + match req.handler.replace(Handler::Aborted) { Handler::Aborted => continue, Handler::ServerHandler(saved) => { let response = saved.response; @@ -4858,11 +4215,13 @@ pub(super) fn finalize_bundle( // bake-harness graceful-exit deinit check ("Failed to trigger // deinit") never sees DevServer Drop. scopeguard::defer! { ctx.deref() }; - dev.on_framework_request_with_bundle( - req.route_bundle_index, - SavedRequestUnion::Saved(saved), - response, - )? + let url = match saved.request() { + Some(r) => bun_core::StringView::new(r.url.get()), + None => bun_core::StringView::EMPTY, + }; + let call = + dev.prepare_framework_request(req.route_bundle_index, url.to_utf8().slice())?; + call.run(dev.server, SavedRequestUnion::Saved(saved), response); } Handler::BundledHtmlPage(ram) => { dev.on_html_request_with_bundle(req.route_bundle_index, ram.response, ram.method) @@ -4873,6 +4232,21 @@ pub(super) fn finalize_bundle( } impl DevServer { + fn set_current_bundle_route_states(&mut self, state: route_bundle::State) { + let Self { + current_bundle, + route_bundles, + .. + } = self; + let promise = ¤t_bundle + .as_ref() + .expect("infallible: bundle active") + .promise; + for route_bundle_index in promise.route_bundle_indices.keys() { + route_bundles[route_bundle_index.get() as usize].server_state = state; + } + } + fn start_next_bundle_if_present(&mut self) { debug_assert!(self.magic == Magic::Valid); // Clear the current bundle @@ -4881,53 +4255,15 @@ impl DevServer { // If there were pending requests, begin another bundle. if self.next_bundle.reload_event.is_some() - || !self.next_bundle.requests.first.is_null() + || !self.next_bundle.requests.is_empty() || self.next_bundle.promise.strong.has_value() { let mut entry_points = EntryPointList::empty(); let (is_reload, timer) = if let Some(event) = self.next_bundle.reload_event.take() { - 'brk: { - // SAFETY: `event` points into `*self.watcher_atomics` (its - // own heap allocation); the slot is exclusively owned by - // this thread until recycled below. - let reload_event_timer = unsafe { (*event).timer }; - - let self_ptr: *mut DevServer = self; - let mut current: *mut HotReloadEvent = event; - loop { - // SAFETY: `self_ptr` is `self`; `*current` lives in - // `*self.watcher_atomics` (a separate heap allocation), - // disjoint from the graph/watcher fields - // `process_file_list` mutates. Both exclusive borrows - // end with this call. - unsafe { - HotReloadEvent::process_file_list( - &mut *current, - &mut *self_ptr, - &mut entry_points, - ) - }; - // SAFETY: `current` points into `*self.watcher_atomics`; - // `recycle_event_from_dev_server` only reads/swaps that - // slot and returns the next exclusively-owned one (a - // slot in `self.watcher_atomics.events[..]`). - let Some(next) = (unsafe { - WatcherAtomics::recycle_event_from_dev_server( - self.watcher_atomics.as_ptr(), - current, - ) - }) else { - break; - }; - current = next; - // SAFETY: `current` is the freshly acquired exclusive slot. - #[cfg(debug_assertions)] - debug_assert!(unsafe { (*current).debug_mutex.try_lock() }); - } - - break 'brk (true, reload_event_timer); - } + let shared = std::sync::Arc::clone(&self.hot_reload); + let timer = self.drain_hot_reload_events(&shared, event, &mut entry_points); + (true, timer) } else { (false, Instant::now()) }; @@ -4989,7 +4325,7 @@ impl DevServer { { // `failed` is cleared by the next successful bundle. watch_for_route_file = !file.failed; - self.client_graph.insert_failure( + self.client_graph_mut().insert_failure( incremental_graph::InsertFailureKey::AbsPath(abs_path), log, false, @@ -4999,17 +4335,17 @@ impl DevServer { } } else { match graph { - bake::Graph::Server => self.server_graph.insert_failure( + bake::Graph::Server => self.server_graph_mut().insert_failure( incremental_graph::InsertFailureKey::AbsPath(abs_path), log, false, )?, - bake::Graph::Ssr => self.server_graph.insert_failure( + bake::Graph::Ssr => self.server_graph_mut().insert_failure( incremental_graph::InsertFailureKey::AbsPath(abs_path), log, true, )?, - bake::Graph::Client => self.client_graph.insert_failure( + bake::Graph::Client => self.client_graph_mut().insert_failure( incremental_graph::InsertFailureKey::AbsPath(abs_path), log, false, @@ -5021,7 +4357,7 @@ impl DevServer { if watch_for_route_file { // Bundles the route again once its html file exists, like a failed import. - self.directory_watchers.track_resolution_failure( + self.track_resolution_failure( abs_path, paths::basename(abs_path), bake::Graph::Client, @@ -5042,10 +4378,14 @@ impl DevServer { let _g = self.graph_safety_lock.guard(); let owner: serialized_failure::OwnerPacked = if graph == bake::Graph::Client { - let idx = self.client_graph.insert_stale(abs_path, graph)?; + let idx = self + .client_graph + .insert_stale(&mut self.assets, abs_path, graph)?; serialized_failure::OwnerPacked::new(bake::Side::Client, idx.get()) } else { - let idx = self.server_graph.insert_stale(abs_path, graph)?; + let idx = self + .server_graph + .insert_stale(&mut self.assets, abs_path, graph)?; serialized_failure::OwnerPacked::new(bake::Side::Server, idx.get()) }; let current_bundle = self @@ -5149,43 +4489,40 @@ impl From for OpaqueFileIdOrOptional { } } -fn on_request(dev: &mut DevServer, req: &mut Request, mut resp: AnyResponse) -> JsResult<()> { +fn on_request( + dev: &mut DevServer, + req: &mut Request, + mut resp: AnyResponse, +) -> JsResult { let mut params: framework_router::MatchedParams = Default::default(); if let Some(route_index) = dev.router.match_slow(req.url(), &mut params) { let route_bundle_index = dev .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) .expect("oom"); let mut ctx = RequestEnsureRouteBundledCtx { - dev: std::ptr::from_mut::(dev), req: ReqOrSaved::Req(req), resp, kind: deferred_request::HandlerKind::ServerHandler, route_bundle_index, + framework_call: None, }; let rbi = ctx.route_bundle_index; return match ensure_route_is_bundled(dev, rbi, &mut ctx) { Err(jsc::JsError::OutOfMemory) => bun_core::out_of_memory(), - bundled => bundled, + Err(err) => Err(err), + Ok(()) => Ok(match ctx.framework_call { + Some(call) => RequestAction::Framework(call), + None => RequestAction::Done, + }), }; } - if !dev - .server - .as_ref() - .expect("infallible: server bound") - .config() - .on_request - .is_empty() - { - dev.server - .as_mut() - .expect("infallible: server bound") - .on_request(req, resp); - return Ok(()); + if !dev.server.config().on_request.is_empty() { + return Ok(RequestAction::UserHandler); } send_built_in_not_found(&mut resp); - Ok(()) + Ok(RequestAction::Done) } impl DevServer { @@ -5203,11 +4540,11 @@ impl DevServer { .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Html(html)) .map_err(|_| AllocError)?; let mut ctx = RequestEnsureRouteBundledCtx { - dev: std::ptr::from_mut::(self), req: ReqOrSaved::Req(req), resp, kind: deferred_request::HandlerKind::BundledHtmlPage, route_bundle_index, + framework_call: None, }; let rbi = ctx.route_bundle_index; match ensure_route_is_bundled(self, rbi, &mut ctx) { @@ -5227,17 +4564,13 @@ impl DevServer { &mut self, route: route_bundle::UnresolvedIndex, ) -> crate::Result { - let index_location: *mut route_bundle::IndexOptional = match route { + let existing = match route { route_bundle::UnresolvedIndex::Framework(route_index) => { - &raw mut self.router.route_ptr_mut(route_index).bundle + self.router.route_ptr(route_index).bundle } - // R-2: `dev_server_id` is `Cell>`; `Cell::as_ptr` - // yields the inner `*mut Option` so the `*index_location` - // read/write below stays raw and matches the framework arm's type. - route_bundle::UnresolvedIndex::Html(html) => html.dev_server_id.as_ptr(), + route_bundle::UnresolvedIndex::Html(html) => html.dev_server_id.get(), }; - // SAFETY: index_location points into self/html which outlive this fn - if let Some(bundle_index) = unsafe { *index_location } { + if let Some(bundle_index) = existing { return Ok(bundle_index); } @@ -5274,6 +4607,7 @@ impl DevServer { route_bundle::UnresolvedIndex::Html(html) => 'brk: { let html_ref = &*html; let incremental_graph_index = self.client_graph.insert_stale_extra( + &mut self.assets, &html_ref.bundle.path, bake::Graph::Client, incremental_graph::RouteKind::Route, @@ -5299,8 +4633,12 @@ impl DevServer { client_bundle: None, active_viewers: 0, }); - // SAFETY: index_location still valid (route_bundles is a separate field) - unsafe { *index_location = Some(bundle_index) }; + match route { + route_bundle::UnresolvedIndex::Framework(route_index) => { + self.router.route_ptr_mut(route_index).bundle = Some(bundle_index); + } + route_bundle::UnresolvedIndex::Html(html) => html.dev_server_id.set(Some(bundle_index)), + } Ok(bundle_index) } } @@ -5310,7 +4648,7 @@ impl DevServer { &self, failures: &[SerializedFailure], buf: &mut Vec, - inspector_agent: Option<&BunFrontendDevServerAgent>, + inspector_agent: Option>, ) -> Result<(), AllocError> { let mut all_failures_len: usize = 0; for fail in failures { @@ -5325,8 +4663,7 @@ impl DevServer { let len = bun_base64::encode_len(&all_failures); // Zero-extend then encode in place; `encode_len` is an upper bound so - // truncate to the actual encoded length afterward. Avoids the - // `spare_capacity_mut` + `set_len` unsafe dance for a one-shot path. + // truncate to the actual encoded length afterward. buf.resize(failures_start_buf_pos + len, 0); let written = bun_base64::encode(&mut buf[failures_start_buf_pos..], &all_failures); buf.truncate(failures_start_buf_pos + written); @@ -5345,10 +4682,10 @@ impl DevServer { } fn send_serialized_failures( - &mut self, + &self, resp: DevResponse, failures: &[SerializedFailure], - inspector_agent: Option<&BunFrontendDevServerAgent>, + inspector_agent: Option>, ) -> crate::Result<()> { let mut buf: Vec = Vec::with_capacity(2048); @@ -5391,7 +4728,7 @@ impl DevServer { crate::webcore::blob::Any::from_array_list(buf), crate::server::static_route::InitFromBytesOptions { mime_type: Some(&MimeType::HTML), - server: self.server, + server: Some(self.server), status_code: 500, ..Default::default() }, @@ -5407,11 +4744,8 @@ impl DevServer { if headers.get(b"etag").is_none() && !any_blob.slice().is_empty() { bun_http::headers::append_etag(any_blob.slice(), &mut headers); } - let fetch_headers = bun_http_jsc::headers_jsc::to_fetch_headers(&headers, global)?; - // SAFETY: `to_fetch_headers` returns a fresh +1 `FetchHeaders*`; - // ownership is transferred to `HeadersRef`. let headers_ref = - unsafe { crate::webcore::response::HeadersRef::adopt(fetch_headers) }; + bun_http_jsc::headers_jsc::to_fetch_headers_ref(&headers, global)?; let response: Response = Response::init( crate::webcore::response::Init { status_code: 500, @@ -5494,14 +4828,16 @@ impl DevServer { pub fn emit_visualizer_message_if_needed(&mut self) {} #[inline] - fn timer_heap(&self) -> &mut crate::timer::All { + fn timer_heap() -> &'static mut crate::timer::All { crate::jsc_hooks::timer_all_mut() } - pub fn emit_memory_visualizer_message_timer( - _timer: &mut EventLoopTimer, - _: &bun_core::Timespec, - ) { + pub fn emit_memory_visualizer_message_timer(&mut self, _: &bun_core::Timespec) {} + + /// `EventLoopTimerTag::DevServerSweepSourceMaps` handler. + pub fn sweep_source_map_weak_refs(&mut self, now: &bun_core::Timespec) { + self.source_maps.sweep_weak_refs(now); + self.emit_memory_visualizer_message_if_needed(); } pub fn emit_memory_visualizer_message_if_needed(&mut self) {} @@ -5610,45 +4946,38 @@ impl DevServer { mod c { use super::*; + unsafe extern "C" { + safe fn BakeLoadServerHmrPatch(global: &JSGlobalObject, code: BunString) -> JSValue; + /// Ownership of `source_map_json` (a leaked `Box<[u8]>`) transfers to + /// the C++ `DevServerSourceProvider`, which frees it with the default + /// allocator. + safe fn BakeLoadServerHmrPatchWithSourceMap( + global: &JSGlobalObject, + code: BunString, + source_map_json: bun_core::ffi::FfiSlice<'static, u8>, + ) -> JSValue; + safe fn BakeLoadInitialServerCode( + global: &JSGlobalObject, + code: BunString, + separate_ssr_graph: bool, + ) -> JSValue; + } + pub(super) fn bake_load_server_hmr_patch( global: &JSGlobalObject, code: BunString, ) -> JsResult { - unsafe extern "C" { - safe fn BakeLoadServerHmrPatch(global: &JSGlobalObject, code: BunString) -> JSValue; - } jsc::from_js_host_call(global, || BakeLoadServerHmrPatch(global, code)) } pub(super) fn bake_load_server_hmr_patch_with_source_map( global: &JSGlobalObject, code: BunString, - source_map_json_ptr: *const u8, - source_map_json_len: usize, + source_map_json: Box<[u8]>, ) -> JsResult { - unsafe extern "C" { - // PRECONDITION: `ptr` must be readable for `len` bytes and point to - // a heap allocation whose ownership transfers to the C++ - // `DevServerSourceProvider` (caller wraps the backing `Vec` in - // `ManuallyDrop`). Cannot be `safe fn` — raw ptr+len pair carries a - // caller-side validity + ownership precondition. - fn BakeLoadServerHmrPatchWithSourceMap( - global: *const JSGlobalObject, - code: BunString, - ptr: *const u8, - len: usize, - ) -> JSValue; - } - // SAFETY: `global` is live; `source_map_json_ptr`/`len` are forwarded from - // the sole caller's `ManuallyDrop>` (valid for `len`, ownership - // ceded to C++) — discharges the ptr+len precondition above. - jsc::from_js_host_call(global, || unsafe { - BakeLoadServerHmrPatchWithSourceMap( - global, - code, - source_map_json_ptr, - source_map_json_len, - ) + let source_map_json = bun_core::ffi::FfiSlice::new(Box::leak(source_map_json)); + jsc::from_js_host_call(global, || { + BakeLoadServerHmrPatchWithSourceMap(global, code, source_map_json) }) } @@ -5657,13 +4986,6 @@ mod c { code: BunString, separate_ssr_graph: bool, ) -> JsResult { - unsafe extern "C" { - safe fn BakeLoadInitialServerCode( - global: &JSGlobalObject, - code: BunString, - separate_ssr_graph: bool, - ) -> JSValue; - } jsc::from_js_host_call(global, || { BakeLoadInitialServerCode(global, code, separate_ssr_graph) }) @@ -5707,7 +5029,7 @@ impl DevServer { /// The dev server's inspector agent, or `None` while the /// `BunFrontendDevServer` domain is disabled. The agent's state is /// `Cell`-based, so a shared borrow suffices. JS-thread only. - pub(crate) fn inspector(&self) -> Option<&BunFrontendDevServerAgent> { + pub(crate) fn inspector(&self) -> Option> { if let Some(debugger) = self.vm().debugger.as_ref() { bun_core::hint::cold(); let agent = BunFrontendDevServerAgent::from_slot(&debugger.extension_agent); @@ -5718,64 +5040,37 @@ impl DevServer { } None } +} +impl bun_watcher::WatcherHandler for crate::bake::dev_server::DevWatcherHandler { /// Called on watcher's thread; Access to dev-server state restricted. - pub(crate) fn on_file_update( - &mut self, - events: &[bun_watcher::Event], - changed_files: &[bun_watcher::ChangedFilePath], - watchlist: &bun_watcher::WatchList, - ) { - debug_assert!(self.magic == Magic::Valid); + fn on_file_update(&mut self, batch: &mut bun_watcher::FileUpdateBatch<'_>) { debug_log!("onFileUpdate start"); scopeguard::defer! { debug_log!("onFileUpdate end") }; - let mut slice = watchlist.slice(); - // Note: SoA columns are disjoint, but `items_mut` borrows the whole - // slice mutably. Erase each column to a raw `*mut [T]` and reborrow at - // the use sites — the columns never alias (per `MultiArrayElement` layout). - let file_paths: *const [std::borrow::Cow<'static, [u8]>] = slice.items_file_path(); - // SAFETY: column 4 (`Count`) is `u32` per `WatchItemField`. - let counts: *mut [u32] = slice.items_mut::<"count", u32>(); - let kinds: *const [bun_watcher::Kind] = slice.items_kind(); - // SAFETY: `file_paths`/`kinds`/`counts` point to disjoint SoA columns owned - // by `watchlist`, which outlives this fn; reborrow the shared ones as - // slices for indexing (`counts` stays raw, deref'd per-access below). - let file_paths = unsafe { &*file_paths }; - // SAFETY: see above; `kinds` is a disjoint SoA column owned by `watchlist`. - let kinds = unsafe { &*kinds }; - - let atomics = self.watcher_atomics.as_ptr(); - // SAFETY: `atomics` is the heap `WatcherAtomics`; watcher thread holds - // `Watcher.mutex`. The returned slot is exclusive on this thread and - // is deref'd per-access below (no long-lived `&mut`). - let ev_ptr = unsafe { WatcherAtomics::watcher_acquire_event(atomics) }; - // Note: erase `self` to a raw ptr in the deferred closures so the - // loop body can keep using `self.bun_watcher`. - let self_ptr: *mut Self = self; - let ev_ptr_defer: *mut HotReloadEvent = ev_ptr; - scopeguard::defer! { - // SAFETY: `atomics`/`ev_ptr_defer` are live for the fn body; guard - // runs at scope exit with `Watcher.mutex` still held. - unsafe { WatcherAtomics::watcher_release_and_submit_event(atomics, ev_ptr_defer) } - }; - - // SAFETY: see `self_ptr` SAFETY above. - scopeguard::defer! { unsafe { (*self_ptr).bun_watcher.flush_evictions() } }; + let shared = &self.shared; + batch.watcher().thread_lock.assert_locked(); + let ev = shared.watcher_acquire_event(); + scopeguard::defer! { shared.watcher_release_and_submit_event(ev) }; + let mut batch = scopeguard::guard(batch, |batch| batch.watcher_mut().flush_evictions()); + let mut files = shared.events[ev as usize].data.lock(); - for event in events { + for i in 0..batch.events().len() { + let event = batch.events()[i]; + let index = event.index as usize; // TODO: why does this out of bounds when you delete every file in the directory? - if event.index as usize >= file_paths.len() { + if index >= batch.watcher().watchlist.len() { continue; } - let file_path = &file_paths[event.index as usize]; - // SAFETY: `counts` is a disjoint SoA column owned by `watchlist` - // (see above); accesses are statement-scoped. - let update_count = unsafe { (*counts)[event.index as usize] } + 1; - // SAFETY: as above. - unsafe { (*counts)[event.index as usize] = update_count }; - let kind = kinds[event.index as usize]; + batch + .watcher_mut() + .watchlist + .slice() + .items_mut::<"count", u32>()[index] += 1; + let watchlist = batch.watcher().watchlist.slice(); + let file_path = &watchlist.items_file_path()[index]; + let kind = watchlist.items_kind()[index]; debug_log!( "{} change: {} {}", @@ -5789,21 +5084,19 @@ impl DevServer { match kind { bun_watcher::Kind::File => { + files.append_file(file_path); + if event.op.contains(bun_watcher::Op::DELETE) || event.op.contains(bun_watcher::Op::RENAME) { // TODO: audit this line heavily - self.bun_watcher.remove_at_index::( + batch.watcher_mut().remove_at_index::( bun_watcher::Kind::File, event.index, 0, &[], ); } - - // SAFETY: `ev_ptr` is this thread's exclusively-acquired - // slot (see above); the borrow ends with this call. - unsafe { &mut *ev_ptr }.append_file(file_path); } bun_watcher::Kind::Directory => { // Note: `target_os = "linux"` is false on Android, so @@ -5812,32 +5105,26 @@ impl DevServer { #[cfg(any(target_os = "linux", target_os = "android"))] { // INotifyWatcher stores sub paths into `changed_files` - let names = event.names(changed_files); + let names = event.names(batch.changed_files()); if !names.is_empty() { for maybe_sub_path in names { - // SAFETY: see `ev_ptr` above; call-scoped borrow. - unsafe { &mut *ev_ptr } - .append_dir(file_path, maybe_sub_path.map(|s| s.as_bytes())); + files.append_dir(file_path, maybe_sub_path.map(|s| s.as_bytes())); } } else { - // SAFETY: see `ev_ptr` above; call-scoped borrow. - unsafe { &mut *ev_ptr }.append_dir(file_path, None); + files.append_dir(file_path, None); } } #[cfg(not(any(target_os = "linux", target_os = "android")))] { - let _ = changed_files; - // SAFETY: see `ev_ptr` above; call-scoped borrow. - unsafe { &mut *ev_ptr }.append_dir(file_path, None); + files.append_dir(file_path, None); } } } } } - pub(crate) fn on_watch_error(&self, err: sys::Error) { + fn on_error(&mut self, err: sys::Error) { if !err.path.is_empty() { - // Note: split out path before moving `err` into `Output::err`. let path = err.path.clone(); Output::err( err, @@ -5852,19 +5139,17 @@ impl DevServer { ); // TODO: attempt to automatically restart the watcher thread, perhaps wait for next request. } +} +impl DevServer { pub(crate) fn publish(&self, topic: HmrTopic, message: &[u8], opcode: Opcode) { - if let Some(s) = &self.server { - let _ = s.publish(&topic.uws_topic(), message, opcode, false); - } + let _ = self + .server + .publish(&topic.uws_topic(), message, opcode, false); } pub(crate) fn num_subscribers(&self, topic: HmrTopic) -> u32 { - if let Some(s) = &self.server { - s.num_subscribers(&topic.uws_topic()) - } else { - 0 - } + self.server.num_subscribers(&topic.uws_topic()) } } @@ -5887,44 +5172,53 @@ impl SafeFileId { } } -impl DevServer { - /// Interface function for FrameworkRouter - pub(crate) fn get_file_id_for_router( +/// The `DevServer` state `FrameworkRouter::scan_all` reports discovered route +/// files into. +pub(crate) struct RouterInsertionCtx<'a> { + pub(crate) server_graph: &'a mut IncrementalGraph<{ bake::Side::Server }>, + pub(crate) assets: &'a mut Assets, + pub(crate) route_lookup: + &'a mut ArrayHashMap, + pub(crate) root: &'a [u8], +} + +impl framework_router::InsertionHandler for RouterInsertionCtx<'_> { + fn get_file_id_for_router( &mut self, abs_path: &[u8], associated_route: framework_router::RouteIndex, file_kind: framework_router::FileKind, - ) -> crate::Result { - let index = self - .server_graph - .insert_stale_extra( - abs_path, - bake::Graph::Server, - incremental_graph::RouteKind::Route, - ) - .map_err(crate::Error::from)?; - self.route_lookup.put( - index, - RouteIndexAndRecurseFlag::new( - associated_route, - file_kind == framework_router::FileKind::Layout, - ), + ) -> Result { + let index = self.server_graph.insert_stale_extra( + self.assets, + abs_path, + bake::Graph::Server, + incremental_graph::RouteKind::Route, )?; + self.route_lookup + .put( + index, + RouteIndexAndRecurseFlag::new( + associated_route, + file_kind == framework_router::FileKind::Layout, + ), + ) + .map_err(|_| AllocError)?; Ok(to_opaque_file_id::<{ bake::Side::Server }>(index)) } - pub(crate) fn on_router_syntax_error( - &self, + fn on_router_syntax_error( + &mut self, rel_path: &[u8], - log: &framework_router::TinyLog, + log: framework_router::TinyLog, ) -> Result<(), AllocError> { // TODO: maybe this should track the error, send over HmrSocket? log.print(rel_path); Ok(()) } - pub(crate) fn on_router_collision_error( - &self, + fn on_router_collision_error( + &mut self, rel_path: &[u8], other_id: OpaqueFileId, ty: framework_router::FileKind, @@ -5938,8 +5232,9 @@ impl DevServer { let mut buf = paths::path_buffer_pool::get(); bun_core::pretty_errorln!( " - {}", - bstr::BStr::new(self.relative_path( - &mut *buf, + bstr::BStr::new(relative_path( + self.root, + &mut buf, &self.server_graph.bundled_files.keys() [from_opaque_file_id::<{ bake::Side::Server }>(other_id).get() as usize] )) @@ -5949,33 +5244,6 @@ impl DevServer { } } -impl framework_router::InsertionHandler for DevServer { - fn get_file_id_for_router( - &mut self, - abs_path: &[u8], - associated_route: framework_router::RouteIndex, - kind: framework_router::FileKind, - ) -> Result { - DevServer::get_file_id_for_router(self, abs_path, associated_route, kind) - .map_err(|_| AllocError) - } - fn on_router_syntax_error( - &mut self, - rel_path: &[u8], - fail: framework_router::TinyLog, - ) -> Result<(), AllocError> { - DevServer::on_router_syntax_error(self, rel_path, &fail) - } - fn on_router_collision_error( - &mut self, - rel_path: &[u8], - other_id: OpaqueFileId, - file_kind: framework_router::FileKind, - ) -> Result<(), AllocError> { - DevServer::on_router_collision_error(self, rel_path, other_id, file_kind) - } -} - fn to_opaque_file_id( index: incremental_graph::FileIndex, ) -> OpaqueFileId { @@ -5996,42 +5264,46 @@ fn from_opaque_file_id( incremental_graph::FileIndex::::init(id.get()) } +/// Returns posix style path, suitible for URLs and reproducible hashes. +/// The caller must provide a PathBuffer from the pool. +pub(crate) fn relative_path<'a>( + root: &[u8], + relative_path_buf: &'a mut PathBuffer, + path: &'a [u8], +) -> &'a [u8] { + if !paths::is_absolute(path) { + return path; + } + + if path.len() > root.len() && path[root.len()] == b'/' && path.starts_with(root) { + return &path[root.len() + 1..]; + } + + if path.len() + root.len() * 2 >= paths::MAX_PATH_BYTES { + return path; + } + + // `relative_platform_buf` with ALWAYS_COPY=true writes into + // `relative_path_buf[..len]` (same invariant `relative_buf_z` relies + // on); capture the length, drop the shared borrow, then re-slice + // mutably to convert separators in place. + let rel_len = bun_paths::resolve_path::relative_platform_buf::< + bun_paths::resolve_path::platform::Auto, + true, + >(&mut relative_path_buf[..], root, path) + .len(); + bun_paths::resolve_path::platform_to_posix_in_place::(&mut relative_path_buf[..rel_len]); + &relative_path_buf[..rel_len] +} + impl DevServer { - /// Returns posix style path, suitible for URLs and reproducible hashes. - /// The caller must provide a PathBuffer from the pool. + /// See [`relative_path()`]. pub(crate) fn relative_path<'a>( &self, relative_path_buf: &'a mut PathBuffer, path: &'a [u8], ) -> &'a [u8] { - if !paths::is_absolute(path) { - return path; - } - - if path.len() > self.root.len() - && path[self.root.len()] == b'/' - && path.starts_with(&self.root) - { - return &path[self.root.len() + 1..]; - } - - if path.len() + self.root.len() * 2 >= paths::MAX_PATH_BYTES { - return path; - } - - // `relative_platform_buf` with ALWAYS_COPY=true writes into - // `relative_path_buf[..len]` (same invariant `relative_buf_z` relies - // on); capture the length, drop the shared borrow, then re-slice - // mutably to convert separators in place. - let rel_len = bun_paths::resolve_path::relative_platform_buf::< - bun_paths::resolve_path::platform::Auto, - true, - >(&mut relative_path_buf[..], &self.root, path) - .len(); - bun_paths::resolve_path::platform_to_posix_in_place::( - &mut relative_path_buf[..rel_len], - ); - &relative_path_buf[..rel_len] + relative_path(&self.root, relative_path_buf, path) } /// Either of two conditions make this true: @@ -6186,6 +5458,8 @@ impl DevServer { ) -> crate::Result<()> { let _g = self.graph_safety_lock.guard(); let _ = self.assets.replace_path( + &mut self.client_graph, + self.server, path.text, contents, &MimeType::by_extension(path.name().ext_without_leading_dot()), @@ -6206,14 +5480,9 @@ impl DevServer { pub(crate) fn on_plugins_rejected(&mut self) -> crate::Result<()> { self.plugin_state = PluginState::Err; - while let Some(item) = self.next_bundle.requests.pop_first() { - // SAFETY: `pop_first` returns a valid `*mut Node`; - // `data` was initialized by `defer_request`. - unsafe { - let d = (*item).data.assume_init_mut(); - d.abort(); - d.deref_(); - } + while let Some(d) = self.next_bundle.requests.pop() { + d.abort(); + DeferredRequest::release(d); } self.next_bundle.route_queue.clear_retaining_capacity(); // TODO: allow recovery from this state @@ -6224,92 +5493,54 @@ impl DevServer { /// Problem statement documented on `SCRIPT_UNREF_PAYLOAD` /// Takes 8 bytes: The generation ID in hex. struct UnrefSourceMapRequest { - // BACKREF: DevServer outlives the request; raw ptr avoids the `'static` - // bound on `BodyReaderHandler` that a borrowed `&mut DevServer` would violate. - dev: *mut DevServer, - body: uws::BodyReaderMixin, + dev: bun_ptr::BackRef, + server: AnyServer, } -bun_core::intrusive_field!(UnrefSourceMapRequest, body: uws::BodyReaderMixin); -impl bun_uws_sys::body_reader_mixin::BodyReaderHandler for UnrefSourceMapRequest { - unsafe fn on_body(this: *mut Self, body: &[u8], resp: AnyResponse) -> bun_uws_sys::Result<()> { - // SAFETY: caller (BodyReaderMixin) passes the original heap-allocated - // pointer with full-allocation provenance and no live borrows. - unsafe { Self::run_with_body(this, body, resp) }.map_err(Into::into) - } - unsafe fn on_error(this: *mut Self) { - // SAFETY: caller passes the original heap-allocated pointer; finalize - // consumes it via heap::take exactly once. - unsafe { Self::finalize(this) } - } -} - -impl UnrefSourceMapRequest { - fn run(dev: &mut DevServer, _: &mut Request, resp: &mut R) - where - R: bun_uws_sys::body_reader_mixin::BodyResponse, - { - dev.server - .as_mut() - .expect("server bound") - .on_pending_request(); - let ctx = Box::new(UnrefSourceMapRequest { - dev: std::ptr::from_mut::(dev), - body: uws::BodyReaderMixin::init(), - }); - let raw = bun_core::heap::into_raw(ctx); - uws::BodyReaderMixin::::read_body(raw, resp); - } - - /// SAFETY: `ctx` must be the pointer returned by `heap::alloc` in `run`; - /// called exactly once. - unsafe fn finalize(ctx: *mut UnrefSourceMapRequest) { - // SAFETY: caller contract — ctx is the original Box allocation; no - // live borrow of *ctx exists. - let ctx = unsafe { bun_core::heap::take(ctx) }; - // SAFETY: dev outlives the request - unsafe { - (*ctx.dev) - .server - .as_mut() - .unwrap() - .on_static_request_complete() - }; - drop(ctx); - } - - /// SAFETY: `ctx` must be the pointer returned by `heap::alloc` in `run`. - /// On `Ok` this consumes `ctx` via `finalize`; on `Err` ownership stays - /// with the caller (BodyReaderMixin → `on_error`). - unsafe fn run_with_body( - ctx: *mut UnrefSourceMapRequest, - body: &[u8], - r: AnyResponse, - ) -> crate::Result<()> { +impl bun_uws::BodyReaderHandler for UnrefSourceMapRequest { + fn on_body(&mut self, body: &[u8], r: AnyResponse) -> bun_uws_sys::Result<()> { if body.len() != 8 { - return Err(crate::Error::InvalidRequest); + return Err(crate::Error::InvalidRequest.into()); } let mut generation_bytes = [0u8; 4]; strings::decode_hex_to_bytes(&mut generation_bytes, body) .map_err(|_| crate::Error::InvalidRequest)?; let generation = u32::from_ne_bytes(generation_bytes); let source_map_key = source_map_store::Key::init((generation as u64) << 32); - // SAFETY: ctx is live (caller contract); dev outlives the request; the - // exclusive reborrow is narrowed to `source_maps` for this statement. - let _ = unsafe { &mut (*(*ctx).dev).source_maps }.remove_or_upgrade_weak_ref( - source_map_key, - source_map_store::RemoveOrUpgradeMode::Remove, - ); + self.dev.with_mut(|dev| { + let _ = dev.source_maps.remove_or_upgrade_weak_ref( + source_map_key, + source_map_store::RemoveOrUpgradeMode::Remove, + ); + }); r.write_status(b"204 No Content"); r.end(b"", false); - // SAFETY: ctx is the original heap-allocated pointer; the only borrow - // derived from it points into a separate DevServer allocation and has - // ended. - unsafe { Self::finalize(ctx) }; Ok(()) } } +impl Drop for UnrefSourceMapRequest { + fn drop(&mut self) { + self.server.on_static_request_complete(); + } +} + +impl UnrefSourceMapRequest { + fn run(dev: &mut DevServer, _: &mut Request, resp: &mut R) + where + R: bun_uws_sys::body_reader_mixin::BodyResponse, + { + dev.server.on_pending_request(); + bun_uws::BodyReader::read( + UnrefSourceMapRequest { + dev: bun_ptr::BackRef::new(dev.this().get()), + server: dev.server, + }, + resp, + ); + } +} + #[derive(Default)] pub struct TestingBatch { /// Keys are borrowed. @@ -6346,127 +5577,85 @@ pub(crate) fn get_deinit_count_for_testing() -> usize { } struct PromiseEnsureRouteBundledCtx<'a> { - // Note: raw ptr — `dev` is re-borrowed across the ctx while also passed - // as `&mut` into `ensure_route_is_bundled`. - dev: *mut DevServer, global: &'a JSGlobalObject, + /// The promise handed back to JS: either created here or another handle + /// to the pending bundle's promise. promise: Option, - p: Option<*mut jsc::JSPromise>, // BORROW_FIELD: from sibling self.promise route_bundle_index: route_bundle::Index, + /// The promise was settled synchronously; the caller drains microtasks + /// once the dev server borrow ends. + settled: bool, } impl<'a> PromiseEnsureRouteBundledCtx<'a> { - /// Reborrow the erased `dev` pointer. - /// SAFETY: `self.dev` is set from a live `&mut DevServer` at ctx - /// construction; the ctx is stack-local in the request handler scope. - #[inline] - fn dev_mut(&mut self) -> &mut DevServer { - // SAFETY: `self.dev` was set from a live `&mut DevServer` at ctx - // construction and outlives this stack-local ctx. - unsafe { &mut *self.dev } - } - - /// Reborrow the GC-heap `JSPromise` recorded in `self.p`. Single `unsafe` - /// site for the set-once `p: Option<*mut JSPromise>` self-borrow field; - /// callers (`on_loaded` / `on_plugin_error` / the `to_js()` epilogue in - /// `js_get_route_bundle_promise`) reach this only after `ensure_promise` - /// or `on_defer` has populated `p`. - #[inline] fn promise_mut(&mut self) -> &mut jsc::JSPromise { - // SAFETY: `p` is `Some(strong.get())` after `ensure_promise`/`on_defer`; - // the JSPromise GC cell is rooted for the lifetime of `self` by either - // `self.promise: JSPromiseStrong` or the bundle's `promise.strong` - // (`current_bundle`/`next_bundle`), so the pointee outlives the borrow. - unsafe { &mut *self.p.expect("infallible: promise bound") } + self.promise + .as_ref() + .expect("infallible: promise bound") + .get() } + /// Another strong handle to `self.promise`, creating it if needed. fn ensure_promise(&mut self) -> jsc::JSPromiseStrong { - if self.promise.is_none() { - let strong = jsc::JSPromiseStrong::init(self.global); - self.p = Some(std::ptr::from_mut(strong.get())); - self.promise = Some(strong); + let global = self.global; + let value = self + .promise + .get_or_insert_with(|| jsc::JSPromiseStrong::init(global)) + .value(); + jsc::JSPromiseStrong::from_value(value, global) + } + + fn adopt_or_install(&mut self, promise: &mut DeferredPromise) { + promise + .route_bundle_indices + .put(self.route_bundle_index, ()) + .expect("oom"); + if promise.strong.has_value() { + self.promise = Some(jsc::JSPromiseStrong::from_value( + promise.strong.value(), + self.global, + )); + } else { + promise.strong = self.ensure_promise(); } - // Note: `Strong` owns its slot, so allocate a second - // handle to the same JSPromise — both `self.promise` and the - // returned value root the same cell. - let value = self.promise.as_ref().unwrap().value(); - jsc::JSPromiseStrong::from_value(value, self.global) } +} - fn on_defer(&mut self, bundle_field: BundleQueueType) -> JsResult<()> { - let route_bundle_index = self.route_bundle_index; +impl<'a> EnsureRouteCtx for PromiseEnsureRouteBundledCtx<'a> { + fn on_defer(&mut self, dev: &mut DevServer, bundle_field: BundleQueueType) -> JsResult<()> { match bundle_field { BundleQueueType::CurrentBundle => { - let cb = self - .dev_mut() - .current_bundle - .as_mut() - .expect("infallible: bundle active"); - if cb.promise.strong.has_value() { - cb.promise - .route_bundle_indices - .put(route_bundle_index, ()) - .expect("oom"); - self.p = Some(cb.promise.strong.get()); - return Ok(()); - } - let strong_promise = self.ensure_promise(); - let cb = self - .dev_mut() + let cb = dev .current_bundle .as_mut() .expect("infallible: bundle active"); - cb.promise - .route_bundle_indices - .put(route_bundle_index, ()) - .expect("oom"); - cb.promise.strong = strong_promise; - Ok(()) - } - BundleQueueType::NextBundle => { - if self.dev_mut().next_bundle.promise.strong.has_value() { - self.dev_mut() - .next_bundle - .promise - .route_bundle_indices - .put(route_bundle_index, ()) - .expect("oom"); - self.p = Some(self.dev_mut().next_bundle.promise.strong.get()); - return Ok(()); - } - let strong_promise = self.ensure_promise(); - self.dev_mut() - .next_bundle - .promise - .route_bundle_indices - .put(route_bundle_index, ()) - .expect("oom"); - self.dev_mut().next_bundle.promise.strong = strong_promise; - Ok(()) + self.adopt_or_install(&mut cb.promise); } + BundleQueueType::NextBundle => self.adopt_or_install(&mut dev.next_bundle.promise), } + Ok(()) } - fn on_loaded(&mut self) -> JsResult<()> { + fn on_loaded(&mut self, dev: &mut DevServer) -> JsResult<()> { let _ = self.ensure_promise(); let global = self.global; self.promise_mut().resolve(global, JSValue::TRUE)?; - // SAFETY: dev.vm is JSC_BORROW — valid for DevServer lifetime - self.dev_mut().vm_mut().drain_microtasks(); + let _ = dev; + self.settled = true; Ok(()) } - fn on_plugin_error(&mut self) -> JsResult<()> { + fn on_plugin_error(&mut self, dev: &mut DevServer) -> JsResult<()> { let _ = self.ensure_promise(); let global = self.global; self.promise_mut() .reject(global, BunString::static_("Plugin error").to_js(global))?; - // SAFETY: dev.vm is JSC_BORROW — valid for DevServer lifetime - self.dev_mut().vm_mut().drain_microtasks(); + let _ = dev; + self.settled = true; Ok(()) } - fn to_dev_response(&mut self) -> DevResponse<'_> { + fn to_dev_response(&mut self, _dev: &mut DevServer) -> DevResponse<'_> { DevResponse::Promise(PromiseResponse { promise: self.ensure_promise(), global: self.global, @@ -6474,43 +5663,16 @@ impl<'a> PromiseEnsureRouteBundledCtx<'a> { } } -impl<'a> EnsureRouteCtx for PromiseEnsureRouteBundledCtx<'a> { - fn on_defer(&mut self, bundle_field: BundleQueueType) -> JsResult<()> { - PromiseEnsureRouteBundledCtx::on_defer(self, bundle_field) - } - fn on_loaded(&mut self) -> JsResult<()> { - PromiseEnsureRouteBundledCtx::on_loaded(self) - } - fn on_plugin_error(&mut self) -> JsResult<()> { - PromiseEnsureRouteBundledCtx::on_plugin_error(self) - } - fn to_dev_response(&mut self) -> DevResponse<'_> { - PromiseEnsureRouteBundledCtx::to_dev_response(self) - } -} - -// C++ side declares `extern "C" SYSV_ABI` (BakeAdditionsToGlobalObject.cpp). -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub unsafe fn Bake__bundleNewRouteJSFunctionImpl( - global: &JSGlobalObject, - request_ptr: *mut c_void, - url: &BunString, - ) -> JSValue { - jsc::to_js_host_call(global, || bundle_new_route_js_function_impl(global, request_ptr, url)) - } -} - -fn bundle_new_route_js_function_impl( +/// `import.meta.bakeBundleRoute(request, url)` (BakeAdditionsToGlobalObject.cpp). +// HOST_EXPORT(Bake__bundleNewRouteJSFunctionImpl, jsc) +pub fn bundle_new_route_js_function_impl( global: &JSGlobalObject, - request_ptr: *mut c_void, + request: &mut crate::webcore::Request, url_bunstr: &BunString, ) -> JsResult { let url = url_bunstr.to_utf8(); - // SAFETY: request_ptr is a *bun.webcore.Request from C++ - let request: &mut WebRequest = unsafe { &mut *request_ptr.cast::() }; - let Some(dev) = request.request_context.get().dev_server() else { + let Some(dev_cell) = request.request_context.get().dev_server_cell() else { return Err(global.throw(format_args!( "Request context does not belong to dev server" ))); @@ -6525,58 +5687,50 @@ fn bundle_new_route_js_function_impl( ))); } - let mut params: framework_router::MatchedParams = Default::default(); - let Some(route_index) = dev.router.match_slow(pathname, &mut params) else { - return Err(global.throw(format_args!( - "No route found for path: {}", - bstr::BStr::new(pathname) - ))); - }; - - let _exit = dev.vm().enter_event_loop_scope(); - - let _ = dev; - let Some(dev_ptr) = request.request_context.get().dev_server_mut() else { - return Err(global.throw(format_args!( - "Request context does not belong to dev server" - ))); - }; - // SAFETY: JS-thread single-writer; `dev_server_mut` returns the - // `Box` slot in `NewServer` populated by `set_routes`. - let dev: &mut DevServer = unsafe { &mut *dev_ptr }; - - let route_bundle_index = dev - .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) - .expect("oom"); - let dev_ptr: *mut DevServer = dev; - let mut ctx = PromiseEnsureRouteBundledCtx { - dev: dev_ptr, - global, - promise: None, - p: None, - route_bundle_index, - }; - - let rbi = ctx.route_bundle_index; - // SAFETY: `ctx.dev` aliases the same DevServer. Reborrow via raw ptr to - // satisfy borrowck while ctx is also &mut-borrowed. - ensure_route_is_bundled(unsafe { &mut *dev_ptr }, rbi, &mut ctx)?; - - let array = JSValue::create_empty_array(global, 2)?; + let vm = DevServerCell::get(&dev_cell).vm; + let _exit = vm.enter_event_loop_scope(); + let (route_bundle_index, ctx) = dev_cell.with_mut(|dev| { + let mut params: framework_router::MatchedParams = Default::default(); + let Some(route_index) = dev.router.match_slow(pathname, &mut params) else { + return Err(global.throw(format_args!( + "No route found for path: {}", + bstr::BStr::new(pathname) + ))); + }; - array.put_index(global, 0, JSValue::js_number_from_uint64(rbi.get() as u64))?; + let route_bundle_index = dev + .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) + .expect("oom"); + let mut ctx = PromiseEnsureRouteBundledCtx { + global, + promise: None, + route_bundle_index, + settled: false, + }; - if ctx.p.is_none() { - array.put_index(global, 1, JSValue::UNDEFINED)?; - return Ok(array); + ensure_route_is_bundled(dev, route_bundle_index, &mut ctx)?; + Ok((route_bundle_index, ctx)) + })?; + if ctx.settled { + VirtualMachine::get_mut().drain_microtasks(); + } + { + let array = JSValue::create_empty_array(global, 2)?; + array.put_index( + global, + 0, + JSValue::js_number_from_uint64(route_bundle_index.get() as u64), + )?; + array.put_index( + global, + 1, + match &ctx.promise { + Some(p) => p.get().to_js(), + None => JSValue::UNDEFINED, + }, + )?; + Ok(array) } - - debug_assert!(ctx.p.is_some()); - // Route through the single `promise_mut()` accessor (one audited deref for - // the set-once `p` field) instead of open-coding the raw deref here. - array.put_index(global, 1, ctx.promise_mut().to_js())?; - - Ok(array) } // `JSGlobalObject` is `#[repr(C)]` with `UnsafeCell<[u8; 0]>`; remaining args @@ -6613,17 +5767,9 @@ fn create_dev_server_framework_request_args_object( }) } -#[bun_jsc::host_fn(export = "Bake__getNewRouteParamsJSFunctionImpl")] -fn bake_get_new_route_params_js_function_impl( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> JSValue { - jsc::to_js_host_call(global, || { - new_route_params_for_bundle_promise_for_js(global, callframe) - }) -} - -fn new_route_params_for_bundle_promise_for_js( +/// `import.meta.bakeNewRouteParams(request, routeBundleIndex, url)`. +// HOST_EXPORT(Bake__getNewRouteParamsJSFunctionImpl) +pub fn new_route_params_for_bundle_promise_for_js( global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { @@ -6645,19 +5791,14 @@ fn new_route_params_for_bundle_promise_for_js( return Err(global.throw(format_args!("URL must be a string"))); } - let Some(request_ptr) = ::from_js(request_js) else { + let Some(request) = request_js.as_class_ref::() else { return Err(global.throw(format_args!("Expected a Request object"))); }; - // SAFETY: `from_js` returned a live native pointer; JS holds the GC ref. - let request: &mut WebRequest = unsafe { &mut *request_ptr }; - let Some(dev_ptr) = request.request_context.get().dev_server_mut() else { + let Some(dev_cell) = request.request_context.get().dev_server_cell() else { return Err(global.throw(format_args!( "Request context does not belong to dev server" ))); }; - // SAFETY: JS-thread single-writer; `dev_server_mut` returns the - // `Box` slot in `NewServer` populated by `set_routes`. - let dev: &mut DevServer = unsafe { &mut *dev_ptr }; let route_bundle_index = route_bundle::Index::init( u32::try_from(route_bundle_index_js.to_int32()).expect("int cast"), @@ -6666,7 +5807,9 @@ fn new_route_params_for_bundle_promise_for_js( let url = url_js.to_bun_string(global)?; let url_utf8 = url.to_utf8(); - new_route_params_for_bundle_promise(dev, route_bundle_index, url_utf8.slice()) + dev_cell.with_mut(|dev| { + new_route_params_for_bundle_promise(dev, route_bundle_index, url_utf8.slice()) + }) } fn new_route_params_for_bundle_promise( @@ -6674,20 +5817,7 @@ fn new_route_params_for_bundle_promise( route_bundle_index: route_bundle::Index, url: &[u8], ) -> JsResult { - // Note: erase `dev` so the `route_bundle` / `framework_bundle` - // borrows don't conflict with `dev.router` / `dev.compute_arguments_...`. - let dev_ptr = std::ptr::from_mut::(dev); - // SAFETY: statement-scoped reborrow; the returned `&mut RouteBundle` - // immediately decays to a raw pointer. - let route_bundle: *mut RouteBundle = - unsafe { &mut *dev_ptr }.route_bundle_ptr(route_bundle_index); - // SAFETY: `route_bundle` points into `dev.route_bundles` (not resized in - // this fn); the exclusive borrow decays to a raw pointer at this statement. - let framework_bundle: *mut route_bundle::Framework = match unsafe { &mut (*route_bundle).data } - { - route_bundle::Data::Framework(f) => f, - _ => unreachable!(), - }; + let expected_route_index = dev.framework_bundle_mut(route_bundle_index).route_index; let pathname = extract_pathname_from_url(url); @@ -6699,8 +5829,6 @@ fn new_route_params_for_bundle_promise( bstr::BStr::new(pathname) ))); }; - // SAFETY: statement-scoped read of a `Copy` field of `*framework_bundle`. - let expected_route_index = unsafe { (*framework_bundle).route_index }; if route_index != expected_route_index { return Err(global.throw(format_args!( "Route index mismatch, expected {} but got {}", @@ -6710,19 +5838,8 @@ fn new_route_params_for_bundle_promise( } let params_js_value = params.to_js(global); - // SAFETY: `dev_ptr` is live; `framework_bundle` points into - // `(*dev_ptr).route_bundles[route_bundle_index].data` and its reborrow is - // scoped to this call. Raw-ptr receiver — see Note on - // `compute_arguments_for_framework_request`. - let args = unsafe { - DevServer::compute_arguments_for_framework_request( - dev_ptr, - route_bundle_index, - &mut *framework_bundle, - params_js_value, - false, - ) - }?; + let args = + dev.compute_arguments_for_framework_request(route_bundle_index, params_js_value, false)?; create_dev_server_framework_request_args_object( global, @@ -6765,5 +5882,5 @@ use crate::bake::dev_server::route_bundle; use crate::bake::dev_server::serialized_failure; use crate::bake::dev_server::source_map_store; type DebuggerId = jsc::debugger::DebuggerId; -type BunFrontendDevServerAgent = - crate::bake::dev_server::inspector_agent::BunFrontendDevServerAgent; +type BunFrontendDevServerAgent<'a> = + crate::bake::dev_server::inspector_agent::BunFrontendDevServerAgent<'a>; diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index c5ed15748d3d..f6ae531049e5 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -1165,10 +1165,7 @@ impl Framework { // The bundler crate (lower tier) carries a TYPE_ONLY projection // (`bake_types::Framework`); construct it here and give it arena // lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - // NOTE: interior `Box<[u8]>` in the projection are not dropped by - // bumpalo — bounded per-session, revisit when `bake_types::BuiltInModule` - // is reshaped to `&'a [u8]`. - out.options.framework = Some(&*arena.alloc(self.as_bundler_view())); + out.options.framework = Some(std::sync::Arc::new(self.as_bundler_view())); out.options.inline_entrypoint_import_meta_main = true; if let Some(ignore) = bundler_options.ignore_dce_annotations { out.options.ignore_dce_annotations = ignore; diff --git a/src/runtime/bake/dev_server/assets.rs b/src/runtime/bake/dev_server/assets.rs index e938e2ad17c3..6dfa47e1aad5 100644 --- a/src/runtime/bake/dev_server/assets.rs +++ b/src/runtime/bake/dev_server/assets.rs @@ -5,8 +5,10 @@ use bun_core::{fmt as bun_fmt, scoped_log}; use bun_http::MimeType::MimeType; use bun_ptr::RefPtr; +use super::incremental_graph::IncrementalGraph; use super::memory_cost::{memory_cost_array_hash_map, memory_cost_array_list}; -use super::{ASSET_PREFIX, DevServer, FileKind, Magic}; +use super::{ASSET_PREFIX, DevServer, FileKind}; +use crate::server::AnyServer; use crate::server::StaticRoute; use crate::server::static_route::InitFromBytesOptions; use crate::webcore::AnyBlob; @@ -31,7 +33,6 @@ pub struct Assets { // SAFETY: `Assets` is only ever constructed as the `assets` field of // `DevServer` (which is `Box`-allocated and never moved post-init). -bun_core::impl_field_parent! { Assets => DevServer.assets; fn owner; fn mut owner_mut; } impl Assets { pub(crate) fn get_hash(&self, path: &[u8]) -> Option { @@ -44,15 +45,16 @@ impl Assets { /// browser caching. The old URL is immediately revoked. /// /// `abs_path` is not allocated. Ownership of `contents` is transferred to - /// this function. + /// this function. A new path is also registered in `client_graph`. pub(crate) fn replace_path( &mut self, + client_graph: &mut IncrementalGraph<{ bun_bundler::bake_types::Side::Client }>, + server: AnyServer, abs_path: &[u8], mut contents: AnyBlob, mime_type: &MimeType, content_hash: u64, ) -> Result { - debug_assert!(self.owner().magic == Magic::Valid); // Invariant: `files.count() == refs.len()`, re-checked before each // return below. @@ -69,8 +71,7 @@ impl Assets { bstr::BStr::new(&*mime_type.value), ); - let server = self.owner().server; - debug_assert!(server.is_some()); + let server = Some(server); // `gop` holds key/value ptrs into `path_map` and cannot stay live // across calls that take `&mut self`. Capture `index` / @@ -90,11 +91,7 @@ impl Assets { // `get_or_put` already boxed `abs_path` on insert, so no key // reassignment is needed — `insert_empty` is still called for its // side effect of registering the file in `client_graph`. - let owner = self.owner_mut(); - // SAFETY: accessing disjoint field `client_graph` via parent ptr; `assets` (self) is - // not touched through `owner` for the duration of this borrow. - let _ = - unsafe { &mut (*owner).client_graph }.insert_empty(abs_path, FileKind::Unknown)?; + let _ = client_graph.insert_empty(abs_path, FileKind::Unknown)?; } else { let entry_index = existing_entry.unwrap(); // When there is one reference to the asset, the entry can be diff --git a/src/runtime/bake/dev_server/error_report_request.rs b/src/runtime/bake/dev_server/error_report_request.rs index 13d48cba0e22..a5f722e0a828 100644 --- a/src/runtime/bake/dev_server/error_report_request.rs +++ b/src/runtime/bake/dev_server/error_report_request.rs @@ -31,92 +31,46 @@ use bun_uws::{self as uws, AnyResponse, Request}; use bun_uws_sys::body_reader_mixin::{BodyReaderHandler, BodyResponse}; use super::source_map_store::{self, GetResult, Key as SourceMapKey}; -use super::{CLIENT_PREFIX, DevServer}; +use super::{CLIENT_PREFIX, DevServer, DevServerCell}; use crate::server::StaticRoute; use crate::server::static_route::InitFromBytesOptions; use bun_core::fmt::parse_hex_to_int; pub(crate) struct ErrorReportRequest { - // BACKREF: heap-allocated request; DevServer owns the server lifecycle and - // outlives every in-flight request (BackRef invariant). - dev: bun_ptr::BackRef, - // BodyReaderMixin is a generic helper that stores the buffered body and - // dispatches to the two callbacks below. - body: uws::BodyReaderMixin, + dev: bun_ptr::BackRef, + server: crate::server::AnyServer, } -bun_core::intrusive_field!(ErrorReportRequest, body: uws::BodyReaderMixin); impl BodyReaderHandler for ErrorReportRequest { - unsafe fn on_body(this: *mut Self, body: &[u8], resp: AnyResponse) -> bun_uws_sys::Result<()> { - // SAFETY: caller (BodyReaderMixin) passes the original heap-allocated - // pointer with full-allocation provenance and no live borrows. - unsafe { ErrorReportRequest::run_with_body(this, body, resp) }.map_err(Into::into) + fn on_body(&mut self, body: &[u8], resp: AnyResponse) -> bun_uws_sys::Result<()> { + self.run_with_body(body, resp).map_err(Into::into) } +} - unsafe fn on_error(this: *mut Self) { - // Caller passes the original heap-allocated pointer; finalize - // consumes it via heap::take exactly once. - ErrorReportRequest::finalize(this) +impl Drop for ErrorReportRequest { + fn drop(&mut self) { + self.server.on_static_request_complete(); } } impl ErrorReportRequest { pub(crate) fn run(dev: &mut DevServer, _req: &mut Request, resp: &mut R) { - // Use the caller's `&mut DevServer` directly (matches - // `UnrefSourceMapRequest::run`) — no need to re-derive it through the - // freshly-allocated ctx's `BackRef` under `unsafe`. - dev.server - .as_mut() - .expect("server bound") - .on_pending_request(); - let ctx = bun_core::heap::into_raw(Box::new(ErrorReportRequest { - dev: bun_ptr::BackRef::new_mut(dev), - body: uws::BodyReaderMixin::init(), - })); - uws::BodyReaderMixin::::read_body(ctx, resp); - } - - /// `ctx` must be the pointer returned by `heap::alloc` in `run`; called - /// exactly once (success path here, or via `on_error` on abort/error). - fn finalize(ctx: *mut ErrorReportRequest) { - // SAFETY: `ctx` is the original Box allocation produced by `run`; no - // live borrow of `*ctx` exists (BodyReaderHandler hands us the raw - // pointer, never `&mut self`). Only reachable via `on_body`/`on_error`, - // both of which uphold this contract. - unsafe { - (*ctx) - .dev - .get_mut() - .server - .as_mut() - .unwrap() - .on_static_request_complete(); - drop(bun_core::heap::take(ctx)); - } + dev.server.on_pending_request(); + uws::BodyReader::read( + ErrorReportRequest { + dev: bun_ptr::BackRef::new(dev.this().get()), + server: dev.server, + }, + resp, + ); } - /// SAFETY: `ctx` must be the pointer returned by `heap::alloc` in `run`, - /// with no live `&`/`&mut` into the allocation. On `Ok(())` return this - /// consumes `ctx` via `finalize`; on `Err` the caller (BodyReaderMixin) - /// retains ownership and will call `on_error`. - unsafe fn run_with_body( - ctx: *mut ErrorReportRequest, - body: &[u8], - r: AnyResponse, - ) -> crate::Result<()> { - // .finalize has to be called last, but only in the non-error path. - // On error return, BodyReaderMixin calls `on_error` → `finalize`, so - // here we simply call `finalize` directly at the success tail. - + fn run_with_body(&mut self, body: &[u8], r: AnyResponse) -> crate::Result<()> { let mut reader = bun_io::FixedBufferStream::new(body); let arena = Arena::new(); - // BackRef::get() is safe under the back-reference invariant (DevServer - // outlives this request). No `&mut *ctx` is formed for the body of this - // fn — `finalize(ctx)` at the tail consumes the original Box pointer. - // SAFETY: `ctx` is the live heap allocation from `run` (caller contract). - let dev: &DevServer = unsafe { &*ctx }.dev.get(); + let dev: &DevServer = self.dev.get().get(); // Read payload, assemble ZigException let name = sanitize_for_terminal(read_string32(&mut reader)?, &arena); @@ -320,10 +274,6 @@ impl ErrorReportRequest { // `print_externally_remapped_zig_exception` takes a runtime // `allow_ansi_color` flag. let ansi_colors = Output::enable_ansi_colors_stderr(); - // `dev.vm` is `*const` (shared-ref provenance from `Options.vm`); - // `vm_mut()` recovers `&mut VirtualMachine` via the per-thread - // singleton (`VirtualMachine::get() -> *mut`), which carries - // mutable provenance. Single JS thread — no aliasing `&mut`. let vm = dev.vm_mut(); let _ = vm.print_externally_remapped_zig_exception( &mut exception, @@ -389,15 +339,10 @@ impl ErrorReportRequest { crate::webcore::blob::Any::from_array_list(out), InitFromBytesOptions { mime_type: Some(&bun_http_types::MimeType::OTHER), - server: dev.server, + server: Some(dev.server), ..Default::default() }, ); - // `should_finalize_self = true;` — see Note at fn top. - // `ctx` is the original heap-allocated pointer (caller contract); the - // only borrow derived from it (`dev`) points into a separate DevServer - // allocation, so freeing `*ctx` does not invalidate any live reference. - ErrorReportRequest::finalize(ctx); Ok(()) } } diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 671104b4c823..2db3e70969f5 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -1,13 +1,16 @@ +use core::cell::Cell; + use bun_collections::HashMap; use bun_core::strings; use bun_core::{Output, feature_flags}; +use bun_ptr::{JsCell, ThisPtr}; use bun_uws::AnyWebSocket; use bun_uws_sys::{Opcode, SendStatus}; use crate::timer::EventLoopTimerState; use super::source_map_store::{self, RemoveOrUpgradeMode}; -use super::{ConsoleLogKind, DevServer, HmrTopic, IncomingMessageId, MessageId}; +use super::{ConsoleLogKind, DevServer, DevServerCell, HmrTopic, IncomingMessageId, MessageId}; use crate::bake::dev_server_body::HmrTopicBits; // Struct definition lives in `dev_server/mod.rs` so the public @@ -16,57 +19,51 @@ use crate::bake::dev_server_body::HmrTopicBits; pub(crate) use super::HmrSocket; impl HmrSocket { - pub(crate) fn new(dev: &mut DevServer) -> Box { - Box::new(HmrSocket { - dev: bun_ptr::BackRef::new_mut(dev), - subscriptions: HmrTopicBits::empty(), - active_route: None, - referenced_source_maps: HashMap::default(), - underlying: None, - inspector_connection_id: -1, - }) + pub(crate) fn new(dev: bun_ptr::BackRef) -> HmrSocket { + HmrSocket { + ref_count: Cell::new(1), + dev: Cell::new(Some(dev)), + subscriptions: Cell::new(HmrTopicBits::empty()), + active_route: Cell::new(None), + referenced_source_maps: JsCell::new(HashMap::default()), + underlying: Cell::new(None), + inspector_connection_id: Cell::new(-1), + } } - /// SAFETY: caller must guarantee no other live `&mut DevServer` aliases the - /// returned borrow for its lifetime (BackRef::get_mut exclusivity rule). - /// Liveness is structural: HmrSocket lifetime is strictly nested inside - /// DevServer (the socket is removed from `active_websocket_connections` and - /// destroyed before DevServer is torn down) — the BackRef invariant. - #[inline] - #[allow(clippy::mut_from_ref)] - unsafe fn dev<'a>(&self) -> &'a mut DevServer { - // Detach the borrow from `&self` (explicit unbound `'a`) so callers may - // interleave `self.*` field access with `dev.*` — DevServer is a - // separate heap allocation. - // SAFETY: caller upholds exclusivity; BackRef invariant guarantees liveness. - unsafe { &mut *self.dev.as_ptr() } + /// The owning dev server, or `None` once it has detached this socket + /// (`detach_from_dev_server`) on its way to closing it. + fn dev(&self) -> Option> { + self.dev.get() } - pub(crate) fn on_open(&mut self, ws: AnyWebSocket) { - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. Derived - // via the BackRef accessor (lifetime-detached from `&self`) so we can - // assign `self.underlying` below while `dev` is still live. - let dev = unsafe { self.dev() }; - let mut header = [0u8; 1 + DevServer::CONFIGURATION_HASH_KEY_LEN]; - header[0] = MessageId::Version.char(); - header[1..].copy_from_slice(&dev.configuration_hash_key); - let send_status = ws.send(&header, Opcode::Binary, false, true); - self.underlying = Some(ws); + fn on_open(&self, ws: AnyWebSocket) { + self.underlying.set(Some(ws)); + let Some(dev) = self.dev() else { return }; + dev.with_mut(|dev| { + let mut header = [0u8; 1 + DevServer::CONFIGURATION_HASH_KEY_LEN]; + header[0] = MessageId::Version.char(); + header[1..].copy_from_slice(&dev.configuration_hash_key); + let send_status = ws.send(&header, Opcode::Binary, false, true); - if send_status != SendStatus::Dropped { - // Notify inspector about client connection - if let Some(agent) = dev.inspector() { - self.inspector_connection_id = agent.next_connection_id(); - agent - .notify_client_connected(dev.inspector_server_id, self.inspector_connection_id); + if send_status != SendStatus::Dropped { + // Notify inspector about client connection + if let Some(agent) = dev.inspector() { + self.inspector_connection_id.set(agent.next_connection_id()); + agent.notify_client_connected( + dev.inspector_server_id, + self.inspector_connection_id.get(), + ); + } } - } + }); } - pub(crate) fn on_message(&mut self, ws: AnyWebSocket, msg: &[u8], _opcode: Opcode) { + fn on_message(&self, ws: AnyWebSocket, msg: &[u8], _opcode: Opcode) { if msg.is_empty() { return ws.close(); } + let Some(dev_cell) = self.dev() else { return }; // `msg[0]` may be any byte. Transmuting an out-of-range u8 into a // #[repr(u8)] enum is UB regardless of a wildcard match arm — match on @@ -82,13 +79,13 @@ impl HmrSocket { } let generation = u32::from_ne_bytes(generation_bytes); let source_map_id = source_map_store::Key::init((generation as u64) << 32); - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; - if dev - .source_maps - .remove_or_upgrade_weak_ref(source_map_id, RemoveOrUpgradeMode::Upgrade) - { - self.referenced_source_maps.insert(source_map_id, ()); + let upgraded = dev_cell.with_mut(|dev| { + dev.source_maps + .remove_or_upgrade_weak_ref(source_map_id, RemoveOrUpgradeMode::Upgrade) + }); + if upgraded { + self.referenced_source_maps + .with_mut(|maps| maps.insert(source_map_id, ())); } } x if x == IncomingMessageId::Subscribe as u8 => { @@ -102,16 +99,15 @@ impl HmrSocket { new_bits.insert(topic.as_bit()); } } + let subscriptions = self.subscriptions.get(); for &field in HmrTopic::ALL { let bit = field.as_bit(); - if new_bits.contains(bit) && !self.subscriptions.contains(bit) { + if new_bits.contains(bit) && !subscriptions.contains(bit) { let _ = ws.subscribe(&field.uws_topic()); // on-subscribe hooks if feature_flags::BAKE_DEBUGGING_FEATURES { - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; - match field { + dev_cell.with_mut(|dev| match field { HmrTopic::IncrementalVisualizer => { dev.emit_incremental_visualizer_events += 1; dev.emit_visualizer_message_if_needed(); @@ -124,63 +120,54 @@ impl HmrSocket { dev.memory_visualizer_timer.state != EventLoopTimerState::ACTIVE ); - // Note (jsc/runtime crate cycle): `vm.timer` is `()` on the - // low-tier `VirtualMachine`; the real `timer::All` - // lives in `RuntimeState` (see jsc_hooks.rs). - let state = crate::jsc_hooks::runtime_state(); let next = bun_core::Timespec::ms_from_now( bun_core::TimespecMockMode::ForceRealTime, 1000, ); - // SAFETY: `runtime_state()` is non-null after - // `bun_runtime::init()`; JS-thread only, sole - // `&mut` to `timer` in this scope. - unsafe { - (*state).timer.update( - &raw mut dev.memory_visualizer_timer, - &next, - ); - } + crate::jsc_hooks::timer_all_mut() + .update(&raw mut dev.memory_visualizer_timer, &next); } } _ => {} - } + }); } - } else if new_bits.contains(bit) && !self.subscriptions.contains(bit) { + } else if new_bits.contains(bit) && !subscriptions.contains(bit) { // Note: this `else if` condition is identical to the `if` // above and is therefore unreachable; likely a bug // (intended: `!new && old` → unsubscribe). let _ = ws.unsubscribe(&field.uws_topic()); } } - self.on_unsubscribe(!new_bits & self.subscriptions); - self.subscriptions = new_bits; + dev_cell.with_mut(|dev| self.on_unsubscribe(dev, !new_bits & subscriptions)); + self.subscriptions.set(new_bits); } x if x == IncomingMessageId::SetUrl as u8 => { let pattern = &msg[1..]; - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; - let maybe_rbi = dev.route_to_bundle_index_slow(pattern); - if let Some(agent) = dev.inspector() { - if self.inspector_connection_id > -1 { - let pattern_str = bun_core::String::from_bytes(pattern); - agent.notify_client_navigated( - dev.inspector_server_id, - self.inspector_connection_id, - &pattern_str, - maybe_rbi.map(|i| i.get() as i32).unwrap_or(-1), - ); + let rbi = dev_cell.with_mut(|dev| { + let maybe_rbi = dev.route_to_bundle_index_slow(pattern); + if let Some(agent) = dev.inspector() { + if self.inspector_connection_id.get() > -1 { + let pattern_str = bun_core::String::from_bytes(pattern); + agent.notify_client_navigated( + dev.inspector_server_id, + self.inspector_connection_id.get(), + &pattern_str, + maybe_rbi.map(|i| i.get() as i32).unwrap_or(-1), + ); + } } - } - let Some(rbi) = maybe_rbi else { return }; - if let Some(old) = self.active_route { - if old == rbi { - return; + let rbi = maybe_rbi?; + if let Some(old) = self.active_route.get() { + if old == rbi { + return None; + } + dev.route_bundle_ptr(old).active_viewers -= 1; } - dev.route_bundle_ptr(old).active_viewers -= 1; - } - dev.route_bundle_ptr(rbi).active_viewers += 1; - self.active_route = Some(rbi); + dev.route_bundle_ptr(rbi).active_viewers += 1; + Some(rbi) + }); + let Some(rbi) = rbi else { return }; + self.active_route.set(Some(rbi)); let mut response = [0u8; 5]; response[0] = MessageId::SetUrlResponse.char(); response[1..].copy_from_slice(&rbi.get().to_ne_bytes()); @@ -188,9 +175,7 @@ impl HmrSocket { let _ = ws.send(&response, Opcode::Binary, false, true); } x if x == IncomingMessageId::TestingBatchEvents as u8 => { - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; - match &dev.testing_batch_events { + let close = dev_cell.with_mut(|dev| match &dev.testing_batch_events { super::TestingBatchEvents::Disabled => { if dev.current_bundle.is_some() { dev.testing_batch_events = super::TestingBatchEvents::EnableAfterBundle; @@ -203,21 +188,20 @@ impl HmrSocket { bun_uws::Opcode::BINARY, ); } + false } super::TestingBatchEvents::EnableAfterBundle => { // do not expose a websocket event that panics a release build debug_assert!(false); - ws.close(); + true } super::TestingBatchEvents::Enabled(_event_const) => { - // Replace-and-extract to satisfy borrowck. - let super::TestingBatchEvents::Enabled(mut event) = core::mem::replace( + let super::TestingBatchEvents::Enabled(event) = core::mem::replace( &mut dev.testing_batch_events, super::TestingBatchEvents::Disabled, ) else { unreachable!() }; - let _ = &mut event; if event.entry_points.set.count() == 0 { dev.publish( @@ -225,16 +209,17 @@ impl HmrSocket { &[MessageId::TestingWatchSynchronization.char(), 2], bun_uws::Opcode::BINARY, ); - return; + return false; } let timer = std::time::Instant::now(); dev.start_async_bundle(event.entry_points, true, timer) - // bun.handleOom(err) — Rust aborts on OOM by default .expect("OOM"); - - // `event.entry_points.deinit(allocator)` → Drop handles this + false } + }); + if close { + ws.close(); } } x if x == IncomingMessageId::ConsoleLog as u8 => { @@ -253,8 +238,7 @@ impl HmrSocket { }; let data = &msg[2..]; - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; + let dev: &DevServer = DevServerCell::get(&dev_cell); if let Some(agent) = dev.inspector() { let log_str = bun_core::String::from_bytes(data); @@ -284,24 +268,24 @@ impl HmrSocket { return ws.close(); }; let source_map_id = source_map_store::Key::init(u64::from_le_bytes(bytes)); - let Some(kv) = self.referenced_source_maps.remove_entry(&source_map_id) else { + let removed = self + .referenced_source_maps + .with_mut(|maps| maps.remove_entry(&source_map_id)); + let Some(kv) = removed else { bun_core::debug_warn!( "unref_source_map: no entry found: {:x}\n", source_map_id.get() ); return; // no entry may happen. }; - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - unsafe { self.dev() }.source_maps.unref(kv.0); + dev_cell.with_mut(|dev| dev.source_maps.unref(kv.0)); } _ => ws.close(), } } - fn on_unsubscribe(&mut self, field: HmrTopicBits) { + fn on_unsubscribe(&self, dev: &mut DevServer, field: HmrTopicBits) { if feature_flags::BAKE_DEBUGGING_FEATURES { - // SAFETY: JS-thread only; sole `&mut DevServer` for this scope. - let dev = unsafe { self.dev() }; if field.contains(HmrTopic::IncrementalVisualizer.as_bit()) { dev.emit_incremental_visualizer_events -= 1; } @@ -310,56 +294,73 @@ impl HmrSocket { if dev.emit_incremental_visualizer_events == 0 && dev.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { - // Note (jsc/runtime crate cycle): `vm.timer` is `()` on the low-tier - // `VirtualMachine`; the real `timer::All` lives in `RuntimeState`. - let state = crate::jsc_hooks::runtime_state(); - // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; - // JS-thread only, sole `&mut` to `timer` in this scope. - unsafe { - (*state).timer.remove(&raw mut dev.memory_visualizer_timer); - } + crate::jsc_hooks::timer_all_mut().remove(&raw mut dev.memory_visualizer_timer); } } } } - /// # Safety - /// `s` must be a valid, uniquely-owned `HmrSocket` heap pointer (allocated - /// via `HmrSocket::new`'s caller). uws guarantees the socket context - /// pointer is valid for the duration of the close callback; this function - /// consumes ownership and frees it. - pub(crate) fn on_close(s: *mut HmrSocket, _ws: AnyWebSocket, _exit_code: i32, _message: &[u8]) { - // SAFETY: caller contract above. - let this = unsafe { &mut *s }; + /// Everything `on_close` does to the dev server, so that + /// `Drop for DevServer` can run it itself before closing the socket (its + /// `on_close` then finds itself already removed and leaves `dev` alone). + pub(crate) fn detach_from_dev_server(&self, dev: &mut DevServer) { + self.dev.set(None); + self.on_unsubscribe(dev, self.subscriptions.replace(HmrTopicBits::empty())); - let subs = this.subscriptions; - this.on_unsubscribe(subs); - - // SAFETY: JS-thread only; the `on_unsubscribe` borrow above has been - // released, so this is the sole `&mut DevServer` for the remainder. - let dev = unsafe { this.dev() }; - if this.inspector_connection_id > -1 { + if self.inspector_connection_id.get() > -1 { // Notify inspector about client disconnection if let Some(agent) = dev.inspector() { agent.notify_client_disconnected( dev.inspector_server_id, - this.inspector_connection_id, + self.inspector_connection_id.get(), ); } + self.inspector_connection_id.set(-1); } - if let Some(old) = this.active_route { + if let Some(old) = self.active_route.take() { dev.route_bundle_ptr(old).active_viewers -= 1; } - for key in this.referenced_source_maps.keys() { + for key in self + .referenced_source_maps + .replace(HashMap::default()) + .keys() + { dev.source_maps.unref(*key); } - // referenced_source_maps.deinit(allocator) → Drop on HashMap (below) - let removed = dev.active_websocket_connections.remove(&s); - debug_assert!(removed.is_some()); - // SAFETY: `s` was heap-allocated in `new()`'s caller; this is the sole - // owner reclaiming it. Matches `s.dev.arena().destroy(s)`. - drop(unsafe { bun_core::heap::take(s) }); + } + + fn on_close(&self, _ws: AnyWebSocket, _exit_code: i32, _message: &[u8]) { + // `None` when `Drop for DevServer` is the one closing this socket. + let Some(dev) = self.dev() else { return }; + dev.with_mut(|dev| { + let removed = dev + .active_websocket_connections + .remove(&(std::ptr::from_ref(self) as usize)); + debug_assert!(removed.is_some()); + self.detach_from_dev_server(dev); + if let Some(map_ref) = removed { + map_ref.deref(); + } + }); + } +} + +impl bun_uws_sys::web_socket::WebSocketHandlerRef for HmrSocket { + // `Wrap.apply` leaves the drain/ping/pong C callbacks `null` when + // `HAS_ON_* == false`. + const HAS_ON_DRAIN: bool = false; + const HAS_ON_PING: bool = false; + const HAS_ON_PONG: bool = false; + + fn on_open(this: ThisPtr, ws: AnyWebSocket) { + this.get().on_open(ws) + } + fn on_message(this: ThisPtr, ws: AnyWebSocket, message: &[u8], opcode: Opcode) { + this.get().on_message(ws, message, opcode) + } + fn on_close(this: ThisPtr, ws: AnyWebSocket, code: i32, message: &[u8]) { + this.get().on_close(ws, code, message) } } diff --git a/src/runtime/bake/dev_server/incremental_graph.rs b/src/runtime/bake/dev_server/incremental_graph.rs index 4b4ccc882199..1a1edbaf5f6c 100644 --- a/src/runtime/bake/dev_server/incremental_graph.rs +++ b/src/runtime/bake/dev_server/incremental_graph.rs @@ -8,12 +8,12 @@ //! payload is folded into a single struct carrying both field sets and //! per-side behaviour is dispatched on the `SIDE` const parameter. -use core::mem::offset_of; use std::io::Write as _; use bun_collections::{ArrayHashMap, StringArrayHashMap, bit_set::DynamicBitSetUnmanaged}; use bun_core::strings; +use super::assets::Assets; use super::{ CLIENT_PREFIX, ChunkKind, DevServer, EntryPointList, FileKind, GraphTraceState, SerializedFailure, TraceImportGoal, packed_map, route_bundle, serialized_failure, @@ -307,49 +307,6 @@ pub struct GraphMemoryCost { } impl IncrementalGraph { - /// `@fieldParentPtr(@tagName(side) ++ "_graph", g)` — recover the owning - /// `DevServer` from this inline field. Returns a raw pointer because the - /// caller already holds `&mut self` (a sub-borrow of `*dev`); forming - /// `&mut DevServer` would alias. Callers must only touch *sibling* fields. - /// - /// SAFETY: `self` must be the `client_graph` / `server_graph` field of a - /// heap-allocated `DevServer` (guaranteed by `Box` ownership). - #[inline] - unsafe fn owner(&mut self) -> *mut DevServer { - let offset = match SIDE { - Side::Client => offset_of!(DevServer, client_graph), - Side::Server => offset_of!(DevServer, server_graph), - }; - // SAFETY: `self` is the `_graph` field of `DevServer`. - unsafe { bun_core::container_of::(std::ptr::from_mut(self), offset) } - } - - /// Safe sibling-projection: borrow the owning [`DevServer`]'s - /// `incremental_result` while holding `&mut self`. The two fields are - /// disjoint, so the returned `&mut` does not alias `self`. - #[inline] - fn dev_incremental_result(&mut self) -> &mut super::IncrementalResult { - // SAFETY: `owner()` recovers the heap-allocated `DevServer`; - // `incremental_result` is field-disjoint from both `client_graph` and - // `server_graph`, so the returned borrow and `&mut self` cover - // non-overlapping memory. - unsafe { &mut (*self.owner()).incremental_result } - } - - /// Safe sibling-projection: borrow the owning [`DevServer`]'s - /// `bundling_failures` while holding `&mut self` (same disjoint-field - /// rationale as [`dev_incremental_result`](Self::dev_incremental_result)). - #[inline] - fn dev_bundling_failures( - &mut self, - ) -> &mut ArrayHashMap { - // SAFETY: `owner()` recovers the heap-allocated `DevServer`; - // `bundling_failures` is field-disjoint from both `client_graph` and - // `server_graph`, so the returned borrow and `&mut self` cover - // non-overlapping memory. - unsafe { &mut (*self.owner()).bundling_failures } - } - /// `IncrementalGraph(side).getFileByIndex` — direct value-slot accessor. #[inline] pub(crate) fn get_file_by_index(&self, index: FileIndex) -> &File { @@ -446,7 +403,7 @@ impl IncrementalGraph { /// `IncrementalGraph(side).freeFileContent` (client only). /// Frees the file's `source_map` + `content`, optionally unref'ing the /// associated CSS asset. Leaves `content = .Unknown`. - fn free_file_content(&mut self, key: &[u8], file: &mut File, css: FreeCssMode) { + fn free_file_content(assets: &mut Assets, key: &[u8], file: &mut File, css: FreeCssMode) { debug_assert!(matches!(SIDE, Side::Client)); let _ = file.source_map.take(); // Rc drop releases backing PackedMap match core::mem::replace(&mut file.content, Content::Unknown) { @@ -455,8 +412,7 @@ impl IncrementalGraph { } Content::CssRoot(_) | Content::CssChild => { if css == FreeCssMode::UnrefCss { - // SAFETY: see `owner()`; touches `assets` sibling only. - unsafe { (*self.owner()).assets.unref_by_path(key) }; + assets.unref_by_path(key); } } Content::Unknown => {} @@ -509,6 +465,7 @@ impl IncrementalGraph { fn disconnect_and_delete_file( &mut self, directory_watchers: &mut super::DirectoryWatchStore, + watcher: &mut bun_watcher::Watcher, file_index: FileIndex, ) { let lists = &mut self.edge_lists[file_index.get() as usize]; @@ -526,8 +483,10 @@ impl IncrementalGraph { // DirectoryWatchStore.Dep.source_file_path borrows this key; remove // any such dependencies before freeing it so they do not dangle. - directory_watchers - .remove_dependencies_for_file(&self.bundled_files.keys()[file_index.get() as usize]); + directory_watchers.remove_dependencies_for_file( + watcher, + &self.bundled_files.keys()[file_index.get() as usize], + ); // Free the key string and tombstone the slot. Cannot swap-remove since // FrameworkRouter / SerializedFailure hold FileIndices into this graph. @@ -539,1171 +498,1340 @@ impl IncrementalGraph { } // ──────────────────────────────────────────────────────────────────────── - // receiveChunk + // insertStale / insertEmpty / insertCssFileOnServer / insertFailure // ──────────────────────────────────────────────────────────────────────── - /// Tracks a bundled code chunk for cross-bundle chunks, ensuring it has an - /// entry in `bundled_files`. For client, takes ownership of the code slice; - /// for server, the code is kept in `current_chunk_code` until - /// `take_js_bundle` consumes it. - pub(crate) fn receive_chunk( + /// `IncrementalGraph(side).insertStale` — adds a file to the graph in the + /// stale state without bundled content. Thin forwarder (spec :1295). + pub(crate) fn insert_stale( &mut self, - ctx: &mut HotUpdateContext<'_>, - index: impl Into, - content: ReceiveChunkContent, - is_ssr_graph: bool, - ) -> Result<(), crate::Error> { - let index: bun_ast::Index = index.into(); - // SAFETY: see `owner()`. - let dev = unsafe { self.owner() }; - // SAFETY: `graph_safety_lock` is a sibling field; debug-assert only. - unsafe { (*dev).graph_safety_lock.assert_locked() }; - - let path = &ctx.sources[index.get() as usize].path; - let key = path.key_for_incremental_graph(); - - if cfg!(debug_assertions) { - if let ReceiveChunkContent::Js { code, .. } = &content { - if strings::is_all_whitespace(code) { - bun_core::Output::panic(format_args!( - "Empty chunk is impossible: {} {}", - bstr::BStr::new(key), - match SIDE { - Side::Client => "client", - Side::Server => - if is_ssr_graph { - "ssr" - } else { - "server" - }, - }, - )); - } - } - } + assets: &mut Assets, + abs_path: &[u8], + graph: bake::Graph, + ) -> Result, bun_alloc::AllocError> { + self.insert_stale_extra(assets, abs_path, graph, RouteKind::NotRoute) + } - let gop = self.bundled_files.get_or_put(key)?; - let file_index = FileIndex::::init(gop.index as u32); + /// `IncrementalGraph(side).insertStaleExtra` (spec :1300). `assets` is + /// where a replaced client CSS file's asset is released. + pub(crate) fn insert_stale_extra( + &mut self, + assets: &mut Assets, + abs_path: &[u8], + graph: bake::Graph, + route: RouteKind, + ) -> Result, bun_alloc::AllocError> { + debug_assert!(match SIDE { + Side::Client => graph == bake::Graph::Client, + Side::Server => graph != bake::Graph::Client, + }); + let gop = self.bundled_files.get_or_put(abs_path)?; + let idx = gop.index; let found_existing = gop.found_existing; - if !found_existing { - *gop.key_ptr = Box::<[u8]>::from(key); + if found_existing { + if matches!(SIDE, Side::Server) && route == RouteKind::Route { + gop.value_ptr.is_route = true; + } + } else { + *gop.key_ptr = Box::<[u8]>::from(abs_path); } - // Note: drop `gop` borrow before pushing to other Vecs / re-borrowing. if !found_existing { self.edge_lists.push(EdgeLists::default()); } - - if self.stale_files.bit_length > file_index.get() as usize { - self.stale_files.unset(file_index.get() as usize); + if self.stale_files.bit_length > idx { + self.stale_files.set(idx); } - *ctx.get_cached_index(SIDE, index) = - CachedFileIndex::from(Some::>(file_index)); - match SIDE { Side::Client => { - let mut html_route_bundle_index: Option = None; - let mut is_special_framework_file = false; - if found_existing { - // Note: take the existing slot out so `free_file_content` - // can borrow `&mut self` while we hold the `File` by value. - let mut existing = core::mem::take( - &mut self.bundled_files.values_mut()[file_index.get() as usize], - ); - self.free_file_content(key, &mut existing, FreeCssMode::IgnoreCss); - - if existing.failed { - let owner = - serialized_failure::OwnerPacked::new(Side::Client, file_index.get()); - let kv = self.dev_bundling_failures().fetch_swap_remove(&owner); - let kv = kv.unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Missing SerializedFailure in IncrementalGraph", - )) - }); - self.dev_incremental_result().failures_removed.push(kv.1); - } - - html_route_bundle_index = existing.html_route_bundle_index; - is_special_framework_file = existing.is_special_framework_file; - } - - let (new_content, new_source_map, code_len) = match content { - ReceiveChunkContent::Css(css) => { - (Content::CssRoot(css), packed_map::Shared::None, None) - } - ReceiveChunkContent::Js { code, source_map } => { - let len = code.len(); - let kind = if ctx.loaders[index.get() as usize].is_javascript_like() { - Content::Js(code) - } else { - Content::Asset(code) - }; - if source_map.is_some() { - debug_assert!(html_route_bundle_index.is_none()); // suspect behind #17956 - } - let sm = match source_map { - Some(mut sm) if sm.chunk.buffer.len() > 0 => { - packed_map::Shared::Some(packed_map::PackedMap::new_non_empty( - &mut sm.chunk, - sm.escaped_source.take().expect("escaped_source"), - )) - } - _ => { - // Must precompute line count so source-map - // concatenation knows how many newlines to skip. - let count = match &kind { - Content::Js(c) | Content::Asset(c) => { - strings::count_char(&c[..], b'\n') as u32 - } - _ => 0, - }; - packed_map::Shared::LineCount(packed_map::LineCount::init(count)) - } - }; - (kind, sm, Some(len)) - } - }; - - self.bundled_files.values_mut()[file_index.get() as usize] = File { - kind: new_content.kind(), - failed: false, - is_rsc: false, - is_ssr: false, - is_client_component_boundary: false, - is_route: false, - is_hmr_root: ctx.server_to_client_bitset.is_set(index.get() as usize), - is_special_framework_file, - html_route_bundle_index, - source_map: new_source_map, - content: new_content, - }; - - if let Some(len) = code_len { - self.current_chunk_parts.push(file_index); - self.current_chunk_len += len; + let mut existing = core::mem::take(&mut self.bundled_files.values_mut()[idx]); + let key = &self.bundled_files.keys()[idx]; + Self::free_file_content(assets, key, &mut existing, FreeCssMode::UnrefCss); + existing.kind = FileKind::Unknown; + self.bundled_files.values_mut()[idx] = existing; + } else { + self.bundled_files.values_mut()[idx] = File::default(); } } Side::Server => { - let new_kind = match &content { - ReceiveChunkContent::Js { .. } => FileKind::Js, - ReceiveChunkContent::Css(_) => FileKind::Css, - }; + let is_ssr_graph = graph == bake::Graph::Ssr; if !found_existing { - let scb = ctx.server_to_client_bitset.is_set(index.get() as usize); - self.bundled_files.values_mut()[file_index.get() as usize] = File { - kind: new_kind, + self.bundled_files.values_mut()[idx] = File { + kind: FileKind::Unknown, failed: false, is_rsc: !is_ssr_graph, is_ssr: is_ssr_graph, - is_client_component_boundary: scb, - is_route: false, + is_route: route == RouteKind::Route, + is_client_component_boundary: false, ..Default::default() }; - if scb { - self.dev_incremental_result() - .client_components_added - .push(ServerFileIndex::init(file_index.get())); - } + } else if is_ssr_graph { + self.bundled_files.values_mut()[idx].is_ssr = true; } else { - let scb = ctx.server_to_client_bitset.is_set(index.get() as usize); - { - let f = &mut self.bundled_files.values_mut()[file_index.get() as usize]; - f.kind = new_kind; - if is_ssr_graph { - f.is_ssr = true; - } else { - f.is_rsc = true; - } - } - if scb { - self.bundled_files.values_mut()[file_index.get() as usize] - .is_client_component_boundary = true; - self.dev_incremental_result() - .client_components_added - .push(ServerFileIndex::init(file_index.get())); - } else if self.bundled_files.values()[file_index.get() as usize] - .is_client_component_boundary - { - // SAFETY: cross-graph access via `owner()`. We hold - // `&mut self` (server_graph); `client_graph` and - // `directory_watchers` are disjoint sibling fields. - let (client_graph, directory_watchers) = - unsafe { (&mut (*dev).client_graph, &mut (*dev).directory_watchers) }; - let key = bun_ptr::RawSlice::new( - &*self.bundled_files.keys()[file_index.get() as usize], - ); - let client_index = - client_graph.get_file_index(key.slice()).unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Client graph's SCB was already deleted", - )) - }); - client_graph.disconnect_and_delete_file(directory_watchers, client_index); - self.bundled_files.values_mut()[file_index.get() as usize] - .is_client_component_boundary = false; - self.dev_incremental_result() - .client_components_removed - .push(ServerFileIndex::init(file_index.get())); - } - - if self.bundled_files.values()[file_index.get() as usize].failed { - self.bundled_files.values_mut()[file_index.get() as usize].failed = false; - let owner = - serialized_failure::OwnerPacked::new(Side::Server, file_index.get()); - let kv = self.dev_bundling_failures().fetch_swap_remove(&owner); - let kv = kv.unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Missing failure in IncrementalGraph", - )) - }); - self.dev_incremental_result().failures_removed.push(kv.1); - } - } - - if let ReceiveChunkContent::Js { code, source_map } = content { - let code_len = code.len(); - let line_count = strings::count_char(&code, b'\n') as u32; - self.current_chunk_code.push(code); - self.current_chunk_len += code_len; - - let packed = match source_map { - Some(mut sm) - if sm.chunk.buffer.len() > 0 && sm.escaped_source.is_some() => - { - packed_map::Shared::Some(packed_map::PackedMap::new_non_empty( - &mut sm.chunk, - sm.escaped_source.take().unwrap(), - )) - } - _ => packed_map::Shared::LineCount(packed_map::LineCount::init(line_count)), - }; - self.current_chunk_source_maps - .push(CurrentChunkSourceMapData { - file_index: ServerFileIndex::init(file_index.get()), - source_map: packed, - }); + self.bundled_files.values_mut()[idx].is_rsc = true; } } } - Ok(()) + Ok(FileIndex::init(idx as u32)) } - // ──────────────────────────────────────────────────────────────────────── - // processChunkDependencies - // ──────────────────────────────────────────────────────────────────────── - - /// Second pass of IncrementalGraph indexing: updates dependency information - /// for each file and resolves what the HMR roots are. - pub(crate) fn process_chunk_dependencies( + /// `IncrementalGraph(side).insertEmpty(abs_path, kind)` (spec :1354). + pub(crate) fn insert_empty( &mut self, - ctx: &mut HotUpdateContext<'_>, - mode: ProcessMode, - bundle_graph_index: impl Into, - ) -> Result<(), crate::Error> { - let bundle_graph_index: bun_ast::Index = bundle_graph_index.into(); - let file_index: FileIndex = ctx - .get_cached_index(SIDE, bundle_graph_index) - .unwrap::() - .expect("unresolved index"); // do not process for failed chunks - - // Build a map from the existing import list. Later, entries that - // were not marked as `.seen = true` will be freed. - let mut quick_lookup: ArrayHashMap, TempLookup> = ArrayHashMap::default(); - { - let mut it = self.edge_lists[file_index.get() as usize].first_import; - while let Some(edge_index) = it { - let dep = self.edges[edge_index.get() as usize]; - it = dep.next_import; - debug_assert_eq!(dep.dependency.get(), file_index.get()); - quick_lookup.put_no_clobber( - dep.imported, - TempLookup { - seen: false, - edge_index, + abs_path: &[u8], + kind: FileKind, + ) -> Result, bun_alloc::AllocError> { + let gop = self.bundled_files.get_or_put(abs_path)?; + let idx = gop.index; + let found_existing = gop.found_existing; + if !found_existing { + *gop.key_ptr = Box::<[u8]>::from(abs_path); + *gop.value_ptr = match SIDE { + Side::Client => File { + kind, + content: match kind { + FileKind::Unknown => Content::Unknown, + FileKind::Js => Content::Js(Box::default()), + FileKind::Asset => Content::Asset(Box::default()), + FileKind::Css => Content::CssChild, }, - )?; - } - } - - // `process_chunk_import_records` appends new entries (always seen=true). - // Snapshot the length so the new ones are ignored when removing edges. - let quick_lookup_values_to_care_len = quick_lookup.count(); - - // A new import linked list is constructed from scratch. - let mut new_imports: Option = None; - - if mode == ProcessMode::Normal && matches!(SIDE, Side::Server) { - if ctx.server_seen_bit_set.is_set(file_index.get() as usize) { - self.edge_lists[file_index.get() as usize].first_import = new_imports; - return Ok(()); - } - // RSC+SSR dual-index dispatch (`ctx.scbs.getSSRIndex`) is - // intentionally not implemented. - } - - match mode { - ProcessMode::Normal => self.process_chunk_import_records( - ctx, - &mut quick_lookup, - &mut new_imports, - file_index, - bundle_graph_index, - )?, - ProcessMode::Css => self.process_css_chunk_import_records( - ctx, - &mut quick_lookup, - &mut new_imports, - file_index, - bundle_graph_index, - )?, + ..Default::default() + }, + Side::Server => File { + kind, + ..Default::default() + }, + }; } - - // Clear the old head before freeing so `check_edge_removal` wouldn't - // see a stale reference. - self.edge_lists[file_index.get() as usize].first_import = None; - - // '.seen = false' means an import was removed and should be freed. - for val in &quick_lookup.values()[0..quick_lookup_values_to_care_len] { - if !val.seen { - self.dev_incremental_result().had_adjusted_edges = true; - self.disconnect_edge_from_dependency_list(val.edge_index); - self.free_edge(val.edge_index); - } + // Capture the interned-key fat ptr now so the `gop` borrow on + // `bundled_files` ends before `ensure_stale_bit_capacity` reborrows. + let key = bun_ptr::RawSlice::new(&**gop.key_ptr); + if !found_existing { + self.edge_lists.push(EdgeLists::default()); + self.ensure_stale_bit_capacity(true)?; } - - self.edge_lists[file_index.get() as usize].first_import = new_imports; - - // Follow this file to the route / HMR root to mark it stale. - self.trace_dependencies( - file_index, - ctx.gts, - TraceDependencyGoal::StopAtBoundary, - file_index, - ) + Ok(InsertEmptyResult { + index: FileIndex::init(idx as u32), + key, + }) } - fn process_chunk_import_records( + /// `IncrementalGraph(.server).insertCssFileOnServer` (spec :1390). + /// Server CSS files are just targets for graph traversal; content lives + /// only on the client. + pub(crate) fn insert_css_file_on_server( &mut self, ctx: &mut HotUpdateContext<'_>, - quick_lookup: &mut ArrayHashMap, TempLookup>, - new_imports: &mut Option, - file_index: FileIndex, index: bun_ast::Index, + abs_path: &[u8], ) -> Result<(), crate::Error> { - debug_assert!(index.is_valid()); - debug_assert!(!ctx.loaders[index.get() as usize].is_css()); - - let records_len = ctx.import_records[index.get() as usize].len(); - for i in 0..records_len { - // Note: snapshot the three fields we need so the shared borrow - // on `ctx.import_records` ends before `process_edge_attachment` - // takes `&mut ctx`. - let (flags, src, key) = { - let ir = &ctx.import_records[index.get() as usize].as_slice()[i]; - ( - ir.flags, - ir.source_index, - ir.path.key_for_incremental_graph(), - ) - }; - let _ = self.process_edge_attachment( - ctx, - quick_lookup, - new_imports, - file_index, - flags, - src, - key, - EdgeAttachmentMode::JsOrHtml, - )?; + debug_assert!(matches!(SIDE, Side::Server)); + let gop = self.bundled_files.get_or_put(abs_path)?; + let file_index = FileIndex::::init(gop.index as u32); + let found_existing = gop.found_existing; + if !found_existing { + *gop.key_ptr = Box::<[u8]>::from(abs_path); } + *gop.value_ptr = File { + kind: FileKind::Css, + ..Default::default() + }; + if !found_existing { + self.edge_lists.push(EdgeLists::default()); + } + *ctx.get_cached_index(Side::Server, index) = + CachedFileIndex::from(Some::>(file_index)); Ok(()) } - fn process_css_chunk_import_records( + // ──────────────────────────────────────────────────────────────────────── + // onFileDeleted / invalidate + // ──────────────────────────────────────────────────────────────────────── + + /// `IncrementalGraph(side).onFileDeleted` (spec :1528). + pub(crate) fn on_file_deleted( &mut self, - ctx: &mut HotUpdateContext<'_>, - quick_lookup: &mut ArrayHashMap, TempLookup>, - new_imports: &mut Option, - file_index: FileIndex, - bundler_index: bun_ast::Index, - ) -> Result<(), crate::Error> { - debug_assert!(bundler_index.is_valid()); - debug_assert!(ctx.loaders[bundler_index.get() as usize].is_css()); + abs_path: &[u8], + bv2: &mut bun_bundler::BundleV2<'_>, + ) -> Result<(), bun_alloc::AllocError> { + let Some(index) = self.get_file_index(abs_path) else { + return Ok(()); + }; - // Queue avoids stack overflow; tracing bits in `process_edge_attachment` - // prevent infinite recursion. - let mut queue: Vec = Vec::new(); - queue.push(bundler_index); + // Disconnect all imports. + let mut it = self.edge_lists[index.get() as usize].first_import.take(); + while let Some(edge_index) = it { + let dep = self.edges[edge_index.get() as usize]; + it = dep.next_import; + debug_assert_eq!(dep.dependency.get(), index.get()); + self.disconnect_edge_from_dependency_list(edge_index); + self.free_edge(edge_index); + } - while let Some(idx) = queue.pop() { - let records_len = ctx.import_records[idx.get() as usize].len(); - for i in 0..records_len { - let (flags, src, key) = { - let ir = &ctx.import_records[idx.get() as usize].as_slice()[i]; - ( - ir.flags, - ir.source_index, - ir.path.key_for_incremental_graph(), - ) - }; - let result = self.process_edge_attachment( - ctx, - quick_lookup, - new_imports, - file_index, - flags, - src, - key, - EdgeAttachmentMode::Css, - )?; - if result == EdgeAttachmentResult::Continue && src.is_valid() { - queue.push(src); - } - } + // Rebuild all dependencies. + let target = match SIDE { + Side::Client => bun_ast::Target::Browser, + Side::Server => bun_ast::Target::Bun, + }; + let mut it = self.edge_lists[index.get() as usize].first_dep; + while let Some(edge_index) = it { + let dep = self.edges[edge_index.get() as usize]; + it = dep.next_dependency; + debug_assert_eq!(dep.imported.get(), index.get()); + let key = &self.bundled_files.keys()[dep.dependency.get() as usize]; + let loader = self.bundled_files.values()[dep.dependency.get() as usize] + .html_route_bundle_index + .is_some() + .then_some(bun_ast::Loader::Html); + bun_core::handle_oom( + bv2.enqueue_file_from_dev_server_incremental_graph_invalidation( + key, target, loader, + ), + ); + } + + // Bust the resolution cache of the dir containing this file. + let dirname = bun_paths::dirname(abs_path).unwrap_or(abs_path); + let _ = bv2.transpiler.resolver.bust_dir_cache(dirname); + + // Clear the cached entry from the path→source-index maps. + for map in bv2.graph.build_graphs.values_mut() { + map.remove(abs_path); } Ok(()) } - #[allow(clippy::too_many_arguments)] - fn process_edge_attachment( - &mut self, - ctx: &mut HotUpdateContext<'_>, - quick_lookup: &mut ArrayHashMap, TempLookup>, - new_imports: &mut Option, - file_index: FileIndex, - ir_flags: bun_ast::ImportRecordFlags, - ir_source_index: bun_ast::Index, - key: &[u8], - mode: EdgeAttachmentMode, - ) -> Result { - // Duplicated import records are marked unused by `ConvertESMExportsForHmr`. - if ir_flags.contains(bun_ast::ImportRecordFlags::IS_UNUSED) { - return Ok(EdgeAttachmentResult::Stop); - } - if ir_source_index.is_runtime() { - return Ok(EdgeAttachmentResult::Stop); + /// See `EntryPointList::append_html`. + fn append_client_entry_point( + &self, + entry_points: &mut EntryPointList, + index: usize, + ) -> Result<(), crate::Error> { + debug_assert!(matches!(SIDE, Side::Client)); + let abs_path = &self.bundled_files.keys()[index]; + if self.bundled_files.values()[index] + .html_route_bundle_index + .is_some() + { + entry_points.append_html(abs_path) + } else { + entry_points.append_js(abs_path, bake::Graph::Client) } + } - // Locate the FileIndex from bundle_v2's Source.Index. - let (imported_file_index, kind): (FileIndex, FileKind) = 'brk: { - if ir_source_index.is_valid() { - let kind = if mode == EdgeAttachmentMode::Css { - if ctx.loaders[ir_source_index.get() as usize].is_css() { - FileKind::Css - } else { - FileKind::Asset + /// `IncrementalGraph(side).invalidate` (spec :1589). Given a set of paths, + /// mark the relevant files as stale and append them into `entry_points`. + pub(crate) fn invalidate( + &mut self, + paths: &[Box<[u8]>], + entry_points: &mut EntryPointList, + ) -> Result<(), crate::Error> { + for path in paths { + let Some(index) = self.bundled_files.get_index(path) else { + continue; + }; + self.stale_files.set(index); + // Store the graph-owned key, not the incoming `path` (which may be + // freed before `entry_points` is consumed). + // Note: re-derive via `RawSlice` so the immutable key borrow + // does not conflict with the `&mut entry_points` push below. + let owned_path = bun_ptr::RawSlice::new(&*self.bundled_files.keys()[index]); + let owned_path = owned_path.slice(); + match SIDE { + Side::Client => match &self.bundled_files.values()[index].content { + Content::CssRoot(_) | Content::CssChild => { + if matches!( + self.bundled_files.values()[index].content, + Content::CssRoot(_), + ) { + entry_points.append_css(owned_path)?; + } + let mut it = self.edge_lists[index].first_dep; + while let Some(edge_index) = it { + let entry = self.edges[edge_index.get() as usize]; + let dep = entry.dependency; + self.stale_files.set(dep.get() as usize); + if matches!( + self.bundled_files.values()[dep.get() as usize].content, + Content::CssRoot(_), + ) { + let k = bun_ptr::RawSlice::new( + &*self.bundled_files.keys()[dep.get() as usize], + ); + entry_points.append_css(k.slice())?; + } + it = entry.next_dependency; + } + } + Content::Asset(_) => { + let mut it = self.edge_lists[index].first_dep; + while let Some(edge_index) = it { + let entry = self.edges[edge_index.get() as usize]; + let dep = entry.dependency; + self.stale_files.set(dep.get() as usize); + let k = bun_ptr::RawSlice::new( + &*self.bundled_files.keys()[dep.get() as usize], + ); + let k = k.slice(); + if matches!( + self.bundled_files.values()[dep.get() as usize].content, + Content::CssRoot(_), + ) { + entry_points.append_css(k)?; + } else { + self.append_client_entry_point(entry_points, dep.get() as usize)?; + } + it = entry.next_dependency; + } + self.append_client_entry_point(entry_points, index)?; + } + // When re-bundling SCBs, only bundle the server. + Content::Js(_) | Content::Unknown => { + if !self.bundled_files.values()[index].is_hmr_root { + self.append_client_entry_point(entry_points, index)?; + } } - } else { - FileKind::Unknown - }; - if let Some(i) = ctx.get_cached_index(SIDE, ir_source_index).unwrap::() { - break 'brk (i, kind); - } else if mode == EdgeAttachmentMode::Css { - let index = self.insert_empty(key, kind)?.index; - ctx.gts.resize(SIDE, index.get() as usize + 1)?; - break 'brk (index, kind); - } - } - match mode { - // Invalid source indices in CSS are external URLs. - EdgeAttachmentMode::Css => return Ok(EdgeAttachmentResult::Stop), - // Check IncrementalGraph for a file from a prior build. - EdgeAttachmentMode::JsOrHtml => match self.bundled_files.get_index(key) { - Some(i) => (FileIndex::::init(i as u32), FileKind::Unknown), - None => return Ok(EdgeAttachmentResult::Continue), }, + Side::Server => { + let f = &self.bundled_files.values()[index]; + if f.is_rsc { + entry_points.append_js(owned_path, bake::Graph::Server)?; + } + if f.is_ssr && !f.is_client_component_boundary { + entry_points.append_js(owned_path, bake::Graph::Ssr)?; + } + } } - }; + } + Ok(()) + } - debug_assert!((imported_file_index.get() as usize) < self.bundled_files.count()); + // ──────────────────────────────────────────────────────────────────────── + // takeJSBundle / takeSourceMap + // ──────────────────────────────────────────────────────────────────────── - // For CSS visiting CSS, prevent infinite recursion via tracing bits. - if mode == EdgeAttachmentMode::Css && kind == FileKind::Css { - if ctx - .gts - .bits(SIDE) - .is_set(imported_file_index.get() as usize) - { - return Ok(EdgeAttachmentResult::Stop); - } - ctx.gts.bits(SIDE).set(imported_file_index.get() as usize); - } + /// `IncrementalGraph(.server).takeJSBundle` — server-side overload. + pub(crate) fn take_js_bundle_server( + &mut self, + opts: &TakeJSBundleOptionsServer, + ) -> Result, crate::Error> { + let mut chunk = Vec::new(); + self.take_js_bundle_to_list_server(&mut chunk, opts)?; + Ok(chunk) + } - let gop = quick_lookup.get_or_put(imported_file_index)?; - if gop.found_existing { - if gop.value_ptr.seen { - return Ok(EdgeAttachmentResult::Continue); - } - gop.value_ptr.seen = true; - let ei = gop.value_ptr.edge_index; - self.edges[ei.get() as usize].next_import = *new_imports; - *new_imports = Some(ei); - } else { - // A new edge is needed to represent the dependency and import. - let first_dep = self.edge_lists[imported_file_index.get() as usize].first_dep; - let edge = self.new_edge(Edge { - next_import: *new_imports, - next_dependency: first_dep, - prev_dependency: None, - imported: imported_file_index, - dependency: file_index, - })?; - if let Some(dep) = first_dep { - self.edges[dep.get() as usize].prev_dependency = Some(edge); - } - *new_imports = Some(edge); - self.edge_lists[imported_file_index.get() as usize].first_dep = Some(edge); + /// `IncrementalGraph(.server).takeJSBundleToList` (spec :1713). + fn take_js_bundle_to_list_server( + &mut self, + list: &mut Vec, + options: &TakeJSBundleOptionsServer, + ) -> Result<(), crate::Error> { + debug_assert!(matches!(SIDE, Side::Server)); + debug_assert!(self.current_chunk_len > 0); - self.dev_incremental_result().had_adjusted_edges = true; + let runtime: bake::HmrRuntime = match options.kind { + ChunkKind::InitialResponse => bake::get_hmr_runtime(Side::Server), + ChunkKind::HmrChunk => bake::HmrRuntime { + code: bun_core::ZStr::from_static(b"({\0"), + line_count: 0, + }, + }; + // Server `.InitialResponse` is unreachable per spec; only HmrChunk hits + // the end-builder. + let end: &[u8] = b"})"; - *gop.value_ptr = TempLookup { - edge_index: edge, - seen: true, - }; + let runtime_code = runtime.code.as_bytes(); + let start = list.len(); + list.reserve_exact(self.current_chunk_len + runtime_code.len() + end.len()); + list.extend_from_slice(runtime_code); + for code in &self.current_chunk_code { + list.extend_from_slice(code); } - Ok(EdgeAttachmentResult::Continue) - } + list.extend_from_slice(end); - // ──────────────────────────────────────────────────────────────────────── - // traceDependencies / traceImports - // ──────────────────────────────────────────────────────────────────────── + let _ = start; + Ok(()) + } - /// Walks dependents (importers) outward from `file_index`, marking - /// visited files in `gts` to find the routes/HMR roots affected by a - /// change. - pub(crate) fn trace_dependencies( + /// `IncrementalGraph(side).takeSourceMap` (spec :1843). + /// Fills in all fields of `out` except `ref_count`. + pub(crate) fn take_source_map( &mut self, - file_index: FileIndex, - gts: &mut GraphTraceState, - goal: TraceDependencyGoal, - from_file_index: FileIndex, + out: &mut source_map_store::Entry, ) -> Result<(), crate::Error> { - if gts.bits(SIDE).is_set(file_index.get() as usize) { - return Ok(()); - } - gts.bits(SIDE).set(file_index.get() as usize); - - // SAFETY: see `owner()`. - let dev = unsafe { self.owner() }; - + let paths = self.bundled_files.keys(); match SIDE { - Side::Server => { - let (is_route, is_scb) = { - let file = &self.bundled_files.values()[file_index.get() as usize]; - (file.is_route, file.is_client_component_boundary) - }; - if is_route { - // SAFETY: sibling-field access. - let route_index = unsafe { - (*dev) - .route_lookup - .get(&ServerFileIndex::init(file_index.get())) - } - .copied() - .unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Route not in lookup index: {} {:?}", - file_index.get(), - bstr::BStr::new(&self.bundled_files.keys()[file_index.get() as usize]), - )) - }); - self.dev_incremental_result() - .framework_routes_affected - .push(route_index); - } - if is_scb { - self.dev_incremental_result() - .client_components_affected - .push(ServerFileIndex::init(file_index.get())); - } - } Side::Client => { - let (is_hmr_root, html_rbi) = { - let file = &self.bundled_files.values()[file_index.get() as usize]; - (file.is_hmr_root, file.html_route_bundle_index) - }; - if is_hmr_root { - let key = bun_ptr::RawSlice::new( - &*self.bundled_files.keys()[file_index.get() as usize], - ); - // SAFETY: cross-graph sibling access; `server_graph` disjoint. - let server_graph = unsafe { &mut (*dev).server_graph }; - let index = server_graph.get_file_index(key.slice()).unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Server Incremental Graph is missing component for {:?}", - bstr::BStr::new(key.slice()), - )) - }); - server_graph.trace_dependencies(index, gts, goal, index)?; - } else if let Some(route_bundle_index) = html_rbi { - // HTML modified or asset modified → hard reload; else soft. - let hard = from_file_index == file_index - || matches!( - self.bundled_files.values()[from_file_index.get() as usize].content, - Content::Asset(_), - ); - let ir = self.dev_incremental_result(); - if hard { - ir.html_routes_hard_affected.push(route_bundle_index); - } else { - ir.html_routes_soft_affected.push(route_bundle_index); - } - if goal == TraceDependencyGoal::StopAtBoundary { - return Ok(()); + let mut file_paths: Vec> = + Vec::with_capacity(self.current_chunk_parts.len()); + let mut contained_maps: Vec = + Vec::with_capacity(self.current_chunk_parts.len()); + let mut overlapping_memory_cost: usize = 0; + + for file_index in &self.current_chunk_parts { + // Borrowing slice headers from `bundled_files.keys()` + // would be self-referential w.r.t. `DevServer`, so own a + // copy. Paths are short and source-map entries are + // infrequent — profile if hot. + file_paths.push(Box::<[u8]>::from(&*paths[file_index.get() as usize])); + let sm = self.bundled_files.values()[file_index.get() as usize] + .source_map + .clone(); + if let Some(map) = sm.get() { + overlapping_memory_cost += map.memory_cost(); } + contained_maps.push(sm); } - } - } + overlapping_memory_cost += contained_maps.capacity() + * core::mem::size_of::() + + file_paths.len() * core::mem::size_of::>(); - // Certain files do not propagate updates to dependencies. - if goal == TraceDependencyGoal::StopAtBoundary { - if self.bundled_files.values()[file_index.get() as usize].stops_dependency_trace(SIDE) { - return Ok(()); + let ref_count = out.ref_count; + *out = source_map_store::Entry { + ref_count, + paths: file_paths.into_boxed_slice(), + files: contained_maps, + overlapping_memory_cost: overlapping_memory_cost as u32, + }; } - } + Side::Server => { + let mut file_paths: Vec> = + Vec::with_capacity(self.current_chunk_source_maps.len()); + let mut contained_maps: Vec = + Vec::with_capacity(self.current_chunk_source_maps.len()); + let mut overlapping_memory_cost: u32 = 0; - // Recurse. - let mut it = self.edge_lists[file_index.get() as usize].first_dep; - while let Some(dep_index) = it { - let edge = self.edges[dep_index.get() as usize]; - it = edge.next_dependency; - self.trace_dependencies(edge.dependency, gts, goal, file_index)?; + for item in &self.current_chunk_source_maps { + file_paths.push(Box::<[u8]>::from(&*paths[item.file_index.get() as usize])); + contained_maps.push(item.source_map.clone()); + overlapping_memory_cost += item.source_map.memory_cost() as u32; + } + overlapping_memory_cost += (contained_maps.capacity() + * core::mem::size_of::() + + file_paths.len() * core::mem::size_of::>()) + as u32; + + *out = source_map_store::Entry { + ref_count: out.ref_count, + paths: file_paths.into_boxed_slice(), + files: contained_maps, + overlapping_memory_cost, + }; + } } Ok(()) } +} - /// Walks imports inward from `file_index`, marking visited files in - /// `gts` to collect everything a bundle for this file must include. - pub(crate) fn trace_imports( - &mut self, - file_index: FileIndex, - gts: &mut GraphTraceState, - goal: TraceImportGoal, - ) -> Result<(), crate::Error> { - if gts.bits(SIDE).is_set(file_index.get() as usize) { - return Ok(()); +// ────────────────────────────────────────────────────────────────────────── +// GraphRef — a graph plus the sibling `DevServer` state its methods reach +// ────────────────────────────────────────────────────────────────────────── + +/// The `DevServer` fields an [`IncrementalGraph`] method touches besides the +/// graph itself, borrowed alongside it (see `DevServer::client_graph_mut` / +/// `server_graph_mut`). `client_graph`/`server_graph` hold the *other* graph +/// (the one not in [`GraphRef::g`]); it is `None` while a call has crossed +/// over into it. +pub(crate) struct GraphSiblings<'a> { + pub(crate) client_graph: Option<&'a mut IncrementalGraph<{ Side::Client }>>, + pub(crate) server_graph: Option<&'a mut IncrementalGraph<{ Side::Server }>>, + pub(crate) incremental_result: &'a mut super::IncrementalResult, + pub(crate) bundling_failures: + &'a mut ArrayHashMap, + pub(crate) assets: &'a mut Assets, + pub(crate) directory_watchers: &'a mut super::DirectoryWatchStore, + pub(crate) bun_watcher: &'a mut bun_watcher::Watcher, + pub(crate) route_lookup: &'a ArrayHashMap, + pub(crate) root: &'a [u8], + pub(crate) configuration_hash_key: &'a [u8; DevServer::CONFIGURATION_HASH_KEY_LEN], + pub(crate) graph_safety_lock: &'a bun_safety::ThreadLock, +} + +macro_rules! cross_graph { + ($self:ident, $field:ident, $msg:literal) => { + GraphRef { + g: $self.$field.as_deref_mut().expect($msg), + s: GraphSiblings { + client_graph: None, + server_graph: None, + incremental_result: &mut *$self.incremental_result, + bundling_failures: &mut *$self.bundling_failures, + assets: &mut *$self.assets, + directory_watchers: &mut *$self.directory_watchers, + bun_watcher: &mut *$self.bun_watcher, + route_lookup: $self.route_lookup, + root: $self.root, + configuration_hash_key: $self.configuration_hash_key, + graph_safety_lock: $self.graph_safety_lock, + }, } - gts.bits(SIDE).set(file_index.get() as usize); + }; +} - // SAFETY: see `owner()`. - let dev = unsafe { self.owner() }; +impl GraphSiblings<'_> { + /// Cross into the server graph (from a client-graph method). + fn server(&mut self) -> GraphRef<'_, { Side::Server }> { + cross_graph!(self, server_graph, "client graph reaches the server graph") + } - match SIDE { - Side::Server => { - let (is_scb, kind, failed) = { - let f = &self.bundled_files.values()[file_index.get() as usize]; - (f.is_client_component_boundary, f.kind, f.failed) - }; - if is_scb || kind == FileKind::Css { - let key = bun_ptr::RawSlice::new( - &*self.bundled_files.keys()[file_index.get() as usize], - ); - // SAFETY: disjoint sibling `client_graph`. - let client_graph = unsafe { &mut (*dev).client_graph }; - let index = client_graph.get_file_index(key.slice()).unwrap_or_else(|| { - bun_core::Output::panic(format_args!( - "Client Incremental Graph is missing component for {:?}", - bstr::BStr::new(key.slice()), - )) - }); - client_graph.trace_imports(index, gts, goal)?; + /// Cross into the client graph (from a server-graph method). + fn client(&mut self) -> GraphRef<'_, { Side::Client }> { + cross_graph!(self, client_graph, "server graph reaches the client graph") + } +} - if cfg!(debug_assertions) && kind == FileKind::Css { - // Server CSS files never have imports. - let lists = &self.edge_lists[file_index.get() as usize]; - debug_assert!(lists.first_import.is_none()); - } - } - if goal == TraceImportGoal::FindErrors && failed { - let owner = - serialized_failure::OwnerPacked::new(Side::Server, file_index.get()); - let fail = self - .dev_bundling_failures() - .get(&owner) - .cloned() - .expect("Failed to get bundling failure"); - self.dev_incremental_result().failures_added.push(fail); - } - } - Side::Client => { - { - let f = &self.bundled_files.values()[file_index.get() as usize]; - match &f.content { - Content::CssChild => { - debug_assert!(false, "only CSS roots should be found by tracing"); - } - Content::CssRoot(id) => { - if goal == TraceImportGoal::FindCss { - self.current_css_files.push(*id); - } - // CSS can't import JS; trace is done. - return Ok(()); - } - _ => {} - } - } +/// An [`IncrementalGraph`] borrowed together with its [`GraphSiblings`]; the +/// receiver for the graph operations that update dev-server-wide state. +pub(crate) struct GraphRef<'a, const SIDE: Side> { + pub(crate) g: &'a mut IncrementalGraph, + pub(crate) s: GraphSiblings<'a>, +} - if goal == TraceImportGoal::FindClientModules { - let len = self.bundled_files.values()[file_index.get() as usize] - .content - .js_code() - .map(|c| c.len()) - .unwrap_or(0); - self.current_chunk_parts.push(file_index); - self.current_chunk_len += len; - } +impl GraphRef<'_, SIDE> { + // ──────────────────────────────────────────────────────────────────────── + // receiveChunk + // ──────────────────────────────────────────────────────────────────────── - if goal == TraceImportGoal::FindErrors - && self.bundled_files.values()[file_index.get() as usize].failed - { - let owner = - serialized_failure::OwnerPacked::new(Side::Client, file_index.get()); - let fail = self - .dev_bundling_failures() - .get(&owner) - .cloned() - .expect("Failed to get bundling failure"); - self.dev_incremental_result().failures_added.push(fail); - return Ok(()); + /// Tracks a bundled code chunk for cross-bundle chunks, ensuring it has an + /// entry in `bundled_files`. For client, takes ownership of the code slice; + /// for server, the code is kept in `current_chunk_code` until + /// `take_js_bundle` consumes it. + pub(crate) fn receive_chunk( + &mut self, + ctx: &mut HotUpdateContext<'_>, + index: impl Into, + content: ReceiveChunkContent, + is_ssr_graph: bool, + ) -> Result<(), crate::Error> { + let index: bun_ast::Index = index.into(); + self.s.graph_safety_lock.assert_locked(); + + let path = &ctx.sources[index.get() as usize].path; + let key = path.key_for_incremental_graph(); + + if cfg!(debug_assertions) { + if let ReceiveChunkContent::Js { code, .. } = &content { + if strings::is_all_whitespace(code) { + bun_core::Output::panic(format_args!( + "Empty chunk is impossible: {} {}", + bstr::BStr::new(key), + match SIDE { + Side::Client => "client", + Side::Server => + if is_ssr_graph { + "ssr" + } else { + "server" + }, + }, + )); } } } - // Recurse. - let mut it = self.edge_lists[file_index.get() as usize].first_import; - while let Some(dep_index) = it { - let edge = self.edges[dep_index.get() as usize]; - it = edge.next_import; - self.trace_imports(edge.imported, gts, goal)?; - } - Ok(()) - } - - // ──────────────────────────────────────────────────────────────────────── - // insertStale / insertEmpty / insertCssFileOnServer / insertFailure - // ──────────────────────────────────────────────────────────────────────── - - /// `IncrementalGraph(side).insertStale` — adds a file to the graph in the - /// stale state without bundled content. Thin forwarder (spec :1295). - pub(crate) fn insert_stale( - &mut self, - abs_path: &[u8], - graph: bake::Graph, - ) -> Result, bun_alloc::AllocError> { - self.insert_stale_extra(abs_path, graph, RouteKind::NotRoute) - } - - /// `IncrementalGraph(side).insertStaleExtra` (spec :1300). - pub(crate) fn insert_stale_extra( - &mut self, - abs_path: &[u8], - graph: bake::Graph, - route: RouteKind, - ) -> Result, bun_alloc::AllocError> { - debug_assert!(match SIDE { - Side::Client => graph == bake::Graph::Client, - Side::Server => graph != bake::Graph::Client, - }); - let gop = self.bundled_files.get_or_put(abs_path)?; - let idx = gop.index; + let gop = self.g.bundled_files.get_or_put(key)?; + let file_index = FileIndex::::init(gop.index as u32); let found_existing = gop.found_existing; - if found_existing { - if matches!(SIDE, Side::Server) && route == RouteKind::Route { - gop.value_ptr.is_route = true; - } - } else { - *gop.key_ptr = Box::<[u8]>::from(abs_path); + if !found_existing { + *gop.key_ptr = Box::<[u8]>::from(key); } + // Note: drop `gop` borrow before pushing to other Vecs / re-borrowing. if !found_existing { - self.edge_lists.push(EdgeLists::default()); + self.g.edge_lists.push(EdgeLists::default()); } - if self.stale_files.bit_length > idx { - self.stale_files.set(idx); + + if self.g.stale_files.bit_length > file_index.get() as usize { + self.g.stale_files.unset(file_index.get() as usize); } + *ctx.get_cached_index(SIDE, index) = + CachedFileIndex::from(Some::>(file_index)); + match SIDE { Side::Client => { + let mut html_route_bundle_index: Option = None; + let mut is_special_framework_file = false; + if found_existing { - let mut existing = core::mem::take(&mut self.bundled_files.values_mut()[idx]); - // Note: re-derive owned key via `RawSlice` so - // `free_file_content` can borrow `&mut self`. - let key = bun_ptr::RawSlice::new(&*self.bundled_files.keys()[idx]); - self.free_file_content(key.slice(), &mut existing, FreeCssMode::UnrefCss); - existing.kind = FileKind::Unknown; - self.bundled_files.values_mut()[idx] = existing; - } else { - self.bundled_files.values_mut()[idx] = File::default(); + // Note: take the existing slot out so `free_file_content` + // can borrow `&mut self` while we hold the `File` by value. + let mut existing = core::mem::take( + &mut self.g.bundled_files.values_mut()[file_index.get() as usize], + ); + IncrementalGraph::::free_file_content( + self.s.assets, + key, + &mut existing, + FreeCssMode::IgnoreCss, + ); + + if existing.failed { + let owner = + serialized_failure::OwnerPacked::new(Side::Client, file_index.get()); + let kv = self.s.bundling_failures.fetch_swap_remove(&owner); + let kv = kv.unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Missing SerializedFailure in IncrementalGraph", + )) + }); + self.s.incremental_result.failures_removed.push(kv.1); + } + + html_route_bundle_index = existing.html_route_bundle_index; + is_special_framework_file = existing.is_special_framework_file; + } + + let (new_content, new_source_map, code_len) = match content { + ReceiveChunkContent::Css(css) => { + (Content::CssRoot(css), packed_map::Shared::None, None) + } + ReceiveChunkContent::Js { code, source_map } => { + let len = code.len(); + let kind = if ctx.loaders[index.get() as usize].is_javascript_like() { + Content::Js(code) + } else { + Content::Asset(code) + }; + if source_map.is_some() { + debug_assert!(html_route_bundle_index.is_none()); // suspect behind #17956 + } + let sm = match source_map { + Some(mut sm) if sm.chunk.buffer.len() > 0 => { + packed_map::Shared::Some(packed_map::PackedMap::new_non_empty( + &mut sm.chunk, + sm.escaped_source.take().expect("escaped_source"), + )) + } + _ => { + // Must precompute line count so source-map + // concatenation knows how many newlines to skip. + let count = match &kind { + Content::Js(c) | Content::Asset(c) => { + strings::count_char(&c[..], b'\n') as u32 + } + _ => 0, + }; + packed_map::Shared::LineCount(packed_map::LineCount::init(count)) + } + }; + (kind, sm, Some(len)) + } + }; + + self.g.bundled_files.values_mut()[file_index.get() as usize] = File { + kind: new_content.kind(), + failed: false, + is_rsc: false, + is_ssr: false, + is_client_component_boundary: false, + is_route: false, + is_hmr_root: ctx.server_to_client_bitset.is_set(index.get() as usize), + is_special_framework_file, + html_route_bundle_index, + source_map: new_source_map, + content: new_content, + }; + + if let Some(len) = code_len { + self.g.current_chunk_parts.push(file_index); + self.g.current_chunk_len += len; } } Side::Server => { - let is_ssr_graph = graph == bake::Graph::Ssr; + let new_kind = match &content { + ReceiveChunkContent::Js { .. } => FileKind::Js, + ReceiveChunkContent::Css(_) => FileKind::Css, + }; if !found_existing { - self.bundled_files.values_mut()[idx] = File { - kind: FileKind::Unknown, + let scb = ctx.server_to_client_bitset.is_set(index.get() as usize); + self.g.bundled_files.values_mut()[file_index.get() as usize] = File { + kind: new_kind, failed: false, is_rsc: !is_ssr_graph, is_ssr: is_ssr_graph, - is_route: route == RouteKind::Route, - is_client_component_boundary: false, + is_client_component_boundary: scb, + is_route: false, ..Default::default() }; - } else if is_ssr_graph { - self.bundled_files.values_mut()[idx].is_ssr = true; + if scb { + self.s + .incremental_result + .client_components_added + .push(ServerFileIndex::init(file_index.get())); + } } else { - self.bundled_files.values_mut()[idx].is_rsc = true; + let scb = ctx.server_to_client_bitset.is_set(index.get() as usize); + { + let f = &mut self.g.bundled_files.values_mut()[file_index.get() as usize]; + f.kind = new_kind; + if is_ssr_graph { + f.is_ssr = true; + } else { + f.is_rsc = true; + } + } + if scb { + self.g.bundled_files.values_mut()[file_index.get() as usize] + .is_client_component_boundary = true; + self.s + .incremental_result + .client_components_added + .push(ServerFileIndex::init(file_index.get())); + } else if self.g.bundled_files.values()[file_index.get() as usize] + .is_client_component_boundary + { + let client_graph = self + .s + .client_graph + .as_deref_mut() + .expect("server graph reaches the client graph"); + let key = &self.g.bundled_files.keys()[file_index.get() as usize]; + let client_index = client_graph.get_file_index(key).unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Client graph's SCB was already deleted", + )) + }); + client_graph.disconnect_and_delete_file( + self.s.directory_watchers, + self.s.bun_watcher, + client_index, + ); + self.g.bundled_files.values_mut()[file_index.get() as usize] + .is_client_component_boundary = false; + self.s + .incremental_result + .client_components_removed + .push(ServerFileIndex::init(file_index.get())); + } + + if self.g.bundled_files.values()[file_index.get() as usize].failed { + self.g.bundled_files.values_mut()[file_index.get() as usize].failed = false; + let owner = + serialized_failure::OwnerPacked::new(Side::Server, file_index.get()); + let kv = self.s.bundling_failures.fetch_swap_remove(&owner); + let kv = kv.unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Missing failure in IncrementalGraph", + )) + }); + self.s.incremental_result.failures_removed.push(kv.1); + } + } + + if let ReceiveChunkContent::Js { code, source_map } = content { + let code_len = code.len(); + let line_count = strings::count_char(&code, b'\n') as u32; + self.g.current_chunk_code.push(code); + self.g.current_chunk_len += code_len; + + let packed = match source_map { + Some(mut sm) + if sm.chunk.buffer.len() > 0 && sm.escaped_source.is_some() => + { + packed_map::Shared::Some(packed_map::PackedMap::new_non_empty( + &mut sm.chunk, + sm.escaped_source.take().unwrap(), + )) + } + _ => packed_map::Shared::LineCount(packed_map::LineCount::init(line_count)), + }; + self.g + .current_chunk_source_maps + .push(CurrentChunkSourceMapData { + file_index: ServerFileIndex::init(file_index.get()), + source_map: packed, + }); } } } - Ok(FileIndex::init(idx as u32)) - } + Ok(()) + } + + // ──────────────────────────────────────────────────────────────────────── + // processChunkDependencies + // ──────────────────────────────────────────────────────────────────────── + + /// Second pass of IncrementalGraph indexing: updates dependency information + /// for each file and resolves what the HMR roots are. + pub(crate) fn process_chunk_dependencies( + &mut self, + ctx: &mut HotUpdateContext<'_>, + mode: ProcessMode, + bundle_graph_index: impl Into, + ) -> Result<(), crate::Error> { + let bundle_graph_index: bun_ast::Index = bundle_graph_index.into(); + let file_index: FileIndex = ctx + .get_cached_index(SIDE, bundle_graph_index) + .unwrap::() + .expect("unresolved index"); // do not process for failed chunks + + // Build a map from the existing import list. Later, entries that + // were not marked as `.seen = true` will be freed. + let mut quick_lookup: ArrayHashMap, TempLookup> = ArrayHashMap::default(); + { + let mut it = self.g.edge_lists[file_index.get() as usize].first_import; + while let Some(edge_index) = it { + let dep = self.g.edges[edge_index.get() as usize]; + it = dep.next_import; + debug_assert_eq!(dep.dependency.get(), file_index.get()); + quick_lookup.put_no_clobber( + dep.imported, + TempLookup { + seen: false, + edge_index, + }, + )?; + } + } + + // `process_chunk_import_records` appends new entries (always seen=true). + // Snapshot the length so the new ones are ignored when removing edges. + let quick_lookup_values_to_care_len = quick_lookup.count(); + + // A new import linked list is constructed from scratch. + let mut new_imports: Option = None; + + if mode == ProcessMode::Normal && matches!(SIDE, Side::Server) { + if ctx.server_seen_bit_set.is_set(file_index.get() as usize) { + self.g.edge_lists[file_index.get() as usize].first_import = new_imports; + return Ok(()); + } + // RSC+SSR dual-index dispatch (`ctx.scbs.getSSRIndex`) is + // intentionally not implemented. + } - /// `IncrementalGraph(side).insertEmpty(abs_path, kind)` (spec :1354). - pub(crate) fn insert_empty( - &mut self, - abs_path: &[u8], - kind: FileKind, - ) -> Result, bun_alloc::AllocError> { - let gop = self.bundled_files.get_or_put(abs_path)?; - let idx = gop.index; - let found_existing = gop.found_existing; - if !found_existing { - *gop.key_ptr = Box::<[u8]>::from(abs_path); - *gop.value_ptr = match SIDE { - Side::Client => File { - kind, - content: match kind { - FileKind::Unknown => Content::Unknown, - FileKind::Js => Content::Js(Box::default()), - FileKind::Asset => Content::Asset(Box::default()), - FileKind::Css => Content::CssChild, - }, - ..Default::default() - }, - Side::Server => File { - kind, - ..Default::default() - }, - }; + match mode { + ProcessMode::Normal => self.process_chunk_import_records( + ctx, + &mut quick_lookup, + &mut new_imports, + file_index, + bundle_graph_index, + )?, + ProcessMode::Css => self.process_css_chunk_import_records( + ctx, + &mut quick_lookup, + &mut new_imports, + file_index, + bundle_graph_index, + )?, } - // Capture the interned-key fat ptr now so the `gop` borrow on - // `bundled_files` ends before `ensure_stale_bit_capacity` reborrows. - let key = bun_ptr::RawSlice::new(&**gop.key_ptr); - if !found_existing { - self.edge_lists.push(EdgeLists::default()); - self.ensure_stale_bit_capacity(true)?; + + // Clear the old head before freeing so `check_edge_removal` wouldn't + // see a stale reference. + self.g.edge_lists[file_index.get() as usize].first_import = None; + + // '.seen = false' means an import was removed and should be freed. + for val in &quick_lookup.values()[0..quick_lookup_values_to_care_len] { + if !val.seen { + self.s.incremental_result.had_adjusted_edges = true; + self.g.disconnect_edge_from_dependency_list(val.edge_index); + self.g.free_edge(val.edge_index); + } } - Ok(InsertEmptyResult { - index: FileIndex::init(idx as u32), - key, - }) + + self.g.edge_lists[file_index.get() as usize].first_import = new_imports; + + // Follow this file to the route / HMR root to mark it stale. + self.trace_dependencies( + file_index, + ctx.gts, + TraceDependencyGoal::StopAtBoundary, + file_index, + ) } - /// `IncrementalGraph(.server).insertCssFileOnServer` (spec :1390). - /// Server CSS files are just targets for graph traversal; content lives - /// only on the client. - pub(crate) fn insert_css_file_on_server( + fn process_chunk_import_records( &mut self, ctx: &mut HotUpdateContext<'_>, + quick_lookup: &mut ArrayHashMap, TempLookup>, + new_imports: &mut Option, + file_index: FileIndex, index: bun_ast::Index, - abs_path: &[u8], ) -> Result<(), crate::Error> { - debug_assert!(matches!(SIDE, Side::Server)); - let gop = self.bundled_files.get_or_put(abs_path)?; - let file_index = FileIndex::::init(gop.index as u32); - let found_existing = gop.found_existing; - if !found_existing { - *gop.key_ptr = Box::<[u8]>::from(abs_path); - } - *gop.value_ptr = File { - kind: FileKind::Css, - ..Default::default() - }; - if !found_existing { - self.edge_lists.push(EdgeLists::default()); + debug_assert!(index.is_valid()); + debug_assert!(!ctx.loaders[index.get() as usize].is_css()); + + let records_len = ctx.import_records[index.get() as usize].len(); + for i in 0..records_len { + // Note: snapshot the three fields we need so the shared borrow + // on `ctx.import_records` ends before `process_edge_attachment` + // takes `&mut ctx`. + let (flags, src, key) = { + let ir = &ctx.import_records[index.get() as usize].as_slice()[i]; + ( + ir.flags, + ir.source_index, + ir.path.key_for_incremental_graph(), + ) + }; + let _ = self.process_edge_attachment( + ctx, + quick_lookup, + new_imports, + file_index, + flags, + src, + key, + EdgeAttachmentMode::JsOrHtml, + )?; } - *ctx.get_cached_index(Side::Server, index) = - CachedFileIndex::from(Some::>(file_index)); Ok(()) } - /// `IncrementalGraph(side).insertFailure` (spec :1419). - pub(crate) fn insert_failure( + fn process_css_chunk_import_records( &mut self, - key: InsertFailureKey<'_>, - log: &bun_ast::Log, - is_ssr_graph: bool, - ) -> Result<(), bun_alloc::AllocError> { - let (idx, found_existing) = match key { - InsertFailureKey::AbsPath(abs_path) => { - let gop = self.bundled_files.get_or_put(abs_path)?; - if !gop.found_existing { - *gop.key_ptr = Box::<[u8]>::from(abs_path); - } - let (i, fe) = (gop.index, gop.found_existing); - if !fe { - self.edge_lists.push(EdgeLists::default()); - } - (i, fe) - } - InsertFailureKey::Index(i) => (i as usize, true), - }; - self.ensure_stale_bit_capacity(true)?; - self.stale_files.set(idx); + ctx: &mut HotUpdateContext<'_>, + quick_lookup: &mut ArrayHashMap, TempLookup>, + new_imports: &mut Option, + file_index: FileIndex, + bundler_index: bun_ast::Index, + ) -> Result<(), crate::Error> { + debug_assert!(bundler_index.is_valid()); + debug_assert!(ctx.loaders[bundler_index.get() as usize].is_css()); - match SIDE { - Side::Client => { - if found_existing { - let mut existing = core::mem::take(&mut self.bundled_files.values_mut()[idx]); - let key = bun_ptr::RawSlice::new(&*self.bundled_files.keys()[idx]); - self.free_file_content(key.slice(), &mut existing, FreeCssMode::UnrefCss); - existing.failed = true; - existing.kind = FileKind::Unknown; - self.bundled_files.values_mut()[idx] = existing; - } else { - self.bundled_files.values_mut()[idx] = File { - failed: true, - ..Default::default() - }; + // Queue avoids stack overflow; tracing bits in `process_edge_attachment` + // prevent infinite recursion. + let mut queue: Vec = Vec::new(); + queue.push(bundler_index); + + while let Some(idx) = queue.pop() { + let records_len = ctx.import_records[idx.get() as usize].len(); + for i in 0..records_len { + let (flags, src, key) = { + let ir = &ctx.import_records[idx.get() as usize].as_slice()[i]; + ( + ir.flags, + ir.source_index, + ir.path.key_for_incremental_graph(), + ) + }; + let result = self.process_edge_attachment( + ctx, + quick_lookup, + new_imports, + file_index, + flags, + src, + key, + EdgeAttachmentMode::Css, + )?; + if result == EdgeAttachmentResult::Continue && src.is_valid() { + queue.push(src); } } - Side::Server => { - if !found_existing { - self.bundled_files.values_mut()[idx] = File { - failed: true, - is_rsc: !is_ssr_graph, - is_ssr: is_ssr_graph, - ..Default::default() - }; - } else { - let f = &mut self.bundled_files.values_mut()[idx]; - if is_ssr_graph { - f.is_ssr = true; + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn process_edge_attachment( + &mut self, + ctx: &mut HotUpdateContext<'_>, + quick_lookup: &mut ArrayHashMap, TempLookup>, + new_imports: &mut Option, + file_index: FileIndex, + ir_flags: bun_ast::ImportRecordFlags, + ir_source_index: bun_ast::Index, + key: &[u8], + mode: EdgeAttachmentMode, + ) -> Result { + // Duplicated import records are marked unused by `ConvertESMExportsForHmr`. + if ir_flags.contains(bun_ast::ImportRecordFlags::IS_UNUSED) { + return Ok(EdgeAttachmentResult::Stop); + } + if ir_source_index.is_runtime() { + return Ok(EdgeAttachmentResult::Stop); + } + + // Locate the FileIndex from bundle_v2's Source.Index. + let (imported_file_index, kind): (FileIndex, FileKind) = 'brk: { + if ir_source_index.is_valid() { + let kind = if mode == EdgeAttachmentMode::Css { + if ctx.loaders[ir_source_index.get() as usize].is_css() { + FileKind::Css } else { - f.is_rsc = true; + FileKind::Asset } - f.failed = true; + } else { + FileKind::Unknown + }; + if let Some(i) = ctx.get_cached_index(SIDE, ir_source_index).unwrap::() { + break 'brk (i, kind); + } else if mode == EdgeAttachmentMode::Css { + let index = self.g.insert_empty(key, kind)?.index; + ctx.gts.resize(SIDE, index.get() as usize + 1)?; + break 'brk (index, kind); } } + match mode { + // Invalid source indices in CSS are external URLs. + EdgeAttachmentMode::Css => return Ok(EdgeAttachmentResult::Stop), + // Check IncrementalGraph for a file from a prior build. + EdgeAttachmentMode::JsOrHtml => match self.g.bundled_files.get_index(key) { + Some(i) => (FileIndex::::init(i as u32), FileKind::Unknown), + None => return Ok(EdgeAttachmentResult::Continue), + }, + } + }; + + debug_assert!((imported_file_index.get() as usize) < self.g.bundled_files.count()); + + // For CSS visiting CSS, prevent infinite recursion via tracing bits. + if mode == EdgeAttachmentMode::Css && kind == FileKind::Css { + if ctx + .gts + .bits(SIDE) + .is_set(imported_file_index.get() as usize) + { + return Ok(EdgeAttachmentResult::Stop); + } + ctx.gts.bits(SIDE).set(imported_file_index.get() as usize); } - // SAFETY: see `owner()`. - let dev = unsafe { self.owner() }; - let fail_owner = serialized_failure::OwnerPacked::new(SIDE, idx as u32); + let gop = quick_lookup.get_or_put(imported_file_index)?; + if gop.found_existing { + if gop.value_ptr.seen { + return Ok(EdgeAttachmentResult::Continue); + } + gop.value_ptr.seen = true; + let ei = gop.value_ptr.edge_index; + self.g.edges[ei.get() as usize].next_import = *new_imports; + *new_imports = Some(ei); + } else { + // A new edge is needed to represent the dependency and import. + let first_dep = self.g.edge_lists[imported_file_index.get() as usize].first_dep; + let edge = self.g.new_edge(Edge { + next_import: *new_imports, + next_dependency: first_dep, + prev_dependency: None, + imported: imported_file_index, + dependency: file_index, + })?; + if let Some(dep) = first_dep { + self.g.edges[dep.get() as usize].prev_dependency = Some(edge); + } + *new_imports = Some(edge); + self.g.edge_lists[imported_file_index.get() as usize].first_dep = Some(edge); - // Errors print straight to the terminal; a stdio manager that could - // re-render the error list alongside a REPL is a pending idea. - let _ = log.print(std::ptr::from_mut(bun_core::Output::error_writer())); + self.s.incremental_result.had_adjusted_edges = true; - let failure = { - let mut buf = bun_paths::path_buffer_pool::get(); - let key = bun_ptr::RawSlice::new(&*self.bundled_files.keys()[idx]); - // SAFETY: sibling-field `relative_path` reads `dev.root` only. - let owner_display_name = unsafe { (*dev).relative_path(&mut *buf, key.slice()) }; - SerializedFailure::init_from_log( - match SIDE { - Side::Server => serialized_failure::Owner::Server(FileIndex::init(idx as u32)), - Side::Client => serialized_failure::Owner::Client(FileIndex::init(idx as u32)), - }, - owner_display_name, - &log.msgs, - )? - }; - // SAFETY: sibling-field access. - unsafe { - let fail_gop = (*dev).bundling_failures.get_or_put(fail_owner)?; - (*dev) - .incremental_result - .failures_added - .push(failure.clone()); - if fail_gop.found_existing { - (*dev) - .incremental_result - .failures_removed - .push(core::mem::replace(fail_gop.value_ptr, failure)); - } else { - *fail_gop.value_ptr = failure; - } + *gop.value_ptr = TempLookup { + edge_index: edge, + seen: true, + }; } - Ok(()) + Ok(EdgeAttachmentResult::Continue) } // ──────────────────────────────────────────────────────────────────────── - // onFileDeleted / invalidate + // traceDependencies / traceImports // ──────────────────────────────────────────────────────────────────────── - /// `IncrementalGraph(side).onFileDeleted` (spec :1528). - pub(crate) fn on_file_deleted( + /// Walks dependents (importers) outward from `file_index`, marking + /// visited files in `gts` to find the routes/HMR roots affected by a + /// change. + pub(crate) fn trace_dependencies( &mut self, - abs_path: &[u8], - bv2: &mut bun_bundler::BundleV2<'_>, - ) -> Result<(), bun_alloc::AllocError> { - let Some(index) = self.get_file_index(abs_path) else { + file_index: FileIndex, + gts: &mut GraphTraceState, + goal: TraceDependencyGoal, + from_file_index: FileIndex, + ) -> Result<(), crate::Error> { + if gts.bits(SIDE).is_set(file_index.get() as usize) { return Ok(()); - }; - - // Disconnect all imports. - let mut it = self.edge_lists[index.get() as usize].first_import.take(); - while let Some(edge_index) = it { - let dep = self.edges[edge_index.get() as usize]; - it = dep.next_import; - debug_assert_eq!(dep.dependency.get(), index.get()); - self.disconnect_edge_from_dependency_list(edge_index); - self.free_edge(edge_index); } + gts.bits(SIDE).set(file_index.get() as usize); - // Rebuild all dependencies. - let target = match SIDE { - Side::Client => bun_ast::Target::Browser, - Side::Server => bun_ast::Target::Bun, - }; - let mut it = self.edge_lists[index.get() as usize].first_dep; - while let Some(edge_index) = it { - let dep = self.edges[edge_index.get() as usize]; - it = dep.next_dependency; - debug_assert_eq!(dep.imported.get(), index.get()); - let key = &self.bundled_files.keys()[dep.dependency.get() as usize]; - let loader = self.bundled_files.values()[dep.dependency.get() as usize] - .html_route_bundle_index - .is_some() - .then_some(bun_ast::Loader::Html); - bun_core::handle_oom( - bv2.enqueue_file_from_dev_server_incremental_graph_invalidation( - key, target, loader, - ), - ); + match SIDE { + Side::Server => { + let (is_route, is_scb) = { + let file = &self.g.bundled_files.values()[file_index.get() as usize]; + (file.is_route, file.is_client_component_boundary) + }; + if is_route { + let route_index = self + .s + .route_lookup + .get(&ServerFileIndex::init(file_index.get())) + .copied() + .unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Route not in lookup index: {} {:?}", + file_index.get(), + bstr::BStr::new( + &self.g.bundled_files.keys()[file_index.get() as usize] + ), + )) + }); + self.s + .incremental_result + .framework_routes_affected + .push(route_index); + } + if is_scb { + self.s + .incremental_result + .client_components_affected + .push(ServerFileIndex::init(file_index.get())); + } + } + Side::Client => { + let (is_hmr_root, html_rbi) = { + let file = &self.g.bundled_files.values()[file_index.get() as usize]; + (file.is_hmr_root, file.html_route_bundle_index) + }; + if is_hmr_root { + let key = &self.g.bundled_files.keys()[file_index.get() as usize]; + let mut server_graph = self.s.server(); + let index = server_graph.g.get_file_index(key).unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Server Incremental Graph is missing component for {:?}", + bstr::BStr::new(key), + )) + }); + server_graph.trace_dependencies(index, gts, goal, index)?; + } else if let Some(route_bundle_index) = html_rbi { + // HTML modified or asset modified → hard reload; else soft. + let hard = from_file_index == file_index + || matches!( + self.g.bundled_files.values()[from_file_index.get() as usize].content, + Content::Asset(_), + ); + let ir = &mut *self.s.incremental_result; + if hard { + ir.html_routes_hard_affected.push(route_bundle_index); + } else { + ir.html_routes_soft_affected.push(route_bundle_index); + } + if goal == TraceDependencyGoal::StopAtBoundary { + return Ok(()); + } + } + } } - // Bust the resolution cache of the dir containing this file. - let dirname = bun_paths::dirname(abs_path).unwrap_or(abs_path); - let _ = bv2.transpiler.resolver.bust_dir_cache(dirname); + // Certain files do not propagate updates to dependencies. + if goal == TraceDependencyGoal::StopAtBoundary { + if self.g.bundled_files.values()[file_index.get() as usize].stops_dependency_trace(SIDE) + { + return Ok(()); + } + } - // Clear the cached entry from the path→source-index maps. - for map in bv2.graph.build_graphs.values_mut() { - map.remove(abs_path); + // Recurse. + let mut it = self.g.edge_lists[file_index.get() as usize].first_dep; + while let Some(dep_index) = it { + let edge = self.g.edges[dep_index.get() as usize]; + it = edge.next_dependency; + self.trace_dependencies(edge.dependency, gts, goal, file_index)?; } Ok(()) } - /// See `EntryPointList::append_html`. - fn append_client_entry_point( - &self, - entry_points: &mut EntryPointList, - index: usize, + /// Walks imports inward from `file_index`, marking visited files in + /// `gts` to collect everything a bundle for this file must include. + pub(crate) fn trace_imports( + &mut self, + file_index: FileIndex, + gts: &mut GraphTraceState, + goal: TraceImportGoal, ) -> Result<(), crate::Error> { - debug_assert!(matches!(SIDE, Side::Client)); - let abs_path = &self.bundled_files.keys()[index]; - if self.bundled_files.values()[index] - .html_route_bundle_index - .is_some() - { - entry_points.append_html(abs_path) - } else { - entry_points.append_js(abs_path, bake::Graph::Client) + if gts.bits(SIDE).is_set(file_index.get() as usize) { + return Ok(()); } - } + gts.bits(SIDE).set(file_index.get() as usize); - /// `IncrementalGraph(side).invalidate` (spec :1589). Given a set of paths, - /// mark the relevant files as stale and append them into `entry_points`. - pub(crate) fn invalidate( - &mut self, - paths: &[Box<[u8]>], - entry_points: &mut EntryPointList, - ) -> Result<(), crate::Error> { - for path in paths { - let Some(index) = self.bundled_files.get_index(path) else { - continue; - }; - self.stale_files.set(index); - // Store the graph-owned key, not the incoming `path` (which may be - // freed before `entry_points` is consumed). - // Note: re-derive via `RawSlice` so the immutable key borrow - // does not conflict with the `&mut entry_points` push below. - let owned_path = bun_ptr::RawSlice::new(&*self.bundled_files.keys()[index]); - let owned_path = owned_path.slice(); - match SIDE { - Side::Client => match &self.bundled_files.values()[index].content { - Content::CssRoot(_) | Content::CssChild => { - if matches!( - self.bundled_files.values()[index].content, - Content::CssRoot(_), - ) { - entry_points.append_css(owned_path)?; - } - let mut it = self.edge_lists[index].first_dep; - while let Some(edge_index) = it { - let entry = self.edges[edge_index.get() as usize]; - let dep = entry.dependency; - self.stale_files.set(dep.get() as usize); - if matches!( - self.bundled_files.values()[dep.get() as usize].content, - Content::CssRoot(_), - ) { - let k = bun_ptr::RawSlice::new( - &*self.bundled_files.keys()[dep.get() as usize], - ); - entry_points.append_css(k.slice())?; - } - it = entry.next_dependency; + match SIDE { + Side::Server => { + let (is_scb, kind, failed) = { + let f = &self.g.bundled_files.values()[file_index.get() as usize]; + (f.is_client_component_boundary, f.kind, f.failed) + }; + if is_scb || kind == FileKind::Css { + let key = &self.g.bundled_files.keys()[file_index.get() as usize]; + let mut client_graph = self.s.client(); + let index = client_graph.g.get_file_index(key).unwrap_or_else(|| { + bun_core::Output::panic(format_args!( + "Client Incremental Graph is missing component for {:?}", + bstr::BStr::new(key), + )) + }); + client_graph.trace_imports(index, gts, goal)?; + + if cfg!(debug_assertions) && kind == FileKind::Css { + // Server CSS files never have imports. + let lists = &self.g.edge_lists[file_index.get() as usize]; + debug_assert!(lists.first_import.is_none()); + } + } + if goal == TraceImportGoal::FindErrors && failed { + let owner = + serialized_failure::OwnerPacked::new(Side::Server, file_index.get()); + let fail = self + .s + .bundling_failures + .get(&owner) + .cloned() + .expect("Failed to get bundling failure"); + self.s.incremental_result.failures_added.push(fail); + } + } + Side::Client => { + { + let f = &self.g.bundled_files.values()[file_index.get() as usize]; + match &f.content { + Content::CssChild => { + debug_assert!(false, "only CSS roots should be found by tracing"); } - } - Content::Asset(_) => { - let mut it = self.edge_lists[index].first_dep; - while let Some(edge_index) = it { - let entry = self.edges[edge_index.get() as usize]; - let dep = entry.dependency; - self.stale_files.set(dep.get() as usize); - let k = bun_ptr::RawSlice::new( - &*self.bundled_files.keys()[dep.get() as usize], - ); - let k = k.slice(); - if matches!( - self.bundled_files.values()[dep.get() as usize].content, - Content::CssRoot(_), - ) { - entry_points.append_css(k)?; - } else { - self.append_client_entry_point(entry_points, dep.get() as usize)?; + Content::CssRoot(id) => { + if goal == TraceImportGoal::FindCss { + self.g.current_css_files.push(*id); } - it = entry.next_dependency; - } - self.append_client_entry_point(entry_points, index)?; - } - // When re-bundling SCBs, only bundle the server. - Content::Js(_) | Content::Unknown => { - if !self.bundled_files.values()[index].is_hmr_root { - self.append_client_entry_point(entry_points, index)?; + // CSS can't import JS; trace is done. + return Ok(()); } + _ => {} } - }, - Side::Server => { - let f = &self.bundled_files.values()[index]; - if f.is_rsc { - entry_points.append_js(owned_path, bake::Graph::Server)?; - } - if f.is_ssr && !f.is_client_component_boundary { - entry_points.append_js(owned_path, bake::Graph::Ssr)?; - } + } + + if goal == TraceImportGoal::FindClientModules { + let len = self.g.bundled_files.values()[file_index.get() as usize] + .content + .js_code() + .map(|c| c.len()) + .unwrap_or(0); + self.g.current_chunk_parts.push(file_index); + self.g.current_chunk_len += len; + } + + if goal == TraceImportGoal::FindErrors + && self.g.bundled_files.values()[file_index.get() as usize].failed + { + let owner = + serialized_failure::OwnerPacked::new(Side::Client, file_index.get()); + let fail = self + .s + .bundling_failures + .get(&owner) + .cloned() + .expect("Failed to get bundling failure"); + self.s.incremental_result.failures_added.push(fail); + return Ok(()); } } } + + // Recurse. + let mut it = self.g.edge_lists[file_index.get() as usize].first_import; + while let Some(dep_index) = it { + let edge = self.g.edges[dep_index.get() as usize]; + it = edge.next_import; + self.trace_imports(edge.imported, gts, goal)?; + } Ok(()) } - // ──────────────────────────────────────────────────────────────────────── - // takeJSBundle / takeSourceMap - // ──────────────────────────────────────────────────────────────────────── - - /// `IncrementalGraph(.server).takeJSBundle` — server-side overload. - pub(crate) fn take_js_bundle_server( + /// `IncrementalGraph(side).insertFailure` (spec :1419). + pub(crate) fn insert_failure( &mut self, - opts: &TakeJSBundleOptionsServer, - ) -> Result, crate::Error> { - let mut chunk = Vec::new(); - self.take_js_bundle_to_list_server(&mut chunk, opts)?; - Ok(chunk) + key: InsertFailureKey<'_>, + log: &bun_ast::Log, + is_ssr_graph: bool, + ) -> Result<(), bun_alloc::AllocError> { + let (idx, found_existing) = match key { + InsertFailureKey::AbsPath(abs_path) => { + let gop = self.g.bundled_files.get_or_put(abs_path)?; + if !gop.found_existing { + *gop.key_ptr = Box::<[u8]>::from(abs_path); + } + let (i, fe) = (gop.index, gop.found_existing); + if !fe { + self.g.edge_lists.push(EdgeLists::default()); + } + (i, fe) + } + InsertFailureKey::Index(i) => (i as usize, true), + }; + self.g.ensure_stale_bit_capacity(true)?; + self.g.stale_files.set(idx); + + match SIDE { + Side::Client => { + if found_existing { + let mut existing = core::mem::take(&mut self.g.bundled_files.values_mut()[idx]); + let key = &self.g.bundled_files.keys()[idx]; + IncrementalGraph::::free_file_content( + self.s.assets, + key, + &mut existing, + FreeCssMode::UnrefCss, + ); + existing.failed = true; + existing.kind = FileKind::Unknown; + self.g.bundled_files.values_mut()[idx] = existing; + } else { + self.g.bundled_files.values_mut()[idx] = File { + failed: true, + ..Default::default() + }; + } + } + Side::Server => { + if !found_existing { + self.g.bundled_files.values_mut()[idx] = File { + failed: true, + is_rsc: !is_ssr_graph, + is_ssr: is_ssr_graph, + ..Default::default() + }; + } else { + let f = &mut self.g.bundled_files.values_mut()[idx]; + if is_ssr_graph { + f.is_ssr = true; + } else { + f.is_rsc = true; + } + f.failed = true; + } + } + } + + let fail_owner = serialized_failure::OwnerPacked::new(SIDE, idx as u32); + + // Errors print straight to the terminal; a stdio manager that could + // re-render the error list alongside a REPL is a pending idea. + let _ = log.print(std::ptr::from_mut(bun_core::Output::error_writer())); + + let failure = { + let mut buf = bun_paths::path_buffer_pool::get(); + let key = &self.g.bundled_files.keys()[idx]; + let owner_display_name = + crate::bake::dev_server_body::relative_path(self.s.root, &mut buf, key); + SerializedFailure::init_from_log( + match SIDE { + Side::Server => serialized_failure::Owner::Server(FileIndex::init(idx as u32)), + Side::Client => serialized_failure::Owner::Client(FileIndex::init(idx as u32)), + }, + owner_display_name, + &log.msgs, + )? + }; + let fail_gop = self.s.bundling_failures.get_or_put(fail_owner)?; + self.s + .incremental_result + .failures_added + .push(failure.clone()); + if fail_gop.found_existing { + self.s + .incremental_result + .failures_removed + .push(core::mem::replace(fail_gop.value_ptr, failure)); + } else { + *fail_gop.value_ptr = failure; + } + Ok(()) } /// `IncrementalGraph(.client).takeJSBundle` — client-side overload (kept @@ -1724,7 +1852,7 @@ impl IncrementalGraph { options: &TakeJSBundleOptionsClient, ) -> Result<(), crate::Error> { debug_assert!(matches!(SIDE, Side::Client)); - debug_assert!(self.current_chunk_len > 0); + debug_assert!(self.g.current_chunk_len > 0); let kind = options.kind; let runtime: bake::HmrRuntime = match kind { @@ -1736,17 +1864,16 @@ impl IncrementalGraph { }; let mut end_list: Vec = Vec::with_capacity(256); - // SAFETY: see `owner()`. - let dev = unsafe { self.owner() }; match kind { ChunkKind::InitialResponse => { end_list.extend_from_slice(b"}, {\n main: "); if !options.initial_response_entry_point.is_empty() { let mut buf = bun_paths::path_buffer_pool::get(); - // SAFETY: `relative_path` reads `dev.root` only. - let rel = unsafe { - (*dev).relative_path(&mut *buf, options.initial_response_entry_point) - }; + let rel = crate::bake::dev_server_body::relative_path( + self.s.root, + &mut buf, + options.initial_response_entry_point, + ); bun_js_printer::write_json_string::<_, { bun_js_printer::Encoding::Utf8 }>( rel, &mut end_list, @@ -1767,8 +1894,7 @@ impl IncrementalGraph { bun_core::fmt::hex_lower(&generation.to_ne_bytes()) ); end_list.extend_from_slice(b"\",\n version: \""); - // SAFETY: sibling-field read. - end_list.extend_from_slice(unsafe { &(*dev).configuration_hash_key }); + end_list.extend_from_slice(self.s.configuration_hash_key); if options.console_log { end_list.extend_from_slice(b"\",\n console: true"); } else { @@ -1777,10 +1903,11 @@ impl IncrementalGraph { if !options.react_refresh_entry_point.is_empty() { end_list.extend_from_slice(b",\n refresh: "); let mut buf = bun_paths::path_buffer_pool::get(); - // SAFETY: `relative_path` reads `dev.root` only. - let rel = unsafe { - (*dev).relative_path(&mut *buf, options.react_refresh_entry_point) - }; + let rel = crate::bake::dev_server_body::relative_path( + self.s.root, + &mut buf, + options.react_refresh_entry_point, + ); bun_js_printer::write_json_string::<_, { bun_js_printer::Encoding::Utf8 }>( rel, &mut end_list, @@ -1811,15 +1938,15 @@ impl IncrementalGraph { let runtime_code = runtime.code.as_bytes(); let start = list.len(); - let need = self.current_chunk_len + runtime_code.len() + end_list.len(); + let need = self.g.current_chunk_len + runtime_code.len() + end_list.len(); if start == 0 { list.reserve_exact(need); } else { list.reserve(need); } list.extend_from_slice(runtime_code); - for entry in &self.current_chunk_parts { - if let Some(code) = self.bundled_files.values()[entry.get() as usize] + for entry in &self.g.current_chunk_parts { + if let Some(code) = self.g.bundled_files.values()[entry.get() as usize] .content .js_code() { @@ -1831,106 +1958,4 @@ impl IncrementalGraph { let _ = start; Ok(()) } - - /// `IncrementalGraph(.server).takeJSBundleToList` (spec :1713). - fn take_js_bundle_to_list_server( - &mut self, - list: &mut Vec, - options: &TakeJSBundleOptionsServer, - ) -> Result<(), crate::Error> { - debug_assert!(matches!(SIDE, Side::Server)); - debug_assert!(self.current_chunk_len > 0); - - let runtime: bake::HmrRuntime = match options.kind { - ChunkKind::InitialResponse => bake::get_hmr_runtime(Side::Server), - ChunkKind::HmrChunk => bake::HmrRuntime { - code: bun_core::ZStr::from_static(b"({\0"), - line_count: 0, - }, - }; - // Server `.InitialResponse` is unreachable per spec; only HmrChunk hits - // the end-builder. - let end: &[u8] = b"})"; - - let runtime_code = runtime.code.as_bytes(); - let start = list.len(); - list.reserve_exact(self.current_chunk_len + runtime_code.len() + end.len()); - list.extend_from_slice(runtime_code); - for code in &self.current_chunk_code { - list.extend_from_slice(code); - } - list.extend_from_slice(end); - - let _ = start; - Ok(()) - } - - /// `IncrementalGraph(side).takeSourceMap` (spec :1843). - /// Fills in all fields of `out` except `ref_count`. - pub(crate) fn take_source_map( - &mut self, - out: &mut source_map_store::Entry, - ) -> Result<(), crate::Error> { - let paths = self.bundled_files.keys(); - match SIDE { - Side::Client => { - let mut file_paths: Vec> = - Vec::with_capacity(self.current_chunk_parts.len()); - let mut contained_maps: Vec = - Vec::with_capacity(self.current_chunk_parts.len()); - let mut overlapping_memory_cost: usize = 0; - - for file_index in &self.current_chunk_parts { - // Borrowing slice headers from `bundled_files.keys()` - // would be self-referential w.r.t. `DevServer`, so own a - // copy. Paths are short and source-map entries are - // infrequent — profile if hot. - file_paths.push(Box::<[u8]>::from(&*paths[file_index.get() as usize])); - let sm = self.bundled_files.values()[file_index.get() as usize] - .source_map - .clone(); - if let Some(map) = sm.get() { - overlapping_memory_cost += map.memory_cost(); - } - contained_maps.push(sm); - } - overlapping_memory_cost += contained_maps.capacity() - * core::mem::size_of::() - + file_paths.len() * core::mem::size_of::>(); - - let ref_count = out.ref_count; - *out = source_map_store::Entry { - ref_count, - paths: file_paths.into_boxed_slice(), - files: contained_maps, - overlapping_memory_cost: overlapping_memory_cost as u32, - }; - } - Side::Server => { - let mut file_paths: Vec> = - Vec::with_capacity(self.current_chunk_source_maps.len()); - let mut contained_maps: Vec = - Vec::with_capacity(self.current_chunk_source_maps.len()); - let mut overlapping_memory_cost: u32 = 0; - - for item in &self.current_chunk_source_maps { - file_paths.push(Box::<[u8]>::from(&*paths[item.file_index.get() as usize])); - contained_maps.push(item.source_map.clone()); - overlapping_memory_cost += item.source_map.memory_cost() as u32; - } - overlapping_memory_cost += (contained_maps.capacity() - * core::mem::size_of::() - + file_paths.len() * core::mem::size_of::>()) - as u32; - - *out = source_map_store::Entry { - ref_count: out.ref_count, - paths: file_paths.into_boxed_slice(), - files: contained_maps, - overlapping_memory_cost, - }; - } - } - Ok(()) - } } diff --git a/src/runtime/bake/dev_server/inspector_agent.rs b/src/runtime/bake/dev_server/inspector_agent.rs index 728eaa7dfea2..40592b8d1ff0 100644 --- a/src/runtime/bake/dev_server/inspector_agent.rs +++ b/src/runtime/bake/dev_server/inspector_agent.rs @@ -25,25 +25,23 @@ bun_opaque::opaque_ffi! { /// /// Both slot fields are `Copy` `Cell`s, so every method takes `&self` — /// callers reach this through a shared `&Debugger` borrow. -#[repr(transparent)] -pub(crate) struct BunFrontendDevServerAgent(ErasedAgentSlot); - -impl BunFrontendDevServerAgent { - /// Reinterpret the `Debugger`'s erased slot as this agent. Sound because - /// `Self` is `#[repr(transparent)]` over [`ErasedAgentSlot`] and this - /// module is the slot's sole owner. - pub(crate) fn from_slot(slot: &ErasedAgentSlot) -> &Self { - // SAFETY: `#[repr(transparent)]` guarantees identical layout. - unsafe { &*core::ptr::from_ref(slot).cast::() } +#[derive(Clone, Copy)] +pub(crate) struct BunFrontendDevServerAgent<'a>(&'a ErasedAgentSlot); + +impl<'a> BunFrontendDevServerAgent<'a> { + /// View the `Debugger`'s erased slot as this agent; this module is the + /// slot's sole owner. + pub(crate) fn from_slot(slot: &'a ErasedAgentSlot) -> Self { + Self(slot) } /// `nextConnectionID` — wrapping post-increment. - pub(crate) fn next_connection_id(&self) -> i32 { + pub(crate) fn next_connection_id(self) -> i32 { self.0.post_increment_sequence() } #[inline] - pub(crate) fn is_enabled(&self) -> bool { + pub(crate) fn is_enabled(self) -> bool { !self.0.agent_ptr().is_null() } @@ -52,7 +50,7 @@ impl BunFrontendDevServerAgent { /// stays live while the agent is enabled. Returns `None` when disabled. #[inline] #[allow(clippy::mut_from_ref)] - fn handle_mut(&self) -> Option<&mut InspectorBunFrontendDevServerAgentHandle> { + fn handle_mut(self) -> Option<&'a mut InspectorBunFrontendDevServerAgentHandle> { let handle = self.0.agent_ptr(); if handle.is_null() { return None; @@ -64,7 +62,7 @@ impl BunFrontendDevServerAgent { )) } - pub(crate) fn notify_client_connected(&self, dev_server_id: DebuggerId, connection_id: i32) { + pub(crate) fn notify_client_connected(self, dev_server_id: DebuggerId, connection_id: i32) { if let Some(handle) = self.handle_mut() { ffi::InspectorBunFrontendDevServerAgent__notifyClientConnected( handle, @@ -74,7 +72,7 @@ impl BunFrontendDevServerAgent { } } - pub(crate) fn notify_client_disconnected(&self, dev_server_id: DebuggerId, connection_id: i32) { + pub(crate) fn notify_client_disconnected(self, dev_server_id: DebuggerId, connection_id: i32) { if let Some(handle) = self.handle_mut() { ffi::InspectorBunFrontendDevServerAgent__notifyClientDisconnected( handle, @@ -85,25 +83,20 @@ impl BunFrontendDevServerAgent { } pub(crate) fn notify_bundle_start( - &self, + self, dev_server_id: DebuggerId, - trigger_files: &mut [BunString], + trigger_files: &[BunString], ) { if let Some(handle) = self.handle_mut() { - // SAFETY: `trigger_files` is a valid contiguous slice for the call; - // `(ptr, len)` pair derived from it. - unsafe { - ffi::InspectorBunFrontendDevServerAgent__notifyBundleStart( - handle, - dev_server_id.get(), - trigger_files.as_mut_ptr(), - trigger_files.len(), - ) - } + ffi::InspectorBunFrontendDevServerAgent__notifyBundleStart( + handle, + dev_server_id.get(), + bun_core::ffi::FfiSlice::new(trigger_files), + ) } } - pub(crate) fn notify_bundle_complete(&self, dev_server_id: DebuggerId, duration_ms: f64) { + pub(crate) fn notify_bundle_complete(self, dev_server_id: DebuggerId, duration_ms: f64) { if let Some(handle) = self.handle_mut() { ffi::InspectorBunFrontendDevServerAgent__notifyBundleComplete( handle, @@ -114,7 +107,7 @@ impl BunFrontendDevServerAgent { } pub(crate) fn notify_bundle_failed( - &self, + self, dev_server_id: DebuggerId, build_errors_payload_base64: BunString, ) { @@ -131,7 +124,7 @@ impl BunFrontendDevServerAgent { /// `DevServer.RouteBundle.Index` (`-1` for `None`) — caller does /// `rbi.map(|i| i.get() as i32).unwrap_or(-1)`. pub(crate) fn notify_client_navigated( - &self, + self, dev_server_id: DebuggerId, connection_id: i32, url: &BunString, @@ -150,7 +143,7 @@ impl BunFrontendDevServerAgent { /// `notifyConsoleLog`. `kind` is `DevServer.ConsoleLogKind as u8` (`b'l'` /// / `b'e'`) — caller does `kind as u8`. - pub(crate) fn notify_console_log(&self, dev_server_id: DebuggerId, kind: u8, data: &BunString) { + pub(crate) fn notify_console_log(self, dev_server_id: DebuggerId, kind: u8, data: &BunString) { if let Some(handle) = self.handle_mut() { ffi::InspectorBunFrontendDevServerAgent__notifyConsoleLog( handle, @@ -176,8 +169,7 @@ mod ffi { // SAFETY (safe fn): `InspectorBunFrontendDevServerAgentHandle` is an // `opaque_ffi!` ZST handle (`!Freeze` via `UnsafeCell`); `BunString` is a // `#[repr(C)]` in-param the C++ side reads in-place (`&`) or consumes - // (by value). `notifyBundleStart` keeps a raw - // `(ptr, len)` pair (slice not FFI-safe) and stays `unsafe`. + // (by value). unsafe extern "C" { pub(super) safe fn InspectorBunFrontendDevServerAgent__notifyClientConnected( agent: &mut InspectorBunFrontendDevServerAgentHandle, @@ -189,11 +181,10 @@ mod ffi { dev_server_id: i32, connection_id: i32, ); - pub(super) fn InspectorBunFrontendDevServerAgent__notifyBundleStart( + pub(super) safe fn InspectorBunFrontendDevServerAgent__notifyBundleStart( agent: &mut InspectorBunFrontendDevServerAgentHandle, dev_server_id: i32, - trigger_files: *mut BunString, - trigger_files_len: usize, + trigger_files: bun_core::ffi::FfiSlice<'_, BunString>, ); pub(super) safe fn InspectorBunFrontendDevServerAgent__notifyBundleComplete( agent: &mut InspectorBunFrontendDevServerAgentHandle, diff --git a/src/runtime/bake/dev_server/lifecycle.rs b/src/runtime/bake/dev_server/lifecycle.rs deleted file mode 100644 index f26e7f2509be..000000000000 --- a/src/runtime/bake/dev_server/lifecycle.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! `WatcherContext` trait impl for `DevServer` and `WatcherAtomics::init`. -//! -//! `Watcher::init::` dispatches through the generic -//! `WatcherContext` vtable, so the trait methods here are the *only* path the -//! watcher thread reaches — they forward to the inherent -//! `DevServer::{on_file_update, on_watch_error}` bodies in `../DevServer.rs`. - -use super::{DevServer, HotReloadEvent, WatcherAtomics}; - -// ────────────────────────────────────────────────────────────────────────── -// WatcherContext impl — wires `bun_watcher::Watcher::init::`. -// The watcher's stored fn-ptrs (`on_file_update_wrapped` / `on_error_wrapped` -// in `Watcher::init`) call *these* trait methods, never the inherent ones -// directly, so each forwards to the real ported body on `DevServer`. -// ────────────────────────────────────────────────────────────────────────── -impl bun_watcher::WatcherContext for DevServer { - fn on_file_update( - &mut self, - events: &mut [bun_watcher::WatchEvent], - changed_files: &[bun_watcher::ChangedFilePath], - watchlist: &bun_watcher::WatchList, - ) { - DevServer::on_file_update(self, events, changed_files, watchlist); - } - - /// The trait's default `on_watch_error` would forward here, so route both - /// to the inherent path-aware impl rather than emitting a generic warn. - fn on_error(&mut self, err: bun_sys::Error) { - DevServer::on_watch_error(self, err); - } - - fn on_watch_error(&mut self, err: bun_sys::Error) { - DevServer::on_watch_error(self, err); - } -} - -impl WatcherAtomics { - pub(crate) fn init(owner: *mut DevServer) -> core::ptr::NonNull { - let mk_event = || HotReloadEvent::init_empty(owner); - let atomics: *mut WatcherAtomics = bun_core::heap::into_raw(Box::new(WatcherAtomics { - events: [mk_event(), mk_event(), mk_event()], - next_event: core::sync::atomic::AtomicU8::new(super::NextEvent::DONE.0), - current_event: None, - pending_event: None, - #[cfg(debug_assertions)] - dbg_watcher_event: None, - #[cfg(debug_assertions)] - dbg_server_event: None, - })); - // SAFETY: `atomics` is the fresh `heap::into_raw` result; the - // allocation is exclusively reachable through it here. - unsafe { - for ev in &mut (*atomics).events { - ev.atomics = atomics; - } - core::ptr::NonNull::new_unchecked(atomics) - } - } -} diff --git a/src/runtime/bake/dev_server/memory_cost.rs b/src/runtime/bake/dev_server/memory_cost.rs index 766c627921cd..9685692fd4e2 100644 --- a/src/runtime/bake/dev_server/memory_cost.rs +++ b/src/runtime/bake/dev_server/memory_cost.rs @@ -2,7 +2,7 @@ use core::mem::size_of; use crate::api::server::html_bundle::HTMLBundleRoute; use crate::bake::dev_server::{ - DevServer, HmrSocket, IncrementalResult, TestingBatchEvents, deferred_request, packed_map, + DeferredRequest, DevServer, HmrSocket, IncrementalResult, TestingBatchEvents, packed_map, }; use bun_collections::ArrayHashMap; @@ -40,11 +40,11 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { { let DevServer { magic: _, + this: _, root: _, inspector_server_id: _, configuration_hash_key: _, vm: _, - vm_handle: _, server: _, router: _, route_bundles: _, @@ -65,12 +65,11 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { server_register_update_callback: _, bun_watcher: _, directory_watchers: _, - watcher_atomics: _, + hot_reload: _, testing_batch_events: _, generation: _, bundles_since_last_error: _, framework: _, - bundler_framework_views: _, bundler_options: _, server_transpiler: _, client_transpiler: _, @@ -79,7 +78,6 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { plugin_state: _, current_bundle: _, next_bundle: _, - deferred_request_pool: _, active_websocket_connections: _, emit_incremental_visualizer_events: _, emit_memory_visualizer_events: _, @@ -95,7 +93,6 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { // .bundles_since_last_error // .configuration_hash_key // .inspector_server_id - // .deferred_request_pool // .emit_incremental_visualizer_events // .emit_memory_visualizer_events // .frontend_only @@ -106,7 +103,8 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { // .plugin_state // .server_register_update_callback // .server_fetch_function_callback - // .watcher_atomics + // .hot_reload + // .this // pointers that are not considered a part of DevServer // .vm @@ -128,9 +126,6 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { for bundle in dev.route_bundles.iter() { other_bytes += bundle.memory_cost(); } - // .bundler_framework_views (the pointed-to Frameworks are owned elsewhere; - // count the Vec's own backing store) - other_bytes += memory_cost_array_list(&dev.bundler_framework_views); // .server_graph { let cost = dev.server_graph.memory_cost_detailed(); @@ -150,7 +145,8 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { // .assets assets += dev.assets.memory_cost(); // .active_websocket_connections - other_bytes += dev.active_websocket_connections.capacity() * size_of::<*const HmrSocket>(); + other_bytes += dev.active_websocket_connections.capacity() + * (size_of::() + size_of::>()); // .source_maps other_bytes += memory_cost_array_hash_map(&dev.source_maps.entries); for entry in dev.source_maps.entries.values() { @@ -221,12 +217,11 @@ pub(crate) fn memory_cost_detailed(dev: &DevServer) -> MemoryCost { // All entries are owned by the bundler arena, not DevServer, except for `requests` // .current_bundle if let Some(bundle) = &dev.current_bundle { - // `SinglyLinkedList::len()` is an O(N) walk; only the node count matters. - other_bytes += bundle.requests.len() * size_of::(); + other_bytes += bundle.requests.len() * size_of::(); } // .next_bundle { - other_bytes += dev.next_bundle.requests.len() * size_of::(); + other_bytes += dev.next_bundle.requests.len() * size_of::(); other_bytes += memory_cost_array_hash_map(&dev.next_bundle.route_queue); } // .route_lookup diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index e8b18a04c1e0..e93ef06443de 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -11,9 +11,11 @@ #![allow(clippy::module_inception)] +use core::cell::Cell; use core::sync::atomic::Ordering; use bun_collections::{HashMap, StringArrayHashMap, bit_set::DynamicBitSet}; +use bun_ptr::JsCell; use bun_sys::FdExt as _; use super::jsc; @@ -39,9 +41,9 @@ pub(crate) const CLIENT_PREFIX: &str = "/_bun/client"; // `crate::bake::dev_server::DevServer` (the public path used by `server/`, // `dispatch.rs`, …) resolves to that one struct. pub use super::dev_server_body::{ - CacheEntry, CurrentBundle, DeferredPromise, DeferredRequest, DevServer, EntryPointList, - HTMLRouter, Magic, NextBundle, Options, PluginState, RouteIndexAndRecurseFlag, TestingBatch, - TestingBatchEvents, deferred_request, entry_point_list, + CacheEntry, CurrentBundle, DeferredPromise, DeferredRequest, DevServer, DevServerCell, + EntryPointList, HTMLRouter, Magic, NextBundle, Options, PluginState, RouteIndexAndRecurseFlag, + TestingBatch, TestingBatchEvents, deferred_request, entry_point_list, }; /// `DevServer.FileKind` — kept in lockstep with `bun_bundler::bake_types::CacheKind` @@ -269,7 +271,6 @@ pub(crate) use super::dev_server_body::init; pub mod assets; pub mod incremental_graph; pub mod inspector_agent; -mod lifecycle; pub mod packed_map; pub mod route_bundle; pub mod serialized_failure; @@ -304,20 +305,25 @@ impl ResponseLike for bun_uws::AnyResponse { } /// `DevServer.HmrSocket` — per-WebSocket state. Method bodies (open/close/ -/// message handlers) live in [`hmr_socket`]. +/// message handlers) live in [`hmr_socket`]. Heap-allocated and ref-counted: +/// one ref is held by the uws socket's user-data slot (moved in by +/// `upgrade_ref`, released after `on_close`) and one by +/// `dev.active_websocket_connections` until `on_close` removes it. +#[derive(bun_ptr::CellRefCounted)] pub struct HmrSocket { - /// BACKREF: owned by `dev.active_websocket_connections`; destroyed via - /// `remove` + `heap::take` in `on_close`. - pub(crate) dev: bun_ptr::BackRef, - pub(crate) underlying: Option, - pub(crate) subscriptions: super::dev_server_body::HmrTopicBits, + ref_count: Cell, + /// The owning dev server's cell (it closes every socket before it is + /// freed); `None` once it has detached this socket. + pub(crate) dev: Cell>>, + pub(crate) underlying: Cell>, + pub(crate) subscriptions: Cell, /// By telling DevServer the active route, this enables receiving detailed /// `hot_update` events for when the route is updated. - pub(crate) active_route: route_bundle::IndexOptional, + pub(crate) active_route: Cell, /// Source-map keys this socket has been sent; used to ref-count entries /// in `SourceMapStore` so they survive until the socket disconnects. - pub(crate) referenced_source_maps: HashMap, - pub(crate) inspector_connection_id: i32, + pub(crate) referenced_source_maps: JsCell>, + pub(crate) inspector_connection_id: Cell, } impl HmrSocket { @@ -325,87 +331,47 @@ impl HmrSocket { /// given topic. #[inline] pub(crate) fn is_subscribed(&self, topic: HmrTopic) -> bool { - self.subscriptions.contains(topic.as_bit()) + self.subscriptions.get().contains(topic.as_bit()) } } -/// `DevServer.HotReloadEvent` — produced by the watcher thread. -// Note: cache-line alignment makes each inline `WatcherAtomics.events: [3]` -// element occupy its own cache line, avoiding false sharing on -// `contention_indicator` between watcher and dev-server threads. 128 matches -// the cache line on x86_64/aarch64 (Bun's tier-1 targets) and absorbs Intel -// adjacent-line prefetch. -#[repr(align(128))] -pub struct HotReloadEvent { - /// BACKREF (LIFETIMES.tsv): element of `WatcherAtomics.events: [3]`. - /// Nulled by `Drop for DevServer` when an event is still queued; `run` - /// checks for null before dereferencing. - pub(crate) owner: *mut DevServer, - /// BACKREF to the owning `Box`; `run` reclaims it when - /// `owner` has been nulled. - pub(crate) atomics: *mut WatcherAtomics, - pub(crate) concurrent_task: bun_event_loop::ConcurrentTask::ConcurrentTask, +/// The file lists of one `DevServer.HotReloadEvent`, filled by the watcher +/// thread and drained on the JS thread (see [`HotReloadShared`]). +pub struct HotReloadFiles { pub(crate) files: StringArrayHashMap<()>, pub(crate) dirs: StringArrayHashMap<()>, /// NUL-joined absolute paths. pub(crate) extra_files: Vec, pub(crate) timer: std::time::Instant, - /// 1 if referenced, 0 if unreferenced; see `WatcherAtomics`. - #[cfg(debug_assertions)] - pub(crate) contention_indicator: core::sync::atomic::AtomicU32, - #[cfg(debug_assertions)] - pub(crate) debug_mutex: bun_threading::Mutex, } -impl bun_event_loop::Taskable for HotReloadEvent { - const TAG: bun_event_loop::TaskTag = bun_event_loop::task_tag::BakeHotReloadEvent; - /// An inline slot of the watcher's `WatcherAtomics`. If its DevServer is - /// gone (`owner` nulled by `Drop for DevServer`, which then leaves the - /// atomics to the queued event), this was the last thing keeping the - /// atomics alive; otherwise the DevServer still owns them. - unsafe fn release_unrun(this: *mut Self) { - // SAFETY: fn contract; `atomics` is the heap WatcherAtomics `this` lives in. - unsafe { - if (*this).owner.is_null() { - bun_core::heap::destroy((*this).atomics); - } - } - } +/// `DevServer.HotReloadEvent` — one slot of [`HotReloadShared::events`]. The +/// triple-buffer protocol on `HotReloadShared::next_event` hands a slot to one +/// thread at a time; the mutex makes that exclusive access a type-level fact. +// Cache-line aligned so the watcher and JS thread slots do not false-share. +#[repr(align(128))] +pub struct HotReloadEvent { + pub(crate) data: bun_threading::Guarded, } impl HotReloadEvent { - pub(crate) fn init_empty(owner: *mut DevServer) -> HotReloadEvent { + fn new() -> HotReloadEvent { HotReloadEvent { - owner, - atomics: core::ptr::null_mut(), - concurrent_task: Default::default(), - files: Default::default(), - dirs: Default::default(), - extra_files: Vec::new(), - timer: std::time::Instant::now(), - #[cfg(debug_assertions)] - contention_indicator: core::sync::atomic::AtomicU32::new(0), - #[cfg(debug_assertions)] - debug_mutex: bun_threading::Mutex::default(), + data: bun_threading::Guarded::new(HotReloadFiles { + files: Default::default(), + dirs: Default::default(), + extra_files: Vec::new(), + timer: std::time::Instant::now(), + }), } } +} +impl HotReloadFiles { pub(crate) fn is_empty(&self) -> bool { (self.files.count() + self.dirs.count()) == 0 } - /// Debug-asserts that the owning [`DevServer`]'s watcher thread-lock is - /// held. Centralises the back-ref deref so the call sites in - /// `watcher_acquire_event` / `watcher_release_and_submit_event` stay safe. - #[inline] - pub(crate) fn assert_watcher_thread_locked(&self) { - // SAFETY: BACKREF — `owner` is the live DevServer whose `watcher_atomics` - // holds this event; only reached from the watcher thread while - // `Watcher.mutex` is held, so `owner` has not been nulled. Raw place - // projection so this does not alias any live `&mut HotReloadEvent`. - unsafe { (*self.owner).bun_watcher.thread_lock.assert_locked() }; - } - /// Invalidates items in IncrementalGraph, appending all new items to `entry_points`. pub(crate) fn process_file_list( &mut self, @@ -425,11 +391,7 @@ impl HotReloadEvent { // Bust resolution cache, but since Bun does not watch all // directories in a codebase, this only targets the following resolutions - // SAFETY: server_transpiler is initialized in DevServer::init before any - // HotReloadEvent can fire. - let _ = unsafe { dev.server_transpiler.assume_init_mut() } - .resolver - .bust_dir_cache(changed_dir); + let _ = dev.server_transpiler.resolver.bust_dir_cache(changed_dir); // if a directory watch exists for resolution failures, check those now. if let Some(watcher_index) = dev.directory_watchers.watches.get_index(changed_dir) { @@ -438,35 +400,32 @@ impl HotReloadEvent { Some(dev.directory_watchers.watches.values()[watcher_index].first_dep); while let Some(index) = it { - // Note: reshaped for borrowck — re-index per iteration instead of - // holding `dep` ref across resolver call + appendFile + freeDependencyIndex. - let (source_file_path, specifier, next) = { + let resolved = { let dep = &dev.directory_watchers.dependencies[index as usize]; - (dep.source_file_path, &raw const *dep.specifier, dep.next) + it = dep.next; + dev.server_transpiler + .resolver + .resolve( + bun_paths::resolve_path::dirname::( + dep.source_file_path.slice(), + ), + &dep.specifier, + bun_ast::ImportKind::Stmt, + ) + .is_ok() }; - it = next; - - // `specifier` points into the dep's owned `Box<[u8]>`, which is - // not mutated until after `resolve` returns. - // SAFETY: see `Dep` doc — neither slice is mutated mid-resolve. - let resolved = unsafe { dev.server_transpiler.assume_init_mut() } - .resolver - .resolve( - bun_paths::resolve_path::dirname::( - source_file_path.slice(), - ), - unsafe { &*specifier }, - bun_ast::ImportKind::Stmt, - ) - .is_ok(); if resolved { // this resolution result is not preserved as passing it // into BundleV2 is too complicated. the resolution is // cached, anyways. - // Note: inlined `append_file` body for disjoint borrow - // (`self.dirs.keys()` is held immutably across this loop). - bun_core::handle_oom(self.files.get_or_put(source_file_path.slice())); + bun_core::handle_oom( + self.files.get_or_put( + dev.directory_watchers.dependencies[index as usize] + .source_file_path + .slice(), + ), + ); dev.directory_watchers.free_dependency_index(index); } else { // rebuild a new linked list for unaffected files @@ -480,7 +439,8 @@ impl HotReloadEvent { new_first_dep; } else { // without any files to depend on this watcher is freed - dev.directory_watchers.free_entry(watcher_index); + let (store, watcher) = dev.directory_watchers_and_watcher(); + store.free_entry(watcher, watcher_index); } } } @@ -541,8 +501,6 @@ impl HotReloadEvent { } pub fn reset(&mut self) { - #[cfg(debug_assertions)] - self.debug_mutex.unlock(); self.files.clear_retaining_capacity(); self.dirs.clear_retaining_capacity(); self.extra_files.clear(); @@ -578,107 +536,58 @@ impl HotReloadEvent { self.extra_files.extend_from_slice(sub_path); self.extra_files.push(0); } +} + +/// A hot-reload event posted to the JS thread by the watcher thread +/// ([`HotReloadShared::watcher_release_and_submit_event`]). Dispatched under +/// `task_tag::BakeHotReloadEvent`. +pub struct HotReloadTask { + pub(crate) shared: std::sync::Arc, + pub(crate) first: u8, +} +impl HotReloadTask { /// Main-thread side of the watcher → DevServer hand-off. - /// - /// Takes a raw `*mut` because `first` lives in the heap - /// `(*(*first).atomics).events[_]`; holding a `&mut HotReloadEvent` - /// parameter while also materialising `&mut DevServer` would create two - /// aliasing unique borrows. All event accesses go through the raw pointer - /// and `&mut DevServer` is re-borrowed per use, scoped to not overlap any - /// live `&mut *current`. - /// - /// # Safety - /// `first` points into `(*(*first).atomics).events` of a live heap - /// `WatcherAtomics`. `(*first).owner` is either the live owning - /// `DevServer` or null (set by `Drop for DevServer` while this event was - /// still queued). Must run on the DevServer thread. - pub(crate) unsafe fn run(first: *mut HotReloadEvent) { - // SAFETY: caller contract — `first` is a live slot in a heap - // `WatcherAtomics`; `owner` is either the live DevServer or null. - let dev: *mut DevServer = unsafe { (*first).owner }; - if dev.is_null() { - // SAFETY: `atomics` was set in `WatcherAtomics::init` to the - // `heap::into_raw` pointer; DevServer released its reference so - // this is the unique owner. - unsafe { bun_core::heap::destroy((*first).atomics) }; + pub(crate) fn run(self) { + let Some(dev) = self.shared.main.lock().dev.clone() else { + // The DevServer was dropped while this task was queued. return; - } - // SAFETY: see above; `magic` read is non-aliasing. - debug_assert!(unsafe { (*dev).magic } == Magic::Valid); + }; + let first = self.first; + dev.get() + .with_mut(|dev| dev.on_hot_reload_task(&self.shared, first)); + } + + /// The task was still queued when its VM shut down; nothing to release + /// beyond the task itself. + pub(crate) fn release_unrun(self) {} +} + +impl DevServer { + fn on_hot_reload_task(&mut self, shared: &HotReloadShared, first: u8) { + debug_assert!(self.magic == Magic::Valid); bun_core::scoped_log!(DevServer, "HMR Task start"); scopeguard::defer! { bun_core::scoped_log!(DevServer, "HMR Task end"); } - #[cfg(debug_assertions)] - { - // SAFETY: `first` is live and exclusively owned by this thread. - debug_assert!(unsafe { (*first).debug_mutex.try_lock() }); - // SAFETY: `first` is live (caller contract); atomic load needs no - // exclusivity and does not alias any `&mut` borrow. - debug_assert!(unsafe { (*first).contention_indicator.load(Ordering::SeqCst) } == 0); - } - - // SAFETY: `dev` is the unique BACKREF; this fn runs on the DevServer - // thread. No `&mut *first` is live across this borrow. - if unsafe { (*dev).current_bundle.is_some() } { - // SAFETY: as above; `next_bundle` is disjoint from `watcher_atomics`. - unsafe { (*dev).next_bundle.reload_event = Some(first) }; + if self.current_bundle.is_some() { + self.next_bundle.reload_event = Some(first); return; } let mut entry_points = EntryPointList::default(); - - // SAFETY: `first` is live; `&mut *dev` re-borrowed for the call only. - // `process_file_list` mutates graph/watcher/transpiler fields of `dev`, - // all disjoint from `dev.watcher_atomics.events[_]` (where `first` lives). - unsafe { (*first).process_file_list(&mut *dev, &mut entry_points) }; - - // SAFETY: `first` is live; `timer` was set by - // `WatcherAtomics::watcher_acquire_event` before submission. - let timer = unsafe { (*first).timer }; - - // Note: raw-ptr loop because `recycle_event_from_dev_server` returns - // a pointer into `dev.watcher_atomics.events`; re-borrow each iteration - // to avoid aliasing UB. - let mut current: *mut HotReloadEvent = first; - loop { - // SAFETY: `current` always points at a live event owned by - // `dev.watcher_atomics`; `&mut *dev` re-borrowed for the call only, - // disjoint per the note above. - unsafe { (*current).process_file_list(&mut *dev, &mut entry_points) }; - // SAFETY: `dev` is valid; recycle traffics in raw `*mut HotReloadEvent`. - match unsafe { - WatcherAtomics::recycle_event_from_dev_server( - (*dev).watcher_atomics.as_ptr(), - current, - ) - } { - Some(next) => { - current = next; - #[cfg(debug_assertions)] - { - // SAFETY: `current` is a live event we now exclusively own. - debug_assert!(unsafe { (*current).debug_mutex.try_lock() }); - } - } - None => break, - } - } - - // SAFETY: `dev` is valid; no `&mut *current` is live past this point. - let dev_ref = unsafe { &mut *dev }; + let timer = self.drain_hot_reload_events(shared, first, &mut entry_points); if entry_points.set.count() == 0 { return; } - match &mut dev_ref.testing_batch_events { + match &mut self.testing_batch_events { TestingBatchEvents::Disabled => {} TestingBatchEvents::Enabled(ev) => { bun_core::handle_oom(ev.append(&entry_points)); - dev_ref.publish( + self.publish( HmrTopic::TestingWatchSynchronization, &[MessageId::TestingWatchSynchronization.char(), 1], bun_uws::Opcode::BINARY, @@ -688,34 +597,70 @@ impl HotReloadEvent { TestingBatchEvents::EnableAfterBundle => debug_assert!(false), } - if let Err(_err) = dev_ref.start_async_bundle(entry_points, true, timer) { + if let Err(_err) = self.start_async_bundle(entry_points, true, timer) { return; } } + + /// Process the event in slot `first` and every event the watcher queued + /// behind it, appending to `entry_points`. Returns the first event's timer. + pub(crate) fn drain_hot_reload_events( + &mut self, + shared: &HotReloadShared, + first: u8, + entry_points: &mut EntryPointList, + ) -> std::time::Instant { + let mut current = first; + let mut timer = None; + loop { + { + let mut files = shared.events[current as usize].data.lock(); + files.process_file_list(self, entry_points); + timer.get_or_insert(files.timer); + } + match shared.recycle_event_from_dev_server(current) { + Some(next) => current = next, + None => break, + } + } + timer.expect("at least one event") + } +} + +/// State touched only by the watcher thread (under `Watcher.mutex`). +struct WatcherSide { + /// Index into `events` currently being processed by the JS thread. + current_event: Option, + /// Index into `events` queued behind `current_event`. + pending_event: Option, + #[cfg(debug_assertions)] + dbg_acquired_event: Option, +} + +/// State touched only by the JS thread. +pub(crate) struct MainSide { + /// Cleared by `Drop for DevServer`; a queued [`HotReloadTask`] that finds + /// `None` does nothing. + pub(crate) dev: Option>, + #[cfg(debug_assertions)] + dbg_server_event: Option, } -/// `DevServer.WatcherAtomics` — three pre-allocated `HotReloadEvent`s -/// rotated between the watcher thread and the main thread. -pub struct WatcherAtomics { +/// `DevServer.WatcherAtomics` — three pre-allocated `HotReloadEvent`s rotated +/// between the watcher thread and the JS thread. Shared (`Arc`) by the +/// `DevServer`, its watcher's [`DevWatcherHandler`], and any queued +/// [`HotReloadTask`]. +pub struct HotReloadShared { pub(crate) events: [HotReloadEvent; 3], /// Atomically encodes a `NextEvent`: values 0..3 are an index into /// `events`, plus the `WAITING`/`DONE` sentinels. - // Rust cannot align individual fields, so this field is not cache-line - // aligned. Wrap in a `#[repr(align(128))]` newtype (init site: - // lifecycle.rs) if false sharing ever shows up in profiles. pub(crate) next_event: core::sync::atomic::AtomicU8, - /// Watcher-thread-only; index into `events` currently being processed. - pub(crate) current_event: Option, - /// Watcher-thread-only; index into `events` queued behind `current_event`. - pub(crate) pending_event: Option, - // Debug fields to ensure methods are being called in the right order. - #[cfg(debug_assertions)] - pub(crate) dbg_watcher_event: Option<*mut HotReloadEvent>, - #[cfg(debug_assertions)] - pub(crate) dbg_server_event: Option<*mut HotReloadEvent>, + watcher: bun_threading::Guarded, + pub(crate) main: bun_threading::Guarded, + vm_handle: bun_jsc::VmHandle, } -/// Stored in `WatcherAtomics::next_event` (an `AtomicU8`). Modeled as a +/// Stored in `HotReloadShared::next_event` (an `AtomicU8`). Modeled as a /// transparent newtype rather than a `#[repr(u8)] enum` because any value /// other than the named constants is an index into the `events` array, and /// Rust enums cannot hold unlisted discriminants. @@ -731,253 +676,201 @@ impl NextEvent { // Any other value represents an index into the `events` array. } -// These take `this: *mut Self` (not `&mut self`) so the returned -// `*mut HotReloadEvent` and the `concurrent_task` node linked into the event -// loop's queue are derived from the allocation's `heap::into_raw` pointer -// rather than from a `&mut WatcherAtomics` reborrow. The queued pointers must -// stay valid after `Drop for DevServer` writes through the same allocation. -impl WatcherAtomics { +impl HotReloadShared { + pub(crate) fn new( + dev: Option>, + vm_handle: bun_jsc::VmHandle, + ) -> std::sync::Arc { + std::sync::Arc::new(HotReloadShared { + events: [ + HotReloadEvent::new(), + HotReloadEvent::new(), + HotReloadEvent::new(), + ], + next_event: core::sync::atomic::AtomicU8::new(NextEvent::DONE.0), + watcher: bun_threading::Guarded::new(WatcherSide { + current_event: None, + pending_event: None, + #[cfg(debug_assertions)] + dbg_acquired_event: None, + }), + main: bun_threading::Guarded::new(MainSide { + dev, + #[cfg(debug_assertions)] + dbg_server_event: None, + }), + vm_handle, + }) + } + /// Called by DevServer after it receives a task callback. If this returns /// another event, that event should be passed again to this function, and /// so on, until this function returns `None`. /// /// Runs on dev server thread. - /// - /// # Safety - /// `this` is the heap `WatcherAtomics` owned by `DevServer`; `old_event` - /// is a slot in `(*this).events` previously submitted to the dev server - /// thread and now exclusively owned by the caller for reset. - pub(crate) unsafe fn recycle_event_from_dev_server( - this: *mut Self, - old_event: *mut HotReloadEvent, - ) -> Option<*mut HotReloadEvent> { - // SAFETY: caller contract — `this` and `old_event` are live; every - // `(*this)` / `(*old_event)` below is a field access on those. - unsafe { - (*old_event).reset(); - - #[cfg(debug_assertions)] - { - // Not atomic because watcher won't modify this value while an event is running. - let dbg_event = (*this).dbg_server_event; - (*this).dbg_server_event = None; - debug_assert!( - dbg_event == Some(old_event), - "recycleEventFromDevServer: old_event: expected {:?}, got {:p}", - dbg_event, - old_event, - ); - } + pub(crate) fn recycle_event_from_dev_server(&self, old_event: u8) -> Option { + self.events[old_event as usize].data.lock().reset(); + + #[cfg(debug_assertions)] + { + let dbg_event = self.main.lock().dbg_server_event.take(); + debug_assert!( + dbg_event == Some(old_event), + "recycleEventFromDevServer: old_event: expected {dbg_event:?}, got {old_event}", + ); + } - let event: *mut HotReloadEvent = loop { - let next = NextEvent( - (*this) + let event = loop { + let next = NextEvent(self.next_event.swap(NextEvent::WAITING.0, Ordering::AcqRel)); + match next { + NextEvent::WAITING => { + // Success order is not AcqRel because the swap above performed an Acquire load. + // Failure order is Relaxed because we're going to perform an Acquire load + // in the next loop iteration. + if self .next_event - .swap(NextEvent::WAITING.0, Ordering::AcqRel), - ); - match next { - NextEvent::WAITING => { - // Success order is not AcqRel because the swap above performed an Acquire load. - // Failure order is Relaxed because we're going to perform an Acquire load - // in the next loop iteration. - if (*this) - .next_event - .compare_exchange_weak( - NextEvent::WAITING.0, - NextEvent::DONE.0, - Ordering::Release, - Ordering::Relaxed, - ) - .is_err() - { - continue; // another event may have been added - } - return None; // done running events + .compare_exchange_weak( + NextEvent::WAITING.0, + NextEvent::DONE.0, + Ordering::Release, + Ordering::Relaxed, + ) + .is_err() + { + continue; // another event may have been added } - NextEvent::DONE => unreachable!(), - _ => break &raw mut (*this).events[next.0 as usize], + return None; // done running events } - }; - - #[cfg(debug_assertions)] - { - // Not atomic because watcher won't modify this value while an event is running. - (*this).dbg_server_event = Some(event); + NextEvent::DONE => unreachable!(), + _ => break next.0, } - Some(event) + }; + + #[cfg(debug_assertions)] + { + self.main.lock().dbg_server_event = Some(event); } + Some(event) } - /// Atomically get a `*mut HotReloadEvent` that is not in use by the - /// DevServer thread. Call `watcher_release_and_submit_event` when it is - /// filled with files. + /// Pick the slot in `events` that the JS thread is not using. Call + /// `watcher_release_and_submit_event` when it is filled with files. /// /// Called from watcher thread. - /// - /// # Safety - /// `this` is the heap `WatcherAtomics` owned by `DevServer`; the watcher - /// thread holds `Watcher.mutex`. - pub(crate) unsafe fn watcher_acquire_event(this: *mut Self) -> *mut HotReloadEvent { - // SAFETY: caller contract — `this` is live; every `(*this)` / `(*ev)` - // below is a field access on the heap `WatcherAtomics` allocation. - unsafe { - let mut available = [true; 3]; - if let Some(i) = (*this).current_event { - available[i as usize] = false; - } - if let Some(i) = (*this).pending_event { - available[i as usize] = false; - } - - let index = 'find: { - for (i, &is_available) in available.iter().enumerate() { - if is_available { - break 'find i; - } - } - unreachable!() - }; - let ev: *mut HotReloadEvent = &raw mut (*this).events[index]; - - #[cfg(debug_assertions)] - { - debug_assert!( - (*this).dbg_watcher_event.is_none(), - "must call `watcher_release_and_submit_event` before calling `watcher_acquire_event` again", - ); - (*this).dbg_watcher_event = Some(ev); - } - - // `ev` points into `(*this).events[index]`, which the watcher thread - // has exclusive access to (neither `current_event` nor `pending_event`). - let ev_ref = &mut *ev; - - // Initialize the timer if it is empty. - if ev_ref.is_empty() { - // Monotonic start time; elapsed is computed at the read site. - ev_ref.timer = std::time::Instant::now(); - } - - ev_ref.assert_watcher_thread_locked(); + pub(crate) fn watcher_acquire_event(&self) -> u8 { + let mut w = self.watcher.lock(); + let mut available = [true; 3]; + if let Some(i) = w.current_event { + available[i as usize] = false; + } + if let Some(i) = w.pending_event { + available[i as usize] = false; + } + let index = available + .iter() + .position(|&free| free) + .expect("one of three slots is always free") as u8; - #[cfg(debug_assertions)] - debug_assert!(ev_ref.debug_mutex.try_lock()); + #[cfg(debug_assertions)] + { + debug_assert!( + w.dbg_acquired_event.is_none(), + "must call `watcher_release_and_submit_event` before calling `watcher_acquire_event` again", + ); + w.dbg_acquired_event = Some(index); + } + drop(w); - ev + let mut files = self.events[index as usize].data.lock(); + // Initialize the timer if it is empty. + if files.is_empty() { + files.timer = std::time::Instant::now(); } + index } - /// Release the pointer from `watcher_acquire_event`, submitting the event - /// if it contains new files. + /// Release the slot from `watcher_acquire_event`, submitting the event to + /// the JS thread if it contains new files. /// /// Called from watcher thread. - /// - /// # Safety - /// `this` is the heap `WatcherAtomics` owned by `DevServer`; `ev` is the - /// pointer returned by the matching `watcher_acquire_event` call, and the - /// watcher thread still holds exclusive access to it. - // `&(...)` is deliberate — sidesteps dangerous_implicit_autorefs. - #[allow(clippy::needless_borrow)] - pub(crate) unsafe fn watcher_release_and_submit_event( - this: *mut Self, - ev: *mut HotReloadEvent, - ) { - // SAFETY: caller contract — `this` and `ev` are live slots in the heap - // `WatcherAtomics`; every `(*this)` / `(*ev)` below is a field access - // on that allocation. `(*ev).owner` is the live DevServer (watcher - // thread holds `Watcher.mutex`). - unsafe { - (*ev).assert_watcher_thread_locked(); - - #[cfg(debug_assertions)] - { - let Some(dbg_event) = (*this).dbg_watcher_event else { - panic!( - "must call `watcher_acquire_event` before `watcher_release_and_submit_event`" + pub(crate) fn watcher_release_and_submit_event(self: &std::sync::Arc, ev: u8) { + let mut w = self.watcher.lock(); + #[cfg(debug_assertions)] + { + let dbg_event = w.dbg_acquired_event.take().expect( + "must call `watcher_acquire_event` before `watcher_release_and_submit_event`", + ); + debug_assert!( + dbg_event == ev, + "watcher_release_and_submit_event: event is not from last `watcher_acquire_event` call (expected {dbg_event}, got {ev})", + ); + } + + if self.events[ev as usize].data.lock().is_empty() { + return; + } + // There are files to be processed. + + let old_next = NextEvent(self.next_event.swap(ev, Ordering::AcqRel)); + match old_next { + NextEvent::DONE => { + // Dev server is done running events. We need to schedule the event directly. + w.current_event = Some(ev); + w.pending_event = None; + // Relaxed because the dev server is not running events right now. + // (could technically be made non-atomic) + self.next_event + .store(NextEvent::WAITING.0, Ordering::Relaxed); + #[cfg(debug_assertions)] + { + let mut main = self.main.lock(); + debug_assert!( + main.dbg_server_event.is_none(), + "no event should be running right now", ); - }; - debug_assert!( - dbg_event == ev, - "watcher_release_and_submit_event: event is not from last \ - `watcher_acquire_event` call (expected {:p}, got {:p})", - dbg_event, - ev, + main.dbg_server_event = Some(ev); + } + // Refused ⇒ the VM is torn down; the event simply never runs. + let _ = self.vm_handle.post_boxed( + bun_jsc::LoopKind::Regular, + Box::new(HotReloadTask { + shared: std::sync::Arc::clone(self), + first: ev, + }), ); - (*this).dbg_watcher_event = None; - } - - #[cfg(debug_assertions)] - { - (*ev).debug_mutex.unlock(); } - if (*ev).is_empty() { - return; + NextEvent::WAITING => { + if w.pending_event.is_some() { + // `pending_event` is running, which means we're done with `current_event`. + w.current_event = w.pending_event; + } // else, no pending event yet, but not done with `current_event`. + w.pending_event = Some(ev); } - // There are files to be processed. - - let ev_index: u8 = - u8::try_from(ev.offset_from((&raw const (*this).events).cast::())) - .unwrap(); - let old_next = NextEvent((*this).next_event.swap(ev_index, Ordering::AcqRel)); - match old_next { - NextEvent::DONE => { - // Dev server is done running events. We need to schedule the event directly. - (*this).current_event = Some(ev_index); - (*this).pending_event = None; - // Relaxed because the dev server is not running events right now. - // (could technically be made non-atomic) - (*this) - .next_event - .store(NextEvent::WAITING.0, Ordering::Relaxed); - #[cfg(debug_assertions)] - { - debug_assert!( - (*this).dbg_server_event.is_none(), - "no event should be running right now", - ); - // Not atomic because the dev server is not running events right now. - (*this).dbg_server_event = Some(ev); - } - (*ev).concurrent_task = bun_event_loop::ConcurrentTask::ConcurrentTask { - task: bun_event_loop::Task::init(ev), - ..Default::default() - }; - // The queued node pointer is derived from `ev` (allocation-root - // provenance) so it stays valid across `Drop for DevServer`'s - // writes. Refused ⇒ the VM is torn down; the event is one of - // DevServer's inline slots and simply never runs. - let _ = (*(*ev).owner).vm_handle.post( - bun_jsc::LoopKind::Regular, - core::ptr::NonNull::new_unchecked(&raw mut (*ev).concurrent_task), - ); - } - NextEvent::WAITING => { - if (*this).pending_event.is_some() { - // `pending_event` is running, which means we're done with `current_event`. - (*this).current_event = (*this).pending_event; - } // else, no pending event yet, but not done with `current_event`. - (*this).pending_event = Some(ev_index); - } - - _ => { - // This is an index into the `events` array. - let old_index: u8 = old_next.0; - debug_assert!( - (*this).pending_event == Some(old_index), - "watcher_release_and_submit_event: expected `pending_event` to be {}; got {:?}", - old_index, - (*this).pending_event, - ); - // The old pending event hadn't been run yet, so we can replace it with `ev`. - (*this).pending_event = Some(ev_index); - } + _ => { + // This is an index into the `events` array. + let old_index: u8 = old_next.0; + debug_assert!( + w.pending_event == Some(old_index), + "watcher_release_and_submit_event: expected `pending_event` to be {}; got {:?}", + old_index, + w.pending_event, + ); + // The old pending event hadn't been run yet, so we can replace it with `ev`. + w.pending_event = Some(ev); } } } } +/// The watcher-thread half of a `DevServer`: receives file-change batches +/// from its [`bun_watcher::Watcher`] and forwards them to the JS thread through +/// [`HotReloadShared`]. +pub struct DevWatcherHandler { + pub(crate) shared: std::sync::Arc, +} + /// `DevServer.DirectoryWatchStore` — sparse map of directories under watch /// for resolution-failure recovery. /// @@ -997,24 +890,8 @@ pub struct DirectoryWatchStore { /// Dependencies cannot be re-ordered. This list tracks what indexes are free. pub(crate) dependencies_free_list: Vec, } -// SAFETY: `DirectoryWatchStore` is only ever the `directory_watchers` field of -// a heap-allocated `DevServer` (never moved post-init). Raw ptr (not `&mut -// DevServer`) because `&mut self` is live; callers scope their borrows to -// fields disjoint from `directory_watchers`. -bun_core::impl_field_parent! { DirectoryWatchStore => DevServer.directory_watchers; fn mut owner; } impl DirectoryWatchStore { - /// Safe sibling-projection: borrow the owning [`DevServer`]'s - /// `bun_watcher` while holding `&mut self`. The two fields are disjoint, - /// so the returned `&mut Watcher` does not alias `self`. - #[inline] - fn dev_bun_watcher(&mut self) -> &mut bun_watcher::Watcher { - // SAFETY: `owner()` recovers the heap-allocated `DevServer`; - // `bun_watcher` is field-disjoint from `directory_watchers`, so - // `&mut self` and the returned borrow cover non-overlapping memory. - unsafe { &mut (*self.owner()).bun_watcher } - } - /// Returns a dependency slot to the free list so it can be reused. pub(crate) fn free_dependency_index(&mut self, index: u32) { // Zero out the slot so DevServer.deinit/memoryCost — which iterate @@ -1029,7 +906,7 @@ impl DirectoryWatchStore { } /// Expects dependency list to be already freed. - pub(crate) fn free_entry(&mut self, entry_index: usize) { + pub(crate) fn free_entry(&mut self, watcher: &mut bun_watcher::Watcher, entry_index: usize) { let entry = self.watches.values()[entry_index]; bun_core::scoped_log!( @@ -1039,7 +916,7 @@ impl DirectoryWatchStore { entry.dir ); - self.dev_bun_watcher().remove_at_index::( + watcher.remove_at_index::( bun_watcher::WatchItemKind::File, entry.watch_index, 0, @@ -1110,24 +987,29 @@ pub mod directory_watch_store { // ══════════════════════════════════════════════════════════════════════════ bun_bundler::link_impl_DevServerHandle! { - Bake for DevServer => |this| { - barrel_needed_exports() => &raw mut (*this).barrel_needed_exports, - log_for_resolution_failures(abs_path, graph) => { - match (*this).get_log_for_resolution_failures(abs_path, graph) { - Ok(log) => log, + Bake for DevServerCell => |this| { + barrel_needed_exports() => (*this).with_mut(|dev| &raw mut dev.barrel_needed_exports), + log_for_resolution_failures(abs_path, graph) => (*this).with_mut(|dev| { + match dev.get_log_for_resolution_failures(abs_path, graph) { + Ok(log) => std::ptr::from_mut(log), Err(_) => bun_alloc::out_of_memory(), } - }, + }), finalize_bundle(bv2, result) => { - // `bv2` borrows the three `Transpiler`s stored inline in `DevServer` - // (stable heap address); the `'static` is a stand-in for the - // DevServer-self lifetime — see the comment on `CurrentBundle.bv2`. - super::dev_server_body::finalize_bundle(&mut *this, &mut *bv2.cast(), &mut *result) + // `bv2` borrows the three `Transpiler`s owned by `DevServer`; the + // `'static` is a stand-in for the DevServer-self lifetime — see + // the comment on `CurrentBundle.bv2`. + let (bv2, result) = (&mut *bv2.cast(), &mut *result); + (*this) + .with_mut(|dev| super::dev_server_body::finalize_bundle(dev, bv2, result)) .map_err(|e| bun_bundler::Error::from(crate::Error::from(e))) }, handle_parse_task_failure(err, graph, abs_path, log, bv2) => { + let (log, bv2) = (&*log, &mut *bv2); (*this) - .handle_parse_task_failure(&err.into(), graph, abs_path, &*log, &mut *bv2) + .with_mut(|dev| { + dev.handle_parse_task_failure(&err.into(), graph, abs_path, log, bv2) + }) .map_err(Into::into) }, put_or_overwrite_asset(path, contents, content_hash) => { @@ -1136,16 +1018,19 @@ bun_bundler::link_impl_DevServerHandle! { // bytes as an owned blob (ownership is transferred). let path = &*path.cast::>(); let blob = crate::webcore::blob::Any::from_owned_slice(contents.to_vec()); - (*this).put_or_overwrite_asset(path, blob, content_hash).map_err(Into::into) + (*this) + .with_mut(|dev| dev.put_or_overwrite_asset(path, blob, content_hash)) + .map_err(Into::into) }, track_resolution_failure(import_source, specifier, renderer, loader) => { (*this) - .directory_watchers - .track_resolution_failure(import_source, specifier, renderer, loader) + .with_mut(|dev| { + dev.track_resolution_failure(import_source, specifier, renderer, loader) + }) .map_err(Into::into) }, is_file_cached(abs_path, side) => { - (*this).is_file_cached(abs_path, side).map(|e| { + (*this).with_mut(|dev| dev.is_file_cached(abs_path, side)).map(|e| { use bun_bundler::bake_types::CacheKind; bun_bundler::bake_types::CacheEntry { kind: match e.kind { @@ -1157,28 +1042,29 @@ bun_bundler::link_impl_DevServerHandle! { } }) }, - asset_hash(abs_path) => (*this).assets.get_hash(abs_path), - current_bundle_start_data() => { - (*this) - .current_bundle + asset_hash(abs_path) => (*this).get().assets.get_hash(abs_path), + current_bundle_start_data() => (*this).with_mut(|dev| { + dev.current_bundle .as_mut() .map(|c| (&raw mut c.start_data).cast::<()>()) .unwrap_or(core::ptr::null_mut()) - }, + }), register_barrel_with_deferrals(path) => { - let _ = (*this) - .barrel_files_with_deferrals - .get_or_put(path) - .map_err(|_| bun_alloc::out_of_memory()); + (*this).with_mut(|dev| { + let _ = dev + .barrel_files_with_deferrals + .get_or_put(path) + .map_err(|_| bun_alloc::out_of_memory()); + }); Ok(()) }, - register_barrel_export(barrel_path, alias) => { + register_barrel_export(barrel_path, alias) => (*this).with_mut(|dev| { // Silently drop on alloc failure. - let Ok(gop) = (*this).barrel_needed_exports.get_or_put(barrel_path) else { + let Ok(gop) = dev.barrel_needed_exports.get_or_put(barrel_path) else { return; }; let _ = gop.value_ptr.get_or_put(alias); - }, + }), } } @@ -1189,14 +1075,8 @@ impl DevServer { /// Construct the erased handle the bundler stores in /// `Transpiler.options.dev_server` / `LinkerContext.dev_server`. #[inline] - pub(crate) fn bundler_handle(&mut self) -> bun_bundler::dispatch::DevServerHandle { - // SAFETY: `self` is the single per-process DevServer; outlives all dispatch. - unsafe { - bun_bundler::dispatch::DevServerHandle::new( - bun_bundler::dispatch::DevServerHandleKind::Bake, - self, - ) - } + pub(crate) fn bundler_handle(&self) -> bun_bundler::dispatch::DevServerHandle { + bun_bundler::dispatch::DevServerHandle::from_this(self.this()) } } @@ -1210,7 +1090,7 @@ enum DirectoryWatchInsertError { } bun_core::oom_from_alloc!(DirectoryWatchInsertError); -impl DirectoryWatchStore { +impl DevServer { /// Registers a directory watch so that a failed import resolution is /// retried when the containing directory changes. pub(crate) fn track_resolution_failure( @@ -1255,52 +1135,48 @@ impl DirectoryWatchStore { // The `import_source` parameter is not a stable string. Since the // import source will be added to IncrementalGraph anyways, this is a // great place to share memory. - // SAFETY: owner() recovers `*mut DevServer`; `client_graph`/`server_graph`/ - // `graph_safety_lock` are disjoint from `directory_watchers` so this does - // not alias `&mut self`. - let dev = self.owner(); - // SAFETY: `dev` is the heap-allocated DevServer; `graph_safety_lock` is - // disjoint from `directory_watchers`. RAII guard unlocks on drop. - let _g = unsafe { (*dev).graph_safety_lock.guard() }; + let _g = self.graph_safety_lock.guard(); let owned_file_path: bun_ptr::RawSlice = match renderer { Graph::Client => { - // SAFETY: `dev` is the live DevServer owning this store; - // `client_graph` is disjoint from `directory_watchers` so this - // `&mut` does not alias `&mut self`. `graph_safety_lock` is held. - unsafe { &mut (*dev).client_graph } + self.client_graph .insert_empty(import_source, FileKind::Unknown)? .key } Graph::Server | Graph::Ssr => { - // SAFETY: `dev` is the live DevServer owning this store; - // `server_graph` is disjoint from `directory_watchers` so this - // `&mut` does not alias `&mut self`. `graph_safety_lock` is held. - unsafe { &mut (*dev).server_graph } + self.server_graph .insert_empty(import_source, FileKind::Unknown)? .key } }; - match self.insert(dir, owned_file_path, specifier) { + let watcher = self.bun_watcher.as_deref_mut().expect("watcher is live"); + match self.directory_watchers.insert( + watcher, + &mut self.server_transpiler.resolver, + dir, + owned_file_path, + specifier, + ) { Ok(()) => Ok(()), Err(DirectoryWatchInsertError::Ignore) => Ok(()), // ignoring watch errors. Err(DirectoryWatchInsertError::OutOfMemory) => Err(bun_alloc::AllocError), } } +} +impl DirectoryWatchStore { /// `dir_name_to_watch` is cloned; `file_path` must outlive the watch; /// `specifier` is cloned. fn insert( &mut self, + watcher: &mut bun_watcher::Watcher, + resolver: &mut bun_resolver::Resolver, dir_name_to_watch: &[u8], file_path: bun_ptr::RawSlice, specifier: &[u8], ) -> Result<(), DirectoryWatchInsertError> { debug_assert!(!specifier.is_empty()); // TODO: watch the parent dir too. - // Note: take a raw pointer so the &mut self borrow from owner() does - // not overlap subsequent self.* field accesses. - let dev: *mut DevServer = self.owner(); bun_core::scoped_log!( DevServer, @@ -1344,30 +1220,19 @@ impl DirectoryWatchStore { return Ok(()); } - // Note: `errdefer store.watches.swapRemoveAt(gop.index)` — guard the - // map via raw ptr so it doesn't conflict with `&mut self` below. - let watches_ptr: *mut StringArrayHashMap = - &raw mut self.watches; - let watches_guard = scopeguard::guard(gop_index, move |idx| { - // SAFETY: `watches_ptr` points into the heap-allocated DevServer; on - // the error path no other borrow of `self.watches` is outstanding. - let _ = unsafe { (*watches_ptr).swap_remove_at(idx) }; + // errdefer store.watches.swapRemoveAt(gop.index) + let watches_guard = scopeguard::guard(&mut self.watches, |watches| { + let _ = watches.swap_remove_at(gop_index); }); // Try to use an existing open directory handle - // SAFETY: server_transpiler is initialized by Framework::init_transpiler - // before DevServer accepts requests; `dev` is a valid *mut DevServer. - let cache_fd: Option = - match unsafe { (*dev).server_transpiler.assume_init_mut() } - .resolver - .read_dir_info(dir_name_to_watch) - { - Ok(Some(cache)) => { - let fd = cache.get_file_descriptor(); - if fd.is_valid() { Some(fd) } else { None } - } - Ok(None) | Err(_) => None, - }; + let cache_fd: Option = match resolver.read_dir_info(dir_name_to_watch) { + Ok(Some(cache)) => { + let fd = cache.get_file_descriptor(); + if fd.is_valid() { Some(fd) } else { None } + } + Ok(None) | Err(_) => None, + }; let (fd, owned_fd): (bun_sys::Fd, bool) = if bun_watcher::REQUIRES_FILE_DESCRIPTORS { if let Some(fd) = cache_fd { @@ -1415,7 +1280,7 @@ impl DirectoryWatchStore { // the watcher reads it on a file event. Owning the copy also lets the // map keep its own boxed key independently, so no extra intermediate // `Box` is needed here. - let watch_index = match self.dev_bun_watcher().add_directory::( + let watch_index = match watcher.add_directory::( fd, dir_name_to_watch, bun_watcher::Watcher::get_hash(dir_name_to_watch), @@ -1459,7 +1324,11 @@ impl DirectoryWatchStore { /// `file_path`, compared by *pointer identity* since the slice is shared /// with `IncrementalGraph.bundled_files`. Called before IncrementalGraph /// frees a file's key string so no `Dep` is left holding a dangling pointer. - pub(crate) fn remove_dependencies_for_file(&mut self, file_path: &[u8]) { + pub(crate) fn remove_dependencies_for_file( + &mut self, + watcher: &mut bun_watcher::Watcher, + file_path: &[u8], + ) { if self.watches.count() == 0 { return; } @@ -1493,7 +1362,7 @@ impl DirectoryWatchStore { if let Some(new_first_dep) = new_chain { self.watches.values_mut()[watch_index].first_dep = new_first_dep; } else { - self.free_entry(watch_index); + self.free_entry(watcher, watch_index); } } } diff --git a/src/runtime/bake/dev_server/route_bundle.rs b/src/runtime/bake/dev_server/route_bundle.rs index 1451c2c7fa2b..e63a1a1cf73c 100644 --- a/src/runtime/bake/dev_server/route_bundle.rs +++ b/src/runtime/bake/dev_server/route_bundle.rs @@ -72,7 +72,22 @@ impl Data { } } +/// What `DevServer::trace_all_route_imports` needs from a route bundle: which +/// graph files to start tracing from. +#[derive(Clone, Copy)] +pub(crate) enum TraceKey { + Framework(framework_router::RouteIndex), + Html(incremental_graph::ClientFileIndex), +} + impl RouteBundle { + pub(crate) fn trace_key(&self) -> TraceKey { + match &self.data { + Data::Framework(fw) => TraceKey::Framework(fw.route_index), + Data::Html(html) => TraceKey::Html(html.bundled_file), + } + } + /// Note: takes `&mut SourceMapStore` rather than `&mut DevServer` — /// only `dev.source_maps` is touched, and the two keystone /// `DevServer` structs (`dev_server::DevServer` / `dev_server_body::DevServer`) diff --git a/src/runtime/bake/dev_server/source_map_store.rs b/src/runtime/bake/dev_server/source_map_store.rs index c20acff84659..076a91559a62 100644 --- a/src/runtime/bake/dev_server/source_map_store.rs +++ b/src/runtime/bake/dev_server/source_map_store.rs @@ -15,7 +15,7 @@ use crate::bake::dev_server_body::map_log; use crate::bake::{self, Side}; use crate::timer::EventLoopTimerState; -use super::{ChunkKind, DevServer, EventLoopTimer, Magic, TimerTag, packed_map}; +use super::{ChunkKind, EventLoopTimer, TimerTag, packed_map}; /// See `SourceId` for what the content of u64 is. #[repr(transparent)] @@ -92,12 +92,7 @@ impl Entry { } /// `SourceMapStore.Entry.renderJSON`. - pub(crate) fn render_json( - &self, - dev: &mut DevServer, - kind: ChunkKind, - side: Side, - ) -> crate::Result> { + pub(crate) fn render_json(&self, kind: ChunkKind, side: Side) -> crate::Result> { let map_files = self.files.as_slice(); let paths = &self.paths; @@ -218,8 +213,6 @@ impl Entry { let json_bytes = j.done_with_end(b"\"}")?.into_vec(); // errdefer @compileError("last try should be the final alloc") — no further fallible ops below. - let _ = dev; - Ok(json_bytes) } @@ -425,8 +418,6 @@ pub struct SourceMapStore { pub(crate) weak_ref_sweep_timer: EventLoopTimer, } -bun_event_loop::impl_timer_owner!(SourceMapStore; from_timer_ptr => weak_ref_sweep_timer); - impl Default for SourceMapStore { fn default() -> Self { Self { @@ -437,12 +428,6 @@ impl Default for SourceMapStore { } } -// Intrusive backref: recover the owning DevServer. Caller must guarantee `self` -// is the `source_maps` field of a live, heap-allocated `DevServer` (always -// true for production use; the `Default::default()` instance must never call -// this). -bun_core::impl_field_parent! { SourceMapStore => DevServer.source_maps; pub fn mut owner; } - impl SourceMapStore { /// ArrayHashMap/LinearFifo have no `const fn` ctors; callers use /// this in lieu of a `const`. @@ -606,59 +591,34 @@ impl SourceMapStore { } /// `SourceMapStore.sweepWeakRefs` — pop expired weak-refs, decrement, - /// reschedule. Called from the high-tier `EventLoopTimer` dispatch with - /// the raw `*EventLoopTimer`. - /// - /// # Safety - /// `timer` must point to the `weak_ref_sweep_timer` field of a live - /// `SourceMapStore` that is itself the `source_maps` field of a live - /// heap-allocated `DevServer`. - // `timer` is never dereferenced in Rust — `from_timer_ptr` only does - // `container_of` pointer arithmetic to recover the parent `SourceMapStore`; - // the deref is of that recovered parent pointer, not the parameter. - // not_unsafe_ptr_arg_deref is a false positive on this fieldParentPtr pattern. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn sweep_weak_refs( - timer: *mut EventLoopTimer, - now_ts: &bun_event_loop::EventLoopTimer::Timespec, - ) { + /// reschedule. Called from `DevServer::sweep_source_map_weak_refs` (the + /// `EventLoopTimerTag::DevServerSweepSourceMaps` handler). + pub(crate) fn sweep_weak_refs(&mut self, now_ts: &bun_event_loop::EventLoopTimer::Timespec) { map_log!("sweepWeakRefs"); - // SAFETY: `timer` points to the `weak_ref_sweep_timer` field of a SourceMapStore. - let store: &mut SourceMapStore = unsafe { &mut *SourceMapStore::from_timer_ptr(timer) }; - // SAFETY: invariant of `owner()` — store is the `source_maps` field of a live DevServer. - debug_assert!(unsafe { (*store.owner()).magic } == Magic::Valid); // Mixed-sign comparison: a negative `expire` must count as expired. // Keep `now` as i64 (already clamped ≥0) so the comparison stays // sign-correct without u64 wrap. let now: i64 = now_ts.sec.max(0); - // `emitMemoryVisualizerMessageIfNeeded` is inlined at both returns - // (a scopeguard cannot capture &mut store across the loop body - // without aliasing). - - while let Some(item) = store.weak_refs.read_item() { + while let Some(item) = self.weak_refs.read_item() { if item.expire <= now { - store.unref_count(item.key(), item.count); + self.unref_count(item.key(), item.count); } else { - store.weak_refs.unget(&[item]).expect("unreachable"); // space exists since the last item was just removed. - store.weak_ref_sweep_timer.state = EventLoopTimerState::FIRED; + self.weak_refs.unget(&[item]).expect("unreachable"); // space exists since the last item was just removed. + self.weak_ref_sweep_timer.state = EventLoopTimerState::FIRED; Self::timer_all().update( - core::ptr::addr_of_mut!(store.weak_ref_sweep_timer), + core::ptr::addr_of_mut!(self.weak_ref_sweep_timer), &Timespec { sec: item.expire + 1, nsec: 0, }, ); - // SAFETY: invariant of `owner()`. - unsafe { (*store.owner()).emit_memory_visualizer_message_if_needed() }; return; } } - store.weak_ref_sweep_timer.state = EventLoopTimerState::CANCELLED; - // SAFETY: invariant of `owner()`. - unsafe { (*store.owner()).emit_memory_visualizer_message_if_needed() }; + self.weak_ref_sweep_timer.state = EventLoopTimerState::CANCELLED; } /// This is used in exactly one place: remapping errors. diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 96dceecbb2d9..e4b9cb24e1c0 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -18,7 +18,7 @@ use std::borrow::Cow; pub(crate) mod bake_body; #[path = "DevServer.rs"] -mod dev_server_body; +pub mod dev_server_body; pub(crate) use dev_server_body::get_deinit_count_for_testing; pub(crate) use dev_server_body::is_allowed_dev_host; pub(crate) use dev_server_body::is_allowed_host_header; @@ -178,27 +178,28 @@ impl Framework { /// version operates on the keystone `BuildConfigSubset` (which omits /// `conditions`/`env`/`define`/`drop` until the schema types are /// const-constructible — those paths default). - /// Returns the arena slot for the `bake_types::Framework` projection; caller must `drop_in_place` it. + /// The transpiler is boxed because its linker holds pointers into it. pub(crate) fn init_transpiler<'a>( &mut self, arena: &'a bun_alloc::Arena, log: &mut bun_ast::Log, mode: Mode, renderer: Graph, - out: &mut core::mem::MaybeUninit>, bundler_options: &BuildConfigSubset, - ) -> crate::Result<*mut bun_bundler::bake_types::Framework> { + ) -> crate::Result>> { use bun_options_types::schema as bun_schema; let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena); let _ast_scope = ast_memory_allocator.enter(); - let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init( + // Boxed before `configure_linker`, which records pointers into it. + let mut transpiler = Box::new(bun_bundler::Transpiler::init( arena, log, bun_schema::api::TransformOptions::default(), None, )?); + let out = &mut *transpiler; out.options.target = match renderer { Graph::Client => bun_ast::Target::Browser, @@ -249,13 +250,8 @@ impl Framework { out.options.minify_whitespace = mode != Mode::Development; out.options.css_chunking = true; // The bundler crate (lower tier) carries a TYPE_ONLY - // projection (`bake_types::Framework`); construct it here and give it - // arena lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. - let framework_view: *mut bun_bundler::bake_types::Framework = - arena.alloc(self.as_bundler_view()); - // SAFETY: `arena.alloc` returns a non-null, initialized pointer backed by `arena: &'a Arena`, - // which outlives `out: &mut Transpiler<'a>`, so borrowing it as `&'a Framework` is sound. - out.options.framework = Some(unsafe { &*framework_view }); + // projection (`bake_types::Framework`). + out.options.framework = Some(std::sync::Arc::new(self.as_bundler_view())); out.options.inline_entrypoint_import_meta_main = true; if let Some(ignore) = bundler_options.ignore_dce_annotations { out.options.ignore_dce_annotations = ignore; @@ -328,7 +324,7 @@ impl Framework { // Re-sync after define/naming mutations so the // resolver sees the final option set. out.sync_resolver_opts(); - Ok(framework_view) + Ok(transpiler) } /// Resolves built-in module diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index c83ea9819ed2..886feff5d56f 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -105,7 +105,7 @@ use bun_jsc::hot_reloader; use bun_jsc::jsc_scheduler::JSCDeferredWorkTask; use crate::bake::dev_server::DevServer; -use crate::bake::dev_server::HotReloadEvent as BakeHotReloadEvent; +use crate::bake::dev_server::HotReloadTask as BakeHotReloadTask; use crate::bake::dev_server::source_map_store::SourceMapStore; #[cfg(windows)] @@ -572,13 +572,9 @@ fn run_task_cold(task: Task) { // ── bake dev-server ────────────────────────────────────────────── task_tag::BakeHotReloadEvent => { - // SAFETY: §Dispatch — tag identifies pointee; the event lives in a - // heap `WatcherAtomics` that can outlive its `DevServer`. `run` - // either re-derives `&mut DevServer` from the BACKREF or (when the - // owner has been dropped) only reclaims the heap `WatcherAtomics`, - // so pass the raw pointer to avoid materialising an aliasing - // `&mut` here. - unsafe { BakeHotReloadEvent::run(cast_ptr!(BakeHotReloadEvent)) }; + // SAFETY: §Dispatch — boxed by `HotReloadShared::watcher_release_and_submit_event` + // (`VmHandle::post_boxed`); the arm consumes it. + (*unsafe { bun_core::heap::take(cast_ptr!(BakeHotReloadTask)) }).run(); } // Any tag the hot path mis-routed: producer bug. @@ -594,6 +590,17 @@ const _: () = assert!( "dispatch::run_task / release_task_unrun arm count out of sync with bun_event_loop::task_tag", ); +// The dev server's hot-reload task is a plain owned box with safe +// `run`/`release_unrun`; the raw-pointer reclaim lives here next to the arms +// that do the same. +impl bun_event_loop::Taskable for BakeHotReloadTask { + const TAG: bun_event_loop::TaskTag = task_tag::BakeHotReloadEvent; + unsafe fn release_unrun(this: *mut Self) { + // SAFETY: fn contract — the box queued under this tag. + (*unsafe { bun_core::heap::take(this) }).release_unrun(); + } +} + // ──────────────────────────────────────────────────────────────────────────── // `tick_queue_with_count` — the full drain loop. // ──────────────────────────────────────────────────────────────────────────── @@ -1087,17 +1094,20 @@ pub(crate) unsafe fn __bun_fire_timer( .timeout_callback()) } EventLoopTimerTag::DevServerSweepSourceMaps => { - // `sweep_weak_refs` takes the raw `*EventLoopTimer` and recovers - // the store inside. - // SAFETY: per fn contract. - SourceMapStore::sweep_weak_refs(t, unsafe { &*now }); + // `t` is `dev.source_maps.weak_ref_sweep_timer`. + let store: *mut SourceMapStore = owner!(SourceMapStore, weak_ref_sweep_timer); + // SAFETY: per fn contract; `SourceMapStore` is always the + // `source_maps` field of a live `DevServer`, and no other borrow + // of it is live while its timer fires. + unsafe { + let dev = bun_core::from_field_ptr!(DevServer, source_maps, store); + (*dev).sweep_source_map_weak_refs(&*now) + }; Ok(()) } EventLoopTimerTag::DevServerMemoryVisualizerTick => { - // SAFETY: per fn contract; `t` is the `memory_visualizer_timer` - // field of a live DevServer. - DevServer::emit_memory_visualizer_message_timer(unsafe { &mut *t }, unsafe { &*now }); - Ok(()) + timer_arm!(DevServer, memory_visualizer_timer, |c, now, _vm| (*c) + .emit_memory_visualizer_message_timer(&*now)) } EventLoopTimerTag::BunTest => { let container = owner!(BunTest, timer); @@ -1227,7 +1237,7 @@ fn __bun_release_task_unrun(task: bun_event_loop::Task) { unsafe { bun_jsc::job::release_unrun_erased(task.ptr) } } task_tag::AsyncModule => release!(bun_jsc::async_module::AsyncModule), - task_tag::BakeHotReloadEvent => release!(BakeHotReloadEvent), + task_tag::BakeHotReloadEvent => release!(BakeHotReloadTask), task_tag::BundleV2DeferredBatchTask => release!(BundleV2DeferredBatchTask), task_tag::BundleV2PluginResolve => { release!(bun_bundler::bundle_v2::api::JSBundler::Resolve) diff --git a/src/runtime/server/AnyRequestContext.rs b/src/runtime/server/AnyRequestContext.rs index 1c5ea45ebdb0..97bd980b739b 100644 --- a/src/runtime/server/AnyRequestContext.rs +++ b/src/runtime/server/AnyRequestContext.rs @@ -222,29 +222,11 @@ impl AnyRequestContext { dispatch!(self, (), |_T, ctx| ctx.set_signal_aborted(reason)) } - pub(crate) fn dev_server(self) -> Option<&'static crate::bake::DevServer::DevServer> { - dispatch!(self, None, |_T, ctx| ctx.dev_server().map(|r| { - // SAFETY: the server backref outlives any AnyRequestContext (held only - // for the duration of a request callback); `self` is a by-value tagged - // pointer, so there is no input lifetime to tie the borrow to. - unsafe { bun_ptr::detach_lifetime_ref(r) } - })) - } - - /// Mutable access to the attached DevServer. The accessor above hands out - /// `&` only. The `Box` slot - /// inside `NewServer` has a stable address, so deriving `&mut` here is - /// sound as long as the caller upholds the usual single-writer rule on the - /// JS thread. - pub(crate) fn dev_server_mut(self) -> Option<*mut crate::bake::DevServer::DevServer> { - dispatch!(self, None, |_T, ctx| { - let server = ctx.server.get()?; - // SAFETY: `dev_server` is a `Box` field never moved while requests - // are in flight, so dereferencing the slot for exclusive access on - // the JS thread is sound. - let ds = unsafe { (*server.dev_server.as_ptr()).as_deref_mut()? }; - Some(core::ptr::from_mut(ds)) - }) + /// The attached DevServer's cell, for callers that go on to mutate it. + pub(crate) fn dev_server_cell( + self, + ) -> Option> { + dispatch!(self, None, |_T, ctx| ctx.dev_server_cell()) } /// The context behind this handle if it is a `T`. diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 76fb6b8d4a75..dc3a7e56df5c 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -26,15 +26,17 @@ use crate::webcore::{ /// /// If it did, it would *overwrite* the user data context pointer (this /// is what it did before), causing segfaults. -pub struct AdditionalOnAbortCallback { - pub cb: fn(*mut c_void), - pub(crate) data: NonNull, - pub(crate) deref_fn: fn(*mut c_void), -} +pub struct AdditionalOnAbortCallback( + pub(crate) bun_ptr::RefPtr, +); impl AdditionalOnAbortCallback { - pub fn deref(&self) { - (self.deref_fn)(self.data.as_ptr()); + fn fire(&self) { + self.0.on_abort_from_request_context(); + } + /// Release the ref the callback holds. + pub fn deref(self) { + self.0.deref(); } } @@ -228,10 +230,13 @@ where self.defer_deinit_until_callback_completes.get().is_none() } - pub(crate) fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer> { + /// The dev server this request belongs to, as its cell (for the JS entry + /// points that go on to mutate it). + pub(crate) fn dev_server_cell( + &self, + ) -> Option> { let server = self.server.get()?; - // SAFETY: BACKREF — the server outlives every context it allocates. - unsafe { &*server.as_const_ptr() }.dev_server() + server.dev_server_cell() } } @@ -1478,7 +1483,7 @@ where this.flags.set_aborted(true); let abort = this.additional_on_abort.replace(None); if let Some(abort) = abort { - (abort.cb)(abort.data.as_ptr()); + abort.fire(); abort.deref(); } diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 361d325fdad2..0df04ad652bc 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -284,7 +284,8 @@ pub struct NewServer { /// The server's counted ref on the plugin state; released in `Drop`. pub(crate) plugins: JsCell>>, - pub(crate) dev_server: JsCell>>, + pub(crate) dev_server: + JsCell>>, /// Route → index in RouteList.cpp. User routes may be applied multiple /// times due to SNI, so we have to store them. Each is its own allocation: @@ -586,7 +587,7 @@ impl NewServer { .dev_server .get() .as_ref() - .map_or(0, |d| d.memory_cost()) + .map_or(0, |d| d.get().memory_cost()) } pub(crate) fn h3_alt_svc(&self) -> Option<&[u8]> { @@ -2029,6 +2030,7 @@ impl NewServer { arena: &bake_options.arena, root: bake_options.root, vm: jsc::VirtualMachine::get(), + server: AnyServer::from(&*server), // LAYERING: `UserOptions` carries the `bake_body` shapes; // `DevServer::Options` consumes the keystone shapes; // `From` impls in `bake/mod.rs` bridge @@ -2293,13 +2295,13 @@ impl NewServer { AnyRoute::Directory(r) => apply!(DirectoryRoute, r), AnyRoute::Html(r) => { apply!(html_bundle::Route, r); - self.dev_server.with_mut(|dev| { - if let Some(dev) = dev { + if let Some(dev) = self.dev_server.get() { + dev.with_mut(|dev| { bun_core::handle_oom( dev.html_router.put(&entry.path, r.this_ptr().into()), - ); - } - }); + ) + }); + } needs_plugins = true; } AnyRoute::FrameworkRouter(_) => {} @@ -2342,13 +2344,12 @@ impl NewServer { let mut has_dev_server_for_star_path = false; if has_dev_server { // dev.setRoutes might register its own "/*" HTTP handler - has_dev_server_for_star_path = self.dev_server.with_mut(|dev| { - bun_core::handle_oom( - dev.as_deref_mut() - .expect("checked above") - .set_routes::(self), - ) - }); + has_dev_server_for_star_path = self + .dev_server + .get() + .as_ref() + .expect("checked above") + .with_mut(|dev| bun_core::handle_oom(dev.set_routes::(self))); if has_dev_server_for_star_path { // Assume dev server "/*" covers all methods if it exists star_methods_covered_by_user = http_method::Set::all(); @@ -3123,6 +3124,7 @@ pub trait ServerLike { fn config(&self) -> &ServerConfig; fn on_request_complete(&self); fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer>; + fn dev_server_cell(&self) -> Option>; fn js_value(&self) -> &jsc::JsRef; fn h3_alt_svc(&self) -> Option<&[u8]>; fn terminated(&self) -> bool; @@ -3152,7 +3154,13 @@ impl ServerLike for NewServer { } #[inline] fn dev_server(&self) -> Option<&crate::bake::DevServer::DevServer> { - self.dev_server.get().as_deref() + self.dev_server.get().as_ref().map(|d| d.get()) + } + fn dev_server_cell(&self) -> Option> { + self.dev_server + .get() + .as_ref() + .map(|dev| bun_ptr::BackRef::new(&**dev)) } #[inline(always)] fn js_value(&self) -> &jsc::JsRef { @@ -3363,11 +3371,9 @@ impl AnyServer { pub(crate) fn set_inspector_server_id(&self, id: jsc::DebuggerId) { any_server_dispatch!(self, |s| { s.inspector_server_id.set(id); - s.dev_server.with_mut(|dev| { - if let Some(dev_server) = dev.as_deref_mut() { - dev_server.inspector_server_id = id; - } - }); + if let Some(dev) = s.dev_server.get() { + dev.with_mut(|dev| dev.inspector_server_id = id); + } }) } @@ -3471,7 +3477,9 @@ impl AnyServer { ) -> Option { any_server_dispatch!(self, |s| s .dev_server - .with_mut(|dev| dev.as_deref_mut().map(f))) + .get() + .as_ref() + .map(|dev| dev.with_mut(f))) } /// Returns: diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 3aced452a77d..9aca169b36e9 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -2115,13 +2115,13 @@ impl NewServer { } // These get re-applied when we set the static routes again. - self.dev_server.with_mut(|dev| { - if let Some(dev_server) = dev.as_deref_mut() { + if let Some(dev) = self.dev_server.get() { + dev.with_mut(|dev_server| { // Prevent a use-after-free in the hash table keys. dev_server.html_router.clear(); dev_server.html_router.fallback = None; - } - }); + }); + } self.config.with_mut(|c| { // NOTE: `Vec` impls `Drop`, so @@ -3211,9 +3211,9 @@ impl NewServer { httplog!("{} - {}", BStr::new(&m), BStr::new(&u)); } - let dev_server_guard = server.dev_server.get(); + let dev_server_guard = server.dev_server.get().as_ref().map(|d| d.get()); let authorized = 'brk: { - let Some(dev_server) = dev_server_guard.as_deref() else { + let Some(dev_server) = dev_server_guard else { break 'brk false; }; @@ -3239,7 +3239,7 @@ impl NewServer { req.set_yield(true); return; } - let root: &[u8] = &dev_server_guard.as_deref().expect("authorized").root; + let root: &[u8] = &dev_server_guard.expect("authorized").root; // They need a 16 byte uuid. It needs to be somewhat consistent. We don't want to store this field anywhere. diff --git a/src/uws/lib.rs b/src/uws/lib.rs index 627f9ddd52c8..fdb2ad28edae 100644 --- a/src/uws/lib.rs +++ b/src/uws/lib.rs @@ -20,7 +20,7 @@ use bun_core::ZStr; #[cfg(windows)] pub use bun_uws_sys::Timer; pub use bun_uws_sys::{ - AnyWebSocket, BodyReaderMixin, ConnectingSocket, NewApp, RawWebSocket, Request, + AnyWebSocket, BodyReader, BodyReaderHandler, ConnectingSocket, NewApp, RawWebSocket, Request, WebSocketBehavior, us_socket_stream_buffer_t, us_socket_t, uws_res, }; diff --git a/src/uws_sys/BodyReaderMixin.rs b/src/uws_sys/BodyReaderMixin.rs index 1707d5512cd9..de628ba70dba 100644 --- a/src/uws_sys/BodyReaderMixin.rs +++ b/src/uws_sys/BodyReaderMixin.rs @@ -1,10 +1,7 @@ -use core::marker::PhantomData; -use core::mem; - use crate::AnyResponse; use crate::response::Response; -/// Response types that can drive a `BodyReaderMixin`: must support registering +/// Response types that can drive a [`BodyReader`]: must support registering /// data/abort callbacks and converting to `AnyResponse`. /// /// Only `Response` is wired today (DevServer's only consumer is HTTP/1.x); @@ -40,183 +37,103 @@ impl BodyResponse for Response { } } -/// Mixin to read an entire request body into memory and run a callback. -/// Consumers should make sure a reference count is held on the server, -/// and is unreferenced after one of the two callbacks are called. -/// -/// See `DevServer`'s `ErrorReportRequest` for an example. -/// -/// The wrapper type implements this trait; the intrusive field is declared -/// via the [`bun_core::IntrusiveField`] supertrait (implement via -/// `bun_core::intrusive_field!`). -pub trait BodyReaderHandler: bun_core::IntrusiveField> + 'static { - /// `body` is freed after this function returns. - /// - /// Receives the original `heap::alloc`'d pointer (full-allocation - /// provenance) rather than `&mut self`: implementors typically free `Self` - /// (`heap::take`) on the success path, and doing so through a - /// `&mut self`-derived pointer is UB under Stacked/Tree Borrows. - /// - /// SAFETY: `this` is the pointer previously passed to - /// `BodyReaderMixin::read_body`; it is live and uniquely owned by the - /// mixin until this call (no other `&mut` into the allocation is live). - unsafe fn on_body(this: *mut Self, body: &[u8], resp: AnyResponse) -> crate::Result<()>; - - /// Called on error or request abort. Same provenance contract as `on_body`. - /// - /// SAFETY: see `on_body`. - unsafe fn on_error(this: *mut Self); +/// Receives a fully-buffered request body. Owned by the [`BodyReader`] from +/// [`BodyReader::read`] until the body arrives (then `on_body` runs once and +/// the handler is dropped) or the request errors/aborts (the handler is just +/// dropped) — so teardown belongs in `Drop`. +pub trait BodyReaderHandler: Sized + 'static { + /// `body` is freed after this returns. On `Err` the reader answers the + /// request with a 400/500. + fn on_body(&mut self, body: &[u8], resp: AnyResponse) -> crate::Result<()>; } -pub struct BodyReaderMixin { +/// Reads an entire request body into memory and hands it to a +/// [`BodyReaderHandler`]. +pub struct BodyReader { + handler: H, body: Vec, - _wrap: PhantomData, } const MAX_BODY_SIZE: usize = 1024 * 1024 * 128; -impl BodyReaderMixin { - pub fn init() -> Self { - Self { +impl BodyReader { + /// Start reading `resp`'s body; `handler` is owned by the in-flight read. + pub fn read(handler: H, resp: &mut R) { + let raw: *mut Self = bun_core::heap::into_raw(Box::new(Self { + handler, body: Vec::new(), - _wrap: PhantomData, - } - } - - /// Memory is freed after the callback returns, or automatically on failure. - /// - /// Takes `*mut Wrap` (not `&mut self`) so the registered C user_data carries - /// provenance for the *entire* enclosing `Wrap`, not just the mixin field. - /// Deriving the parent by `.byte_sub(OFFSET)` from a - /// `&mut self`-sourced pointer is out-of-provenance under Stacked Borrows - /// and the resulting `&mut Wrap` would overlap a live `&mut Self`. Callers - /// pass the `heap::alloc`'d wrapper pointer directly; trampolines below - /// reach the mixin via *forward* offset (`mixin_of`), so the stored pointer - /// already has full-Wrap provenance and no overlapping `&mut` are formed. - pub fn read_body(wrap: *mut Wrap, resp: &mut R) { - resp.on_data(Self::on_data_generic::, wrap); - resp.on_aborted(Self::on_aborted_handler::, wrap); + })); + resp.on_data(Self::on_data::, raw); + resp.on_aborted(Self::on_aborted::, raw); } - /// Forward offset `Wrap` → its embedded mixin field, materialised as `&mut`. - /// We go parent→field because - /// the stored user_data is the parent (full provenance), never the field. - /// - /// Single nonnull-asref accessor for the set-once `wrap` user-data. - /// - /// Type invariant (encapsulated `unsafe`): every `*mut Wrap` reaching this - /// fn is the heap-allocated pointer registered by [`Self::read_body`] as - /// the uWS user-data; uWS dispatch is single-threaded and the only access - /// path to the allocation is via these crate-private trampolines, so no - /// other `&`/`&mut` into `*wrap` is live for the returned borrow's - /// duration. Each caller drops the returned `&mut Self` (NLL temporary) - /// before any `Wrap::on_body`/`on_error` call that may `heap::take(wrap)`. - /// Crate-private — collapses the per-call-site proof into this one block. - #[inline] - fn mixin_of<'a>(wrap: *mut Wrap) -> &'a mut Self { - // SAFETY: type invariant — see doc comment above. `IntrusiveField::OFFSET` - // is `offset_of!(Wrap, )`, so the result is in-bounds and inherits - // `wrap`'s provenance over the whole allocation. - unsafe { &mut *Wrap::field_of(wrap) } - } - - fn on_data_generic(wrap: *mut Wrap, r: &mut R, chunk: &[u8], last: bool) { + /// Detach both callbacks and take the reader back. `raw` is the pointer + /// registered by [`read`](Self::read); each uWS callback is the only path + /// to it and this is called at most once per reader (the callbacks are + /// cleared here, before anything can re-enter). + fn finish(raw: *mut Self, r: &mut R) -> Box { let any = r.to_any(); - match Self::on_data(wrap, any, chunk, last) { - Ok(()) => {} - Err(crate::Error::Alloc(_)) => Self::on_oom(wrap, any), - Err(_) => Self::on_invalid(wrap, any), - } + any.clear_on_data(); + any.clear_aborted(); + // SAFETY: see doc comment — `raw` is the live registration, reclaimed once. + unsafe { bun_core::heap::take(raw) } } - fn on_aborted_handler(wrap: *mut Wrap, _r: &mut R) { - // The temporary `&mut` from `mixin_of` ends at the `;`, before - // `on_error` (which may `heap::take(wrap)`). - Self::mixin_of(wrap).body = Vec::new(); - // SAFETY: wrap is the original heap-allocated pointer; the temporary - // &mut to the mixin field above has ended, so on_error receives sole - // ownership of the allocation and may heap::take it. - unsafe { Wrap::on_error(wrap) }; + fn on_aborted(raw: *mut Self, r: &mut R) { + drop(Self::finish(raw, r)); } - fn on_data(wrap: *mut Wrap, resp: AnyResponse, chunk: &[u8], last: bool) -> crate::Result<()> { - if last { - // Free everything after. Take via the mixin field first — no - // `&mut Wrap` is live yet, and the temporary `&mut Self` ends at - // the `;` (before `on_body`, which may heap::take(wrap)). - let mut body = mem::take(&mut Self::mixin_of(wrap).body); - resp.clear_on_data(); - if !body.is_empty() { - if body.len().saturating_add(chunk.len()) > MAX_BODY_SIZE { - return Err(crate::Error::RequestBodyTooLarge); - } - // Handle OOM gracefully here (error → 500); use try_reserve so - // allocation failure surfaces as an error instead of an abort. - if body.try_reserve(chunk.len()).is_err() { - return Err(crate::Error::Alloc(bun_alloc::AllocError)); - } - body.extend_from_slice(chunk); - // SAFETY: wrap is the original heap-allocated pointer; the &mut to - // mixin.body has ended, so on_body receives sole ownership of the - // allocation and may heap::take it on success. - unsafe { Wrap::on_body(wrap, body.as_slice(), resp)? }; + fn on_data(raw: *mut Self, r: &mut R, chunk: &[u8], last: bool) { + let result = if last { + let mut this = Self::finish(raw, r); + let any = r.to_any(); + let result = if this.body.is_empty() { + Self::check_len(0, chunk).and_then(|()| this.handler.on_body(chunk, any)) } else { - if chunk.len() > MAX_BODY_SIZE { - return Err(crate::Error::RequestBodyTooLarge); - } - // SAFETY: wrap is the original heap-allocated pointer; the &mut to - // mixin.body has ended, so on_body receives sole ownership of the - // allocation and may heap::take it on success. - unsafe { Wrap::on_body(wrap, chunk, resp)? }; - } - // `body` drops here - Ok(()) + let mut body = core::mem::take(&mut this.body); + Self::append(&mut body, chunk).and_then(|()| this.handler.on_body(&body, any)) + }; + drop(this); + result } else { - let body = &mut Self::mixin_of(wrap).body; - if body.len().saturating_add(chunk.len()) > MAX_BODY_SIZE { - return Err(crate::Error::RequestBodyTooLarge); - } - // Propagate OOM here too - // (error → 500); use try_reserve so allocation failure surfaces as an - // error instead of an abort. - if body.try_reserve(chunk.len()).is_err() { - return Err(crate::Error::Alloc(bun_alloc::AllocError)); + // SAFETY: `raw` is the live registration from `read`; uWS dispatch + // is single-threaded and this borrow ends before returning to it. + let this = unsafe { &mut *raw }; + match Self::append(&mut this.body, chunk) { + Ok(()) => return, + Err(e) => { + drop(Self::finish(raw, r)); + Err(e) + } } - body.extend_from_slice(chunk); - Ok(()) + }; + match result { + Ok(()) => {} + Err(crate::Error::Alloc(_)) => Self::fail(r.to_any(), b"500 Internal Server Error"), + Err(_) => Self::fail(r.to_any(), b"400 Bad Request"), } } - fn on_oom(wrap: *mut Wrap, r: AnyResponse) { - // The temporary `&mut` from `mixin_of` ends at the `;`, before - // `on_error` (which may `heap::take(wrap)`). - drop(mem::take(&mut Self::mixin_of(wrap).body)); - r.clear_aborted(); - r.clear_on_data(); - r.clear_on_writable(); - - r.write_status(b"500 Internal Server Error"); - r.end_without_body(true); - - // SAFETY: wrap is the original heap-allocated pointer; the &mut to - // mixin.body above has ended; on_error may heap::take it. - unsafe { Wrap::on_error(wrap) }; + fn check_len(have: usize, chunk: &[u8]) -> crate::Result<()> { + if have.saturating_add(chunk.len()) > MAX_BODY_SIZE { + return Err(crate::Error::RequestBodyTooLarge); + } + Ok(()) } - fn on_invalid(wrap: *mut Wrap, r: AnyResponse) { - // The temporary `&mut` from `mixin_of` ends at the `;`, before - // `on_error` (which may `heap::take(wrap)`). - drop(mem::take(&mut Self::mixin_of(wrap).body)); + fn append(body: &mut Vec, chunk: &[u8]) -> crate::Result<()> { + Self::check_len(body.len(), chunk)?; + // Surface OOM as an error (→ 500) instead of aborting. + if body.try_reserve(chunk.len()).is_err() { + return Err(crate::Error::Alloc(bun_alloc::AllocError)); + } + body.extend_from_slice(chunk); + Ok(()) + } - r.clear_aborted(); - r.clear_on_data(); + fn fail(r: AnyResponse, status: &[u8]) { r.clear_on_writable(); - - r.write_status(b"400 Bad Request"); + r.write_status(status); r.end_without_body(true); - - // SAFETY: wrap is the original heap-allocated pointer; the &mut to - // mixin.body above has ended; on_error may heap::take it. - unsafe { Wrap::on_error(wrap) }; } } diff --git a/src/uws_sys/Response.rs b/src/uws_sys/Response.rs index 1c9e2e9dd96e..62fb9519b33c 100644 --- a/src/uws_sys/Response.rs +++ b/src/uws_sys/Response.rs @@ -588,6 +588,26 @@ impl Response { ); } + /// [`upgrade`](Self::upgrade) for a [`WebSocketHandlerRef`](crate::web_socket::WebSocketHandlerRef) + /// handler: the socket's user-data slot takes over `data`'s ref, released + /// after `on_close`. + pub fn upgrade_ref( + &mut self, + data: bun_ptr::RefPtr, + sec_web_socket_key: &[u8], + sec_web_socket_protocol: &[u8], + sec_web_socket_extensions: &[u8], + ctx: Option<&mut WebSocketUpgradeContext>, + ) -> *mut Socket { + self.upgrade( + data.into_raw(), + sec_web_socket_key, + sec_web_socket_protocol, + sec_web_socket_extensions, + ctx, + ) + } + pub fn upgrade( &mut self, data: *mut D, diff --git a/src/uws_sys/WebSocket.rs b/src/uws_sys/WebSocket.rs index 2e325cbad9ac..3f52b3fbf72b 100644 --- a/src/uws_sys/WebSocket.rs +++ b/src/uws_sys/WebSocket.rs @@ -321,6 +321,73 @@ pub trait WebSocketHandler: Sized + 'static { unsafe fn on_close(this: *mut Self, ws: AnyWebSocket, code: i32, message: &[u8]); } +/// A [`WebSocketHandler`] that is an intrusively ref-counted heap object. +/// [`AnyWebSocket::upgrade_ref`] / [`uws::response::Response::upgrade_ref`] +/// move one ref into the socket's user-data slot; every callback runs under a +/// fresh ref guard (so `ws.close()` from inside a handler — which dispatches +/// `on_close` synchronously — cannot free the object under the running +/// handler); the socket's own ref is released after `on_close` returns. +pub trait WebSocketHandlerRef: + bun_ptr::AnyRefCounted + Sized + 'static +{ + const HAS_ON_MESSAGE: bool = true; + const HAS_ON_DRAIN: bool = true; + const HAS_ON_PING: bool = true; + const HAS_ON_PONG: bool = true; + + fn on_open(this: bun_ptr::ThisPtr, ws: AnyWebSocket); + fn on_message(this: bun_ptr::ThisPtr, ws: AnyWebSocket, message: &[u8], opcode: Opcode); + fn on_drain(_this: bun_ptr::ThisPtr, _ws: AnyWebSocket) {} + fn on_ping(_this: bun_ptr::ThisPtr, _ws: AnyWebSocket, _message: &[u8]) {} + fn on_pong(_this: bun_ptr::ThisPtr, _ws: AnyWebSocket, _message: &[u8]) {} + fn on_close(this: bun_ptr::ThisPtr, ws: AnyWebSocket, code: i32, message: &[u8]); +} + +impl WebSocketHandler for T { + const HAS_ON_MESSAGE: bool = ::HAS_ON_MESSAGE; + const HAS_ON_DRAIN: bool = ::HAS_ON_DRAIN; + const HAS_ON_PING: bool = ::HAS_ON_PING; + const HAS_ON_PONG: bool = ::HAS_ON_PONG; + + unsafe fn on_open(this: *mut Self, ws: AnyWebSocket) { + // SAFETY: trait contract — `this` is the socket's user-data, which holds a ref (`upgrade_ref`). + let this = unsafe { bun_ptr::ThisPtr::new(this) }; + let _keep = this.ref_guard(); + ::on_open(this, ws) + } + unsafe fn on_message(this: *mut Self, ws: AnyWebSocket, message: &[u8], opcode: Opcode) { + // SAFETY: see `on_open`. + let this = unsafe { bun_ptr::ThisPtr::new(this) }; + let _keep = this.ref_guard(); + ::on_message(this, ws, message, opcode) + } + unsafe fn on_drain(this: *mut Self, ws: AnyWebSocket) { + // SAFETY: see `on_open`. + let this = unsafe { bun_ptr::ThisPtr::new(this) }; + let _keep = this.ref_guard(); + ::on_drain(this, ws) + } + unsafe fn on_ping(this: *mut Self, ws: AnyWebSocket, message: &[u8]) { + // SAFETY: see `on_open`. + let this = unsafe { bun_ptr::ThisPtr::new(this) }; + let _keep = this.ref_guard(); + ::on_ping(this, ws, message) + } + unsafe fn on_pong(this: *mut Self, ws: AnyWebSocket, message: &[u8]) { + // SAFETY: see `on_open`. + let this = unsafe { bun_ptr::ThisPtr::new(this) }; + let _keep = this.ref_guard(); + ::on_pong(this, ws, message) + } + unsafe fn on_close(this: *mut Self, ws: AnyWebSocket, code: i32, message: &[u8]) { + // SAFETY: see `on_open`; uWS dispatches `on_close` exactly once and never + // calls back into this user-data afterwards, so the socket's ref is released here. + let socket_ref = unsafe { bun_ptr::RefPtr::::from_raw(this) }; + ::on_close(socket_ref.this_ptr(), ws, code, message); + socket_ref.deref(); + } +} + /// Server type that handles the HTTP→WS upgrade. `this` is the user-data /// registered with the `.ws()` route ([`crate::app::App::ws_this`] ties its /// type to `Self`; the raw [`crate::app::App::ws`] leaves that to its caller). diff --git a/src/uws_sys/lib.rs b/src/uws_sys/lib.rs index e1ac6fdd9e3b..60a11b1b5fe9 100644 --- a/src/uws_sys/lib.rs +++ b/src/uws_sys/lib.rs @@ -483,7 +483,7 @@ pub use socket_kind::SocketKind; #[cfg(windows)] pub use timer::Timer; -pub use body_reader_mixin::BodyReaderMixin; +pub use body_reader_mixin::{BodyReader, BodyReaderHandler}; pub use connecting_socket::ConnectingSocket; pub use listen_socket::ListenSocket; pub use request::{AnyRequest, Request}; diff --git a/src/watcher/Watcher.rs b/src/watcher/Watcher.rs index 177487c6fe4c..827e1194c078 100644 --- a/src/watcher/Watcher.rs +++ b/src/watcher/Watcher.rs @@ -132,10 +132,51 @@ pub struct Watcher { pub(crate) ctx: *mut (), pub(crate) on_file_update: fn(*mut (), &mut [WatchEvent], &[ChangedFilePath], &WatchList), pub(crate) on_error: fn(*mut (), sys::Error), + /// Owned callback receiver (see [`Watcher::init_with_handler`]); when set, + /// `ctx`/`on_file_update`/`on_error` are unused. + pub(crate) handler: Option>, pub thread_lock: ThreadLock, } +/// A batch of file-change events being delivered to a [`WatcherHandler`] on +/// the watcher thread while `Watcher.mutex` is held. The handler may evict +/// entries through [`watcher_mut`](Self::watcher_mut) +/// (`remove_at_index::` + `flush_evictions`). +pub struct FileUpdateBatch<'a> { + watcher: &'a mut Watcher, + event_count: usize, + changed_count: usize, +} + +impl FileUpdateBatch<'_> { + #[inline] + pub fn events(&self) -> &[WatchEvent] { + &self.watcher.watch_events[..self.event_count] + } + /// Platform buffer of changed names inside watched directories; index it + /// with [`WatchEvent::names`]. + #[inline] + pub fn changed_files(&self) -> &[ChangedFilePath] { + &self.watcher.changed_filepaths[..self.changed_count] + } + #[inline] + pub fn watcher(&self) -> &Watcher { + self.watcher + } + #[inline] + pub fn watcher_mut(&mut self) -> &mut Watcher { + self.watcher + } +} + +/// Callback receiver owned by a [`Watcher`] (see [`Watcher::init_with_handler`]). +/// Both methods run on the watcher thread. +pub trait WatcherHandler: Send + 'static { + fn on_file_update(&mut self, batch: &mut FileUpdateBatch<'_>); + fn on_error(&mut self, err: sys::Error); +} + /// Context types passed to `Watcher::init` implement this trait. /// The default `on_watch_error` forwards to `on_error`. pub trait WatcherContext { @@ -187,13 +228,47 @@ impl Watcher { unsafe { (*ctx_opaque.cast::()).on_watch_error(err) } } + Self::init_raw( + ctx.cast::<()>(), + on_file_update_wrapped::, + on_error_wrapped::, + None, + top_level_dir, + ) + } + + /// [`init`](Self::init) with an owned callback receiver instead of a raw + /// context pointer. + pub fn init_with_handler( + handler: Box, + top_level_dir: &'static [u8], + ) -> Result, crate::Error> { + fn unused_update(_: *mut (), _: &mut [WatchEvent], _: &[ChangedFilePath], _: &WatchList) {} + fn unused_error(_: *mut (), _: sys::Error) {} + Self::init_raw( + core::ptr::null_mut(), + unused_update, + unused_error, + Some(handler), + top_level_dir, + ) + } + + fn init_raw( + ctx: *mut (), + on_file_update: fn(*mut (), &mut [WatchEvent], &[ChangedFilePath], &WatchList), + on_error: fn(*mut (), sys::Error), + handler: Option>, + top_level_dir: &'static [u8], + ) -> Result, crate::Error> { let this = Box::new(Watcher { watchlist: WatchList::default(), mutex: Mutex::default(), cwd: top_level_dir, - ctx: ctx.cast::<()>(), - on_file_update: on_file_update_wrapped::, - on_error: on_error_wrapped::, + ctx, + on_file_update, + on_error, + handler, platform: Platform::new(top_level_dir)?, watch_events: vec![WatchEvent::default(); MAX_COUNT].into_boxed_slice(), changed_filepaths: [const { None }; MAX_COUNT], @@ -221,14 +296,24 @@ impl Watcher { /// and `self.changed_filepaths[..changed_count]` and calls this instead of /// open-coding the lock/trace/callback sequence. pub(crate) fn dispatch_file_updates(&mut self, event_count: usize, changed_count: usize) { - let _guard = self.mutex.lock_guard(); - if !self.running.load() { - return; + self.mutex.lock(); + if self.running.load() { + let events = &mut self.watch_events[..event_count]; + let changed = &self.changed_filepaths[..changed_count]; + WatcherTrace::write_events(&self.watchlist, events, changed); + match self.handler.take() { + Some(mut handler) => { + handler.on_file_update(&mut FileUpdateBatch { + watcher: self, + event_count, + changed_count, + }); + self.handler = Some(handler); + } + None => (self.on_file_update)(self.ctx, events, changed, &self.watchlist), + } } - let events = &mut self.watch_events[..event_count]; - let changed = &self.changed_filepaths[..changed_count]; - WatcherTrace::write_events(&self.watchlist, events, changed); - (self.on_file_update)(self.ctx, events, changed, &self.watchlist); + self.mutex.unlock(); } pub fn start(&mut self) -> Result<(), crate::Error> { @@ -313,6 +398,13 @@ impl Watcher { } } + /// [`shutdown`](Self::shutdown) for the owning `Box` returned by + /// [`init`](Self::init) / [`init_with_handler`](Self::init_with_handler). + pub fn shutdown_boxed(self: Box, close_descriptors: bool) { + // SAFETY: `self` is the unique owning box, released here. + unsafe { Self::shutdown(Box::into_raw(self), close_descriptors) } + } + pub fn get_hash(filepath: &[u8]) -> HashType { bun_wyhash::hash(filepath) as HashType } @@ -359,7 +451,10 @@ impl Watcher { self.platform.stop(); let running = self.running.load(); if running { - (self.on_error)(self.ctx, err); + match self.handler.as_mut() { + Some(handler) => handler.on_error(err), + None => (self.on_error)(self.ctx, err), + } } running } diff --git a/src/watcher/lib.rs b/src/watcher/lib.rs index 76619cf6836e..71dccce7226a 100644 --- a/src/watcher/lib.rs +++ b/src/watcher/lib.rs @@ -35,7 +35,8 @@ pub use error::{Error, Result}; pub use WatchItemKind as Kind; pub use watcher_impl::{ - AnyResolveWatcher, ChangedFilePath, Event, FdOwnership, HashType, MAX_COUNT, + AnyResolveWatcher, ChangedFilePath, Event, FdOwnership, FileUpdateBatch, HashType, MAX_COUNT, MAX_EVICTION_COUNT, Op, PackageJSON, REQUIRES_FILE_DESCRIPTORS, WATCH_OPEN_FLAGS, WatchEvent, WatchItem, WatchItemColumns, WatchItemIndex, WatchItemKind, WatchList, Watcher, WatcherContext, + WatcherHandler, }; From a433a2f07f7182697d7cf6137bdaa71168e70428 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 20:48:12 +0000 Subject: [PATCH 02/12] bake: drop stale comments --- src/runtime/bake/DevServer.rs | 10 ++-------- src/runtime/bake/dev_server/mod.rs | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 24eded8e174b..7488e5edaea1 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -200,11 +200,6 @@ pub struct Options<'a> { // implemented. Callers construct `Options` via struct-literal at the call site // (see `bake_body.rs::UserOptions::into_dev_server_options`). -// The fields `client_graph`, `server_graph`, `directory_watchers`, and `assets` -// all use `@fieldParentPointer` to access DevServer's state. This pattern has -// made it easier to group related fields together, but one must remember those -// structures still depend on the DevServer pointer. - #[cfg(debug_assertions)] #[repr(u128)] #[derive(Copy, Clone, Eq, PartialEq)] @@ -272,7 +267,6 @@ pub struct CurrentBundle { pub(crate) had_reload_event: bool, /// After a bundle finishes, these requests will be continued, either /// calling their handler on success or sending the error page on failure. - /// Owned by `deferred_request_pool` in DevServer. pub(crate) requests: deferred_request::List, /// Resolution failures are grouped by incremental graph file index. /// Unlike parse failures (`handleParseTaskFailure`), the resolution @@ -4200,13 +4194,13 @@ fn finalize_bundle_serve_requests(dev: &mut DevServer) -> JsResult<()> { // Note: `SavedRequest` is move-only (`Strong` field). Take the // handler by value so the `Saved` payload moves into the union; the - // node is being torn down via `_deref` regardless. + // request is being released regardless. match req.handler.replace(Handler::Aborted) { Handler::Aborted => continue, Handler::ServerHandler(saved) => { let response = saved.response; let ctx = saved.ctx; - // Note: `saved` is moved out (so `__deinit` sees `Aborted`); + // Note: `saved` is moved out (so `release` sees `Aborted`); // `js_request: StrongOptional` releases on Drop, but // `ctx: AnyRequestContext` is `Copy` — explicitly balance the // `ctx.ref_()` from `defer_request` here so the request diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index e93ef06443de..5e5cd22302f3 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -37,7 +37,7 @@ pub(crate) const CLIENT_PREFIX: &str = "/_bun/client"; // LAYERING: the 4.8 kL of method bodies live in `../DevServer.rs` (mounted as // `super::dev_server_body`). The struct definitions are owned there so impl -// blocks and `container_of` submodules name a single type. Re-export so +// blocks and submodules name a single type. Re-export so // `crate::bake::dev_server::DevServer` (the public path used by `server/`, // `dispatch.rs`, …) resolves to that one struct. pub use super::dev_server_body::{ From dfdbb6a3f4dcbb049348ef41ab866d0c1a43f4bf Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 21:10:21 +0000 Subject: [PATCH 03/12] bake: run user JS outside DevServer borrows; review fixes finalize_bundle is driven on the DevServerCell in phases (graph update, server patch load, hot update, serve), with the framework handler, aborted-request signal handlers and microtask checkpoints between the with_mut sections; start_async_bundle enqueues entry points (plugin onResolve) between borrows and takes entry-point resolve failures back from the bundler instead of a re-entrant dispatch; plugin loading in ensure_route_is_bundled happens between borrows. Also: abort pending next-bundle requests on drop, keep the hot-reload slot lock off the graph work, DevServerHandle::from_owner(BackRef), HeadersRef::create(&[..]). --- src/bundler/bundle_v2.rs | 49 +- src/bundler/lib.rs | 13 +- src/http_jsc/headers_jsc.rs | 15 +- src/jsc/FetchHeaders.rs | 19 +- src/runtime/bake/DevServer.rs | 2107 ++++++++++++--------- src/runtime/bake/dev_server/assets.rs | 3 - src/runtime/bake/dev_server/hmr_socket.rs | 12 +- src/runtime/bake/dev_server/mod.rs | 61 +- src/runtime/server/HTMLBundle.rs | 17 +- src/runtime/server/mod.rs | 10 +- src/runtime/server/server_body.rs | 13 +- 11 files changed, 1306 insertions(+), 1013 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 00ba44ff481f..c20182a5b59e 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3229,6 +3229,7 @@ pub mod bv2_impl { &mut self, files: &bake_types::EntryPointList, css_data: &mut ArrayHashMap, + resolve_failures: &mut Vec, ) -> Result<(), Error> { self.enqueue_entry_points_common()?; debug_assert!(self.dev_server.is_some()); @@ -3299,22 +3300,22 @@ pub mod bv2_impl { let mut resolved = match unsafe { &mut *transpiler }.resolve_entry_point(abs_path) { Ok(r) => r, Err(err) => { - let dev = self.dev_server.expect("unreachable"); - dev.handle_parse_task_failure( + // The dev server handles these once `start_from_bake_dev_server` + // returns (it is mid-borrow while this runs). + resolve_failures.push(EntryPointResolveFailure { err, - if flags.client() { + graph: if flags.client() { bake::Graph::Client } else { bake::Graph::Server }, - abs_path, - // SAFETY: `transpiler` points at one of self's transpilers, live for `'a`. - unsafe { (*transpiler).log }.cast_const(), - std::ptr::from_mut(self), - ) - .expect("oom"); - // SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here. - unsafe { (*(*transpiler).log).reset() }; + abs_path: Box::from(&**abs_path), + // SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here. + log: core::mem::replace( + unsafe { &mut *(*transpiler).log }, + bun_ast::Log::init(), + ), + }); continue; } }; @@ -5466,10 +5467,14 @@ pub mod bv2_impl { } /// Dev Server uses this instead to run a subset of the transpiler, and to run it asynchronously. + /// Entry points that failed to resolve are returned for the dev server + /// to record (`DevServer::handle_parse_task_failure`) rather than + /// reported through the dispatch handle, because the dev server is + /// mid-borrow while this runs. pub fn start_from_bake_dev_server( &mut self, bake_entry_points: &bake_types::EntryPointList, - ) -> Result { + ) -> Result<(DevServerInput, Vec), Error> { self.unique_key = generate_unique_key(); /* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */ @@ -5477,11 +5482,16 @@ pub mod bv2_impl { let mut ctx = DevServerInput { css_entry_points: ArrayHashMap::new(), }; - self.enqueue_entry_points_dev_server(bake_entry_points, &mut ctx.css_entry_points)?; + let mut resolve_failures = Vec::new(); + self.enqueue_entry_points_dev_server( + bake_entry_points, + &mut ctx.css_entry_points, + &mut resolve_failures, + )?; /* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */ - Ok(ctx) + Ok((ctx, resolve_failures)) } // The body has deep DevServer field access (current_bundle.start_data, @@ -7985,6 +7995,17 @@ pub mod bv2_impl { pub(crate) css_entry_points: ArrayHashMap, } + /// A dev-server entry point that failed to resolve during + /// `start_from_bake_dev_server`. + pub struct EntryPointResolveFailure { + pub err: Error, + pub graph: bake_types::Graph, + pub abs_path: Box<[u8]>, + /// The resolver's log messages for this failure (taken from the + /// transpiler log, which is left empty). + pub log: bun_ast::Log, + } + /// The lifetime of this structure is tied to the bundler's arena pub struct DevServerOutput<'a> { pub chunks: &'a mut [Chunk], diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 499be8b050fc..aa6dacfeec47 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -355,13 +355,14 @@ bun_dispatch::link_interface! { } } impl DevServerHandle { - /// The handle for a dev server reached through a dispatch-time `ThisPtr` - /// (whose invariant — the pointee stays live for every use through it and - /// its copies — is exactly `of`'s). + /// The handle for the dev server `owner` points at. The handle is another + /// back-reference: whoever stores it takes on `owner`'s holder obligation + /// (must be dropped before the pointee). #[inline] - pub fn from_this(this: bun_ptr::ThisPtr) -> Self { - // SAFETY: see doc comment. - unsafe { Self::of(this.as_ptr()) } + pub fn from_owner(owner: bun_ptr::BackRef) -> Self { + // SAFETY: `BackRef` invariant — the pointee is live while the handle's + // holder (bound by the same obligation) uses it. + unsafe { Self::of(owner.this_ptr().as_ptr()) } } } // SAFETY: the handle is `{ kind, owner: *mut () }`; the raw pointer is what diff --git a/src/http_jsc/headers_jsc.rs b/src/http_jsc/headers_jsc.rs index ad4c058d53ba..7bee55d65fae 100644 --- a/src/http_jsc/headers_jsc.rs +++ b/src/http_jsc/headers_jsc.rs @@ -153,20 +153,13 @@ pub fn to_fetch_headers_ref( if this.entries.len() == 0 { return Ok(bun_jsc::fetch_headers::HeadersRef::create_empty()); } - let names: &[StringPointer] = this.entries.items_name(); - let values: &[StringPointer] = this.entries.items_value(); - // SAFETY: `names`/`values` point into live slices of `this.entries.len()` - // entries; C++ reads exactly `count_` of each and does not retain the pointers. bun_jsc::fetch_headers::HeadersRef::create( global, - // C++ side reads only; cast_mut() is safe (no mutation). - names.as_ptr().cast_mut(), - values.as_ptr().cast_mut(), - // `from_bytes` scans for - // non-ASCII and tags UTF-8; `init` would leave the buffer Latin-1 - // and mojibake any UTF-8 header value bytes ≥0x80. + this.entries.items_name(), + this.entries.items_value(), + // `from_bytes` scans for non-ASCII and tags UTF-8; `init` would leave + // the buffer Latin-1 and mojibake any UTF-8 header value bytes ≥0x80. &ZigString::from_bytes(this.buf.as_slice()), - this.entries.len() as u32, ) .ok_or(JsError::Thrown) } diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index 6ee8bdf0abe9..b157452e45dc 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -337,16 +337,25 @@ impl HeadersRef { Self(FetchHeaders::create_empty()) } - /// [`FetchHeaders::create`], owning the fresh allocation. + /// [`FetchHeaders::create`] from parallel name/value columns into `buf`, + /// owning the fresh allocation. #[inline] pub fn create( global: &JSGlobalObject, - names: *mut StringPointer, - values: *mut StringPointer, + names: &[StringPointer], + values: &[StringPointer], buf: &ZigString, - count_: u32, ) -> Option { - FetchHeaders::create(global, names, values, buf, count_).map(Self) + assert_eq!(names.len(), values.len()); + // C++ only reads the columns. + FetchHeaders::create( + global, + names.as_ptr().cast_mut(), + values.as_ptr().cast_mut(), + buf, + names.len() as u32, + ) + .map(Self) } /// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1. diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 7488e5edaea1..8f929bb8a14e 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -278,6 +278,24 @@ pub struct CurrentBundle { pub(crate) promise: DeferredPromise, } +/// A request to `DevServer::start_async_bundle`. +pub(crate) struct BundleRequest { + pub(crate) entry_points: EntryPointList, + pub(crate) had_reload_event: bool, + /// When the work that led to this bundle started. + pub(crate) timer: Instant, +} + +/// A bundle set up by `start_async_bundle_setup`, before its entry points are +/// enqueued. +struct BundleSetup { + bv2: Box>, + /// Lives in `heap`; AST nodes built while the entry points are enqueued go + /// through it (see `start_async_bundle`). + ast_memory_store: &'static mut bun_ast::ASTMemoryAllocator, + heap: Box, +} + pub struct NextBundle { /// A list of `RouteBundle`s which have active requests to bundle it. pub(crate) route_queue: ArrayHashMap, @@ -923,6 +941,9 @@ impl Drop for DevServer { for request in ::core::mem::take(&mut self.next_bundle.requests) { debug_assert!(!matches!(*request.handler.get(), Handler::ServerHandler(_))); + // Ends the response, which still had this request as its abort + // handler's user-data. + request.abort(); DeferredRequest::release(request); } self.next_bundle.promise.deinit_idempotently(); @@ -1258,7 +1279,7 @@ fn dev_route( // is where what bundling for it left pending is folded. The user's // JS runs after the dev server borrow ends. let server = cell.get().server; - let result = match cell.with_mut(|dev| on_request(dev, req, resp)) { + let result = match on_request(cell, req, resp) { Ok(RequestAction::Done) => Ok(()), Ok(RequestAction::UserHandler) => { server.on_request(req, resp); @@ -1581,11 +1602,65 @@ trait EnsureRouteCtx { fn to_dev_response(&mut self, dev: &mut DevServer) -> DevResponse<'_>; } +/// What `ensure_route_is_bundled_step` needs done outside the dev server +/// borrow before it can continue. +enum EnsureRouteStep { + Done, + /// Ask the server for its plugins (which may evaluate them — user JS). + LoadPlugins, + /// Bundle this route now. + StartBundle(EntryPointList), +} + fn ensure_route_is_bundled( - dev: &mut DevServer, + cell: &DevServerCell, route_bundle_index: route_bundle::Index, ctx: &mut Ctx, ) -> JsResult<()> { + loop { + match cell.with_mut(|dev| ensure_route_is_bundled_step(dev, route_bundle_index, ctx))? { + EnsureRouteStep::Done => return Ok(()), + EnsureRouteStep::StartBundle(entry_points) => { + DevServer::start_async_bundle( + cell, + BundleRequest { + entry_points, + had_reload_event: false, + timer: Instant::now(), + }, + ) + .expect("oom"); + return Ok(()); + } + EnsureRouteStep::LoadPlugins => { + let server = cell.get().server; + let load_result = + server.get_or_load_plugins(crate::server::ServePluginsCallback::DevServer); + cell.with_mut(|dev| match load_result { + crate::server::GetOrStartLoadResult::Pending => { + // A synchronously settled load already moved this on. + if dev.plugin_state == PluginState::Unknown { + dev.plugin_state = PluginState::Pending; + } + } + crate::server::GetOrStartLoadResult::Err => { + dev.plugin_state = PluginState::Err; + } + crate::server::GetOrStartLoadResult::Ready(ready) => { + dev.plugin_state = PluginState::Loaded; + dev.bundler_options.plugin = ready.map(::core::ptr::NonNull::from); + } + }); + } + } + } +} + +fn ensure_route_is_bundled_step( + dev: &mut DevServer, + route_bundle_index: route_bundle::Index, + ctx: &mut Ctx, +) -> JsResult { debug_assert!(dev.magic == Magic::Valid); let mut state = dev.route_bundle_ptr(route_bundle_index).server_state; 'sw: loop { @@ -1597,70 +1672,46 @@ fn ensure_route_is_bundled( ctx.on_defer(dev, BundleQueueType::NextBundle)?; dev.route_bundle_ptr(route_bundle_index).server_state = route_bundle::State::DeferredToNextBundle; - return Ok(()); + return Ok(EnsureRouteStep::Done); } // No current bundle, we'll create a bundle with just this route, but first: // If plugins are not yet loaded, prepare them. - let mut plugin = dev.plugin_state; - 'plugin: loop { - match plugin { - PluginState::Unknown => { - if dev.bundler_options.plugin.is_some() { - // Framework-provided plugin is likely going to be phased out later - dev.plugin_state = PluginState::Loaded; + match dev.plugin_state { + PluginState::Unknown => { + if dev.bundler_options.plugin.is_some() { + // Framework-provided plugin is likely going to be phased out later + dev.plugin_state = PluginState::Loaded; + } else { + // TODO: implement a proper solution here + let has_tailwind = if let Some(serve_plugins) = + &dev.vm().transpiler.options.serve_plugins + { + serve_plugins + .iter() + .find(|p| strings::includes(p, b"tailwind")) + .map(|_| Default::default()) } else { - // TODO: implement a proper solution here - let has_tailwind = if let Some(serve_plugins) = - &dev.vm().transpiler.options.serve_plugins - { - serve_plugins - .iter() - .find(|p| strings::includes(p, b"tailwind")) - .map(|_| Default::default()) - } else { - None - }; - dev.has_tailwind_plugin_hack = has_tailwind; - - let load_result: crate::server::GetOrStartLoadResult = - dev.server.get_or_load_plugins( - crate::server::ServePluginsCallback::DevServer, - ); - match load_result { - crate::server::GetOrStartLoadResult::Pending => { - dev.plugin_state = PluginState::Pending; - plugin = PluginState::Pending; - continue 'plugin; - } - crate::server::GetOrStartLoadResult::Err => { - dev.plugin_state = PluginState::Err; - plugin = PluginState::Err; - continue 'plugin; - } - crate::server::GetOrStartLoadResult::Ready(ready) => { - dev.plugin_state = PluginState::Loaded; - dev.bundler_options.plugin = - ready.map(::core::ptr::NonNull::from); - } - } - } - break 'plugin; - } - PluginState::Pending => { - dev.next_bundle.route_queue.put(route_bundle_index, ())?; - ctx.on_defer(dev, BundleQueueType::NextBundle)?; - dev.route_bundle_ptr(route_bundle_index).server_state = - route_bundle::State::DeferredToNextBundle; - return Ok(()); - } - PluginState::Err => { - // TODO: render plugin error page - ctx.on_plugin_error(dev)?; - return Ok(()); + None + }; + dev.has_tailwind_plugin_hack = has_tailwind; + // Comes back here with `plugin_state` moved on. + return Ok(EnsureRouteStep::LoadPlugins); } - PluginState::Loaded => break 'plugin, } + PluginState::Pending => { + dev.next_bundle.route_queue.put(route_bundle_index, ())?; + ctx.on_defer(dev, BundleQueueType::NextBundle)?; + dev.route_bundle_ptr(route_bundle_index).server_state = + route_bundle::State::DeferredToNextBundle; + return Ok(EnsureRouteStep::Done); + } + PluginState::Err => { + // TODO: render plugin error page + ctx.on_plugin_error(dev)?; + return Ok(EnsureRouteStep::Done); + } + PluginState::Loaded => {} } // Prepare a bundle with just this route. @@ -1689,9 +1740,7 @@ fn ensure_route_is_bundled( dev.route_bundle_ptr(route_bundle_index).server_state = route_bundle::State::Bundling; - dev.start_async_bundle(entry_points, false, Instant::now()) - .expect("oom"); - return Ok(()); + return Ok(EnsureRouteStep::StartBundle(entry_points)); } route_bundle::State::DeferredToNextBundle => { debug_assert!( @@ -1701,12 +1750,12 @@ fn ensure_route_is_bundled( .is_some() ); ctx.on_defer(dev, BundleQueueType::NextBundle)?; - return Ok(()); + return Ok(EnsureRouteStep::Done); } route_bundle::State::Bundling => { debug_assert!(dev.current_bundle.is_some()); ctx.on_defer(dev, BundleQueueType::CurrentBundle)?; - return Ok(()); + return Ok(EnsureRouteStep::Done); } route_bundle::State::PossibleBundlingFailures => { if !dev.bundling_failures.is_empty() { @@ -1714,7 +1763,7 @@ fn ensure_route_is_bundled( // reachable by this route. let resp = ctx.to_dev_response(dev); match check_route_failures(dev, route_bundle_index, resp)? { - CheckResult::Stop => return Ok(()), + CheckResult::Stop => return Ok(EnsureRouteStep::Done), CheckResult::Ok => {} // Errors were cleared or not in the way. CheckResult::Rebuild => { state = route_bundle::State::Unqueued; @@ -1729,7 +1778,7 @@ fn ensure_route_is_bundled( } route_bundle::State::Loaded => { ctx.on_loaded(dev)?; - return Ok(()); + return Ok(EnsureRouteStep::Done); } } } @@ -2581,12 +2630,92 @@ pub struct ResponseAndMethod { } impl DevServer { + /// Start bundling `request.entry_points`. The bundler's entry-point setup + /// can run plugin (`onResolve`) JavaScript, so it runs between — not + /// inside — the dev server borrows. pub(crate) fn start_async_bundle( - &mut self, - entry_points: EntryPointList, - had_reload_event: bool, - timer: Instant, + cell: &DevServerCell, + request: BundleRequest, ) -> crate::Result<()> { + let BundleRequest { + entry_points, + had_reload_event, + timer, + } = request; + let BundleSetup { + mut bv2, + ast_memory_store, + heap, + } = cell.with_mut(|dev| dev.start_async_bundle_setup(&entry_points))?; + + // AST nodes built while the entry points are enqueued live exactly as + // long as the bundle: the `AstAllocState` is taken into `CurrentBundle` + // below and released by the guard on every path. + let mut ast_memory_store = + scopeguard::guard(ast_memory_store, |store| store.release_ast_state()); + let ast_scope = ast_memory_store.enter(); + + // LAYERING: `bun_bundler::bake_types::EntryPointList` is the TYPE_ONLY + // mirror of this file's `EntryPointList` (moved down so `bun_bundler` + // can name it without depending on `bun_runtime`). Convert by value — + // both `Flags` are `#[repr(transparent)] u8` with identical bit layout. + let started = bv2.start_from_bake_dev_server(&{ + let mut bt = bundler::bake_types::EntryPointList::empty(); + for (k, v) in entry_points.set.iter() { + bun_core::handle_oom( + bt.set + .put(k, bundler::bake_types::EntryPointFlags(v.bits())), + ); + } + bt + }); + drop(entry_points); + + let start_data = cell.with_mut(|dev| -> crate::Result<_> { + let (start_data, resolve_failures) = started?; + for failure in resolve_failures { + dev.handle_parse_task_failure( + &crate::Error::from(failure.err), + failure.graph, + &failure.abs_path, + &failure.log, + &mut bv2, + )?; + } + Ok(start_data) + })?; + // End the AST scope and move its state into the bundle so the small + // `AstVec`s built during setup stay alive until the bundle completes. + drop(ast_scope); + let ast_alloc_state = ast_memory_store.take_ast_state(); + drop(ast_memory_store); + + cell.with_mut(move |dev| { + dev.current_bundle = Some(CurrentBundle { + bv2, + heap, + ast_alloc_state, + timer, + start_data, + had_reload_event, + requests: ::core::mem::take(&mut dev.next_bundle.requests), + promise: ::core::mem::take(&mut dev.next_bundle.promise), + resolution_failure_entries: Default::default(), + }); + + dev.next_bundle.promise = DeferredPromise::default(); + dev.next_bundle.requests = deferred_request::List::default(); + dev.next_bundle.route_queue.clear_retaining_capacity(); + }); + Ok(()) + } + + /// Everything `start_async_bundle` does before handing the entry points + /// to the bundler. + fn start_async_bundle_setup( + &mut self, + entry_points: &EntryPointList, + ) -> crate::Result { debug_assert!(self.current_bundle.is_none()); debug_assert!(!entry_points.set.is_empty()); self.log.clear_and_free(); @@ -2615,14 +2744,7 @@ impl DevServer { None => ::core::ptr::NonNull::from(&mut *server_transpiler), }; - // AST nodes built during bundle setup live exactly as long as the - // bundle: the `AstAllocState` is taken into `CurrentBundle` on success - // and released by the guard on every path. - let mut ast_memory_store = scopeguard::guard( - heap_ref.alloc(bun_ast::ASTMemoryAllocator::borrowing(heap_ref)), - |store| store.release_ast_state(), - ); - let ast_scope = ast_memory_store.enter(); + let ast_memory_store = heap_ref.alloc(bun_ast::ASTMemoryAllocator::borrowing(heap_ref)); // The bundler stores `Option>`; park the value // in `heap` so it lives exactly as long as `bv2`. @@ -2660,50 +2782,24 @@ impl DevServer { self.graph_safety_lock.unlock(); } - // LAYERING: `bun_bundler::bake_types::EntryPointList` is the TYPE_ONLY - // mirror of this file's `EntryPointList` (moved down so `bun_bundler` - // can name it without depending on `bun_runtime`). Convert by value — - // both `Flags` are `#[repr(transparent)] u8` with identical bit layout. - let start_data = bv2.start_from_bake_dev_server(&{ - let mut bt = bundler::bake_types::EntryPointList::empty(); - for (k, v) in entry_points.set.iter() { - bun_core::handle_oom( - bt.set - .put(k, bundler::bake_types::EntryPointFlags(v.bits())), - ); - } - bt - })?; - drop(entry_points); - // End the AST scope and move its state into the bundle so the small - // `AstVec`s built during setup stay alive until the bundle completes. - drop(ast_scope); - let ast_alloc_state = ast_memory_store.take_ast_state(); - drop(ast_memory_store); - self.current_bundle = Some(CurrentBundle { + Ok(BundleSetup { bv2, + ast_memory_store, heap, - ast_alloc_state, - timer, - start_data, - had_reload_event, - requests: ::core::mem::take(&mut self.next_bundle.requests), - promise: ::core::mem::take(&mut self.next_bundle.promise), - resolution_failure_entries: Default::default(), - }); - - self.next_bundle.promise = DeferredPromise::default(); - self.next_bundle.requests = deferred_request::List::default(); - self.next_bundle.route_queue.clear_retaining_capacity(); - Ok(()) + }) } /// The two borrows a `BundleV2<'static>` holds for the lifetime of a /// bundle: the server transpiler (`bv2.transpiler`) and the bundle's heap /// (`bv2.graph.heap`). `BundleV2<'a>` ties both to one `'a`, which for the - /// dev server's `Transpiler<'static>` must be `'static`; the transpiler is - /// boxed in `self` and the heap is boxed in the `CurrentBundle` next to - /// `bv2`, and `finalize_bundle_cleanup` drops `bv2` before either. + /// dev server's `Transpiler<'static>` must be `'static`. Liveness: the + /// transpiler is boxed in `self` and the heap is boxed in the + /// `CurrentBundle` next to `bv2`, and `finalize_bundle_cleanup` drops `bv2` + /// before either. Aliasing: this is NOT exclusive — the dev server keeps + /// using `self.server_transpiler` (its resolver) while the bundle runs, as + /// the bundler's own `client_transpiler`/`ssr_transpiler` `NonNull`s + /// already do. Goes away when `BundleV2` takes its primary transpiler the + /// same way (`NonNull`/`BackRef`) and owns the dev-server bundle heap. fn bundle_borrows( &mut self, heap: &bun_alloc::MimallocArena, @@ -2711,8 +2807,8 @@ impl DevServer { &'static mut Transpiler<'static>, &'static bun_alloc::MimallocArena, ) { - // SAFETY: see doc comment — both pointees outlive the `BundleV2` that - // receives these borrows; `BundleV2` needs an owning-handle API to say so. + // SAFETY: see doc comment (liveness holds; exclusivity is the bundler's + // existing shared-transpiler contract, not established here). unsafe { ( bun_ptr::detach_lifetime_mut(&mut *self.server_transpiler), @@ -3227,7 +3323,11 @@ impl<'a> HotUpdateContext<'a> { } } -fn finalize_bundle_cleanup(dev: &mut DevServer, bv2: &mut BundleV2, had_sent_hmr_event: bool) { +fn finalize_bundle_cleanup( + dev: &mut DevServer, + bv2: &mut BundleV2, + had_sent_hmr_event: bool, +) -> Option { bv2.deinit_without_freeing_arena(); if let Some(cb) = &mut dev.current_bundle { cb.promise.deinit_idempotently(); @@ -3260,64 +3360,152 @@ fn finalize_bundle_cleanup(dev: &mut DevServer, bv2: &mut BundleV2, had_sent_hmr ); } - dev.start_next_bundle_if_present(); - - // Unref the ref added in `start_async_bundle` - dev.server.on_static_request_complete(); -} - -fn drain_current_bundle_requests(current_bundle: &mut CurrentBundle) { - if !current_bundle.requests.is_empty() { - // cannot be an assertion because in the case of OOM, the request list was not drained. - bun_core::debug!( - "current_bundle.requests.first != null. this leaves pending requests without an error page!", - ); - } - while let Some(req) = current_bundle.requests.pop() { - req.abort(); - DeferredRequest::release(req); - } + dev.start_next_bundle_if_present() } /// Called at the end of BundleV2 to index bundle contents into the `IncrementalGraph`s /// This function does not recover DevServer state if it fails (allocation failure) -pub(super) fn finalize_bundle( - dev: &mut DevServer, +/// Called at the end of BundleV2 to index bundle contents into the +/// `IncrementalGraph`s. This function does not recover DevServer state if it +/// fails (allocation failure). +/// +/// Everything that runs JavaScript (loading the server patch, the framework's +/// request handler, aborted requests' signal handlers, microtasks) runs +/// between — never inside — the `cell.with_mut` sections. +pub(crate) fn finalize_bundle( + cell: &DevServerCell, bv2: &mut BundleV2, result: &mut bundler::bundle_v2::DevServerOutput, ) -> JsResult<()> { - debug_assert!(dev.magic == Magic::Valid); + debug_assert!(cell.get().magic == Magic::Valid); + let vm = cell.get().vm; + // Promise reactions queued below run when this scope exits, after the + // last dev server borrow. + let event_loop_scope = vm.enter_event_loop_scope(); let mut had_sent_hmr_event = false; - let finalized = finalize_bundle_inner(dev, bv2, result, &mut had_sent_hmr_event); + let finalized = finalize_bundle_phases(cell, bv2, result, &mut had_sent_hmr_event); // The chunks live in the bundle's arena, which does not run their destructors. for chunk in result.chunks.iter_mut() { drop(::core::mem::take(chunk)); } - drain_current_bundle_requests( - dev.current_bundle - .as_mut() - .expect("infallible: bundle active"), - ); - finalize_bundle_cleanup(dev, bv2, had_sent_hmr_event); + let unanswered = cell.with_mut(|dev| { + ::core::mem::take( + &mut dev + .current_bundle + .as_mut() + .expect("infallible: bundle active") + .requests, + ) + }); + abort_unanswered_requests(unanswered); + let next_bundle = cell.with_mut(|dev| finalize_bundle_cleanup(dev, bv2, had_sent_hmr_event)); + let server = cell.get().server; + if let Some(next_bundle) = next_bundle { + DevServer::start_async_bundle(cell, next_bundle).expect("oom"); + } + // Unref the ref added in `start_async_bundle` + server.on_static_request_complete(); + drop(event_loop_scope); finalized } -fn finalize_bundle_inner( - dev: &mut DevServer, - bv2: &mut BundleV2, - result: &mut bundler::bundle_v2::DevServerOutput, - had_sent_hmr_event: &mut bool, -) -> JsResult<()> { - macro_rules! current_bundle { - () => { - dev.current_bundle - .as_mut() - .expect("infallible: bundle active") +/// The server-side HMR patch a bundle produced, loaded into the server +/// runtime between the two graph phases of `finalize_bundle`. +struct ServerPatch { + code: Vec, + source_map_json: Option>, + register_update: JSValue, + client_components_added: JSValue, + client_components_removed: JSValue, +} + +impl ServerPatch { + fn load(self, global: &JSGlobalObject) { + let server_modules = if let Some(json) = self.source_map_json { + // This memory will be owned by the `DevServerSourceProvider` in C++ + match c::bake_load_server_hmr_patch_with_source_map( + global, + BunString::clone_utf8(&self.code), + json.into_boxed_slice(), + ) { + Ok(v) => v, + Err(err) => { + VirtualMachine::get_mut() + .print_error_like_object_to_console(global.take_exception(err)); + // Note: `panic!()` would unwind through the + // `extern "C"` boundary above (`nounwind` UB), so abort. + bun_core::Output::panic(format_args!( + "Error thrown while evaluating server code. This is always a bug in the bundler." + )); + } + } + } else { + match c::bake_load_server_hmr_patch(global, BunString::clone_latin1(&self.code)) { + Ok(v) => v, + Err(err) => { + VirtualMachine::get_mut() + .print_error_like_object_to_console(global.take_exception(err)); + // Note: `panic!()` would unwind through the + // `extern "C"` boundary above (`nounwind` UB), so abort. + bun_core::Output::panic(format_args!( + "Error thrown while evaluating server code. This is always a bug in the bundler." + )); + } + } + }; + let errors = match self.register_update.call( + global, + global.to_js_value(), + &[ + server_modules, + self.client_components_added, + self.client_components_removed, + ], + ) { + Ok(v) => v, + Err(err) => { + VirtualMachine::get_mut() + .print_error_like_object_to_console(global.take_exception(err)); + panic!( + "Error thrown in Hot-module-replacement code. This is always a bug in the HMR runtime." + ); + } }; + let _ = errors; // TODO: } +} + +/// What `finalize_bundle` does with the requests waiting on the bundle. +enum BundleOutcome { + /// The bundle has failures; everything waiting was answered with the + /// error page. These request contexts are to be released. + Failed(Vec), + /// The bundle is good; serve the waiting requests. + Serve, +} - let _lock = dev.graph_safety_lock.guard(); +/// Releases the deferred requests a bundle never answered (only non-empty +/// when finalizing bailed out early). +fn abort_unanswered_requests(requests: deferred_request::List) { + if !requests.is_empty() { + // cannot be an assertion because in the case of OOM, the request list was not drained. + bun_core::debug!( + "current_bundle.requests.first != null. this leaves pending requests without an error page!", + ); + } + for req in requests.into_iter().rev() { + req.abort(); + DeferredRequest::release(req); + } +} +fn finalize_bundle_phases( + cell: &DevServerCell, + bv2: &mut BundleV2, + result: &mut bundler::bundle_v2::DevServerOutput, + had_sent_hmr_event: &mut bool, +) -> JsResult<()> { + let bv2: &BundleV2 = bv2; // The three regions of `result.chunks` are disjoint: // `[0] | [1..1+n_css] | [1+n_css..1+n_css+n_html]`. let n_css = result.css_file_list.count(); @@ -3363,62 +3551,47 @@ fn finalize_bundle_inner( }; let quoted_source_contents = bv2.linker.graph.files.items_quoted_source_contents(); - // Pass 1, update the graph's nodes, resolving every bundler source - // index into its `IncrementalGraph(...).FileIndex` - let js_chunk = &result.chunks[0]; - debug_assert_eq!( - js_chunk.content.javascript().parts_in_chunk_in_order.len(), - js_chunk.compile_results_for_chunk.len() - ); - for (part_range, compile_result) in js_chunk - .content - .javascript() - .parts_in_chunk_in_order - .iter() - .zip(js_chunk.compile_results_for_chunk.iter()) - { - let index = part_range.source_index; - let source_map: bun_sourcemap::Chunk = match compile_result.source_map_chunk() { - Some(c) => c.clone(), - None => 'brk: { - // The source map is `null` if empty - debug_assert!(matches!( - compile_result, - bundler::CompileResult::Javascript { - result: bun_js_printer::PrintResult::Result(_), - .. - } - )); - debug_assert!( - dev.server_transpiler().options.source_map - != bundler::options::SourceMapOption::None - ); - debug_assert!(!part_range.source_index.is_runtime()); - break 'brk bun_sourcemap::Chunk::init_empty(); - } - }; - let quoted_contents = "ed_source_contents[part_range.source_index.get() as usize]; - match targets[part_range.source_index.get() as usize].bake_graph() { - bake::Graph::Client => dev.client_graph_mut().receive_chunk( - &mut ctx, - index, - incremental_graph::ReceiveChunkContent::Js { - code: compile_result.code().to_vec().into_boxed_slice(), - source_map: Some(incremental_graph::ReceiveChunkSourceMap { - chunk: source_map, - // `quoted_contents` lives in the per-bundle AST heap, - // which is destroyed at bundle end; copy onto the - // global heap so the dev-server can hold it across - // rebuilds for stack-trace remapping. - escaped_source: quoted_contents - .as_ref() - .map(|v| v.as_slice().to_vec().into_boxed_slice()), - }), - }, - false, - )?, - graph @ (bake::Graph::Server | bake::Graph::Ssr) => { - dev.server_graph_mut().receive_chunk( + + // Phase 1: index the bundle into the graphs and prepare the server patch. + let server_patch = cell.with_mut(|dev| -> JsResult> { + let _lock = dev.graph_safety_lock.guard(); + // Pass 1, update the graph's nodes, resolving every bundler source + // index into its `IncrementalGraph(...).FileIndex` + let js_chunk = &result.chunks[0]; + debug_assert_eq!( + js_chunk.content.javascript().parts_in_chunk_in_order.len(), + js_chunk.compile_results_for_chunk.len() + ); + for (part_range, compile_result) in js_chunk + .content + .javascript() + .parts_in_chunk_in_order + .iter() + .zip(js_chunk.compile_results_for_chunk.iter()) + { + let index = part_range.source_index; + let source_map: bun_sourcemap::Chunk = match compile_result.source_map_chunk() { + Some(c) => c.clone(), + None => 'brk: { + // The source map is `null` if empty + debug_assert!(matches!( + compile_result, + bundler::CompileResult::Javascript { + result: bun_js_printer::PrintResult::Result(_), + .. + } + )); + debug_assert!( + dev.server_transpiler().options.source_map + != bundler::options::SourceMapOption::None + ); + debug_assert!(!part_range.source_index.is_runtime()); + break 'brk bun_sourcemap::Chunk::init_empty(); + } + }; + let quoted_contents = "ed_source_contents[part_range.source_index.get() as usize]; + match targets[part_range.source_index.get() as usize].bake_graph() { + bake::Graph::Client => dev.client_graph_mut().receive_chunk( &mut ctx, index, incremental_graph::ReceiveChunkContent::Js { @@ -3434,795 +3607,844 @@ fn finalize_bundle_inner( .map(|v| v.as_slice().to_vec().into_boxed_slice()), }), }, - graph == bake::Graph::Ssr, - )? + false, + )?, + graph @ (bake::Graph::Server | bake::Graph::Ssr) => { + dev.server_graph_mut().receive_chunk( + &mut ctx, + index, + incremental_graph::ReceiveChunkContent::Js { + code: compile_result.code().to_vec().into_boxed_slice(), + source_map: Some(incremental_graph::ReceiveChunkSourceMap { + chunk: source_map, + // `quoted_contents` lives in the per-bundle AST heap, + // which is destroyed at bundle end; copy onto the + // global heap so the dev-server can hold it across + // rebuilds for stack-trace remapping. + escaped_source: quoted_contents + .as_ref() + .map(|v| v.as_slice().to_vec().into_boxed_slice()), + }), + }, + graph == bake::Graph::Ssr, + )? + } } } - } - for (i, metadata) in (1..1 + n_css).zip(result.css_file_list.values()) { - debug_assert!(matches!( - result.chunks[i].content, - bundler::chunk::Content::Css(_) - )); + for (i, metadata) in (1..1 + n_css).zip(result.css_file_list.values()) { + debug_assert!(matches!( + result.chunks[i].content, + bundler::chunk::Content::Css(_) + )); - let index = bun_ast::Index::init(result.chunks[i].entry_point.source_index()); - - // `IntermediateOutput::code` takes `&mut self` plus `&Chunk`/`&[Chunk]` - // views of the same storage; take the output out for the call. - let code = { - let mut io = ::core::mem::take(&mut result.chunks[i].intermediate_output); - let code = io.code( - None, - &bv2.graph, - &bv2.linker.graph, - b"THIS_SHOULD_NEVER_BE_EMITTED_IN_DEV_MODE", - &result.chunks[i], - result.chunks, - None, - bundler::chunk::ReferencePathStyle::ImporterRelative, - bundler::chunk::SourceMapShiftTracking::Disabled, - ); - result.chunks[i].intermediate_output = io; - code? - }; + let index = bun_ast::Index::init(result.chunks[i].entry_point.source_index()); - // Create an entry for this file. - let key = ctx.sources[index.get() as usize] - .path - .key_for_incremental_graph(); - // TODO: use a hash mix with the first half being a path hash and the second half content hash - let h = hash(key); - // Track css files that look like tailwind files. - // Note: hoisted before `replace_path` because that consumes - // `code.buffer`. - let looks_like_tailwind = dev.has_tailwind_plugin_hack.is_some() && { - let first_1024 = &code.buffer[..code.buffer.len().min(1024)]; - strings::index_of(first_1024, b"tailwind").is_some() - }; - dev.assets.replace_path( - &mut dev.client_graph, - dev.server, - key, - crate::webcore::blob::Any::from_owned_slice(code.buffer.into()), - &MimeType::CSS, - h, - )?; + // `IntermediateOutput::code` takes `&mut self` plus `&Chunk`/`&[Chunk]` + // views of the same storage; take the output out for the call. + let code = { + let mut io = ::core::mem::take(&mut result.chunks[i].intermediate_output); + let code = io.code( + None, + &bv2.graph, + &bv2.linker.graph, + b"THIS_SHOULD_NEVER_BE_EMITTED_IN_DEV_MODE", + &result.chunks[i], + result.chunks, + None, + bundler::chunk::ReferencePathStyle::ImporterRelative, + bundler::chunk::SourceMapShiftTracking::Disabled, + ); + result.chunks[i].intermediate_output = io; + code? + }; - if let Some(map) = &mut dev.has_tailwind_plugin_hack { - if looks_like_tailwind { - // Note: `get_or_put` consumes the key by value; on miss the key - // already lives in the map so the explicit `*key_ptr =` is redundant. - let _ = map.get_or_put(Box::from(key))?; - } else { - let _ = map.swap_remove(&Box::<[u8]>::from(key)); + // Create an entry for this file. + let key = ctx.sources[index.get() as usize] + .path + .key_for_incremental_graph(); + // TODO: use a hash mix with the first half being a path hash and the second half content hash + let h = hash(key); + // Track css files that look like tailwind files. + // Note: hoisted before `replace_path` because that consumes + // `code.buffer`. + let looks_like_tailwind = dev.has_tailwind_plugin_hack.is_some() && { + let first_1024 = &code.buffer[..code.buffer.len().min(1024)]; + strings::index_of(first_1024, b"tailwind").is_some() + }; + dev.assets.replace_path( + &mut dev.client_graph, + dev.server, + key, + crate::webcore::blob::Any::from_owned_slice(code.buffer.into()), + &MimeType::CSS, + h, + )?; + + if let Some(map) = &mut dev.has_tailwind_plugin_hack { + if looks_like_tailwind { + // Note: `get_or_put` consumes the key by value; on miss the key + // already lives in the map so the explicit `*key_ptr =` is redundant. + let _ = map.get_or_put(Box::from(key))?; + } else { + let _ = map.swap_remove(&Box::<[u8]>::from(key)); + } } - } - dev.client_graph_mut().receive_chunk( - &mut ctx, - index, - incremental_graph::ReceiveChunkContent::Css(h), - false, - )?; + dev.client_graph_mut().receive_chunk( + &mut ctx, + index, + incremental_graph::ReceiveChunkContent::Css(h), + false, + )?; - // If imported on server, there needs to be a server-side file entry - // so that edges can be attached. - if metadata.imported_on_server { - dev.server_graph - .insert_css_file_on_server(&mut ctx, index, key)?; + // If imported on server, there needs to be a server-side file entry + // so that edges can be attached. + if metadata.imported_on_server { + dev.server_graph + .insert_css_file_on_server(&mut ctx, index, key)?; + } } - } - for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter_mut() { - let index = bun_ast::Index::init(chunk.entry_point.source_index()); - let generated_js = dev.generate_javascript_code_for_html_file( - index, - import_records, - input_file_sources, - bv2.graph.input_files.items_loader(), - )?; - dev.client_graph_mut().receive_chunk( - &mut ctx, - index, - incremental_graph::ReceiveChunkContent::Js { - code: generated_js, - source_map: None, - }, - false, - )?; - let client_index = ctx - .get_cached_index(bake::Side::Client, index) - .unwrap::<{ bake::Side::Client }>() - .expect("unresolved index"); - // Not the file of a route: a plugin resolved a route's file to it, or loaded it as html. - let Some(route_bundle_index) = dev.client_graph.html_route_bundle_index(client_index) - else { - continue; - }; - let route_bundle = &mut dev.route_bundles[route_bundle_index.get() as usize]; - debug_assert!(route_bundle.data.html().bundled_file == client_index); - if route_bundle - .data - .html_mut() - .cached_response - .take() - .is_some() - { - route_bundle.invalidate_client_bundle(&mut dev.source_maps); + for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter_mut() { + let index = bun_ast::Index::init(chunk.entry_point.source_index()); + let generated_js = dev.generate_javascript_code_for_html_file( + index, + import_records, + input_file_sources, + bv2.graph.input_files.items_loader(), + )?; + dev.client_graph_mut().receive_chunk( + &mut ctx, + index, + incremental_graph::ReceiveChunkContent::Js { + code: generated_js, + source_map: None, + }, + false, + )?; + let client_index = ctx + .get_cached_index(bake::Side::Client, index) + .unwrap::<{ bake::Side::Client }>() + .expect("unresolved index"); + // Not the file of a route: a plugin resolved a route's file to it, or loaded it as html. + let Some(route_bundle_index) = dev.client_graph.html_route_bundle_index(client_index) + else { + continue; + }; + let route_bundle = &mut dev.route_bundles[route_bundle_index.get() as usize]; + debug_assert!(route_bundle.data.html().bundled_file == client_index); + if route_bundle + .data + .html_mut() + .cached_response + .take() + .is_some() + { + route_bundle.invalidate_client_bundle(&mut dev.source_maps); + } + let html = match &mut route_bundle.data { + route_bundle::Data::Html(h) => h, + _ => unreachable!(), + }; + let bundler::CompileResult::Html { + code: compile_result_code, + script_injection_offset: compile_result_offset, + .. + } = chunk.compile_results_for_chunk.get_mut(0) + else { + unreachable!() + }; + // Drops the previous text and transfers ownership of the chunk's + // compile-result code, leaving an empty + // boxed slice behind in the chunk — nothing reads it after this. + html.bundled_html_text = Some(std::mem::take(compile_result_code)); + html.script_injection_offset = + Some(route_bundle::ByteOffset::init(*compile_result_offset)); + + chunk + .entry_point + .set_entry_point_id(route_bundle_index.get()); } - let html = match &mut route_bundle.data { - route_bundle::Data::Html(h) => h, - _ => unreachable!(), - }; - let bundler::CompileResult::Html { - code: compile_result_code, - script_injection_offset: compile_result_offset, - .. - } = chunk.compile_results_for_chunk.get_mut(0) - else { - unreachable!() - }; - // Drops the previous text and transfers ownership of the chunk's - // compile-result code, leaving an empty - // boxed slice behind in the chunk — nothing reads it after this. - html.bundled_html_text = Some(std::mem::take(compile_result_code)); - html.script_injection_offset = Some(route_bundle::ByteOffset::init(*compile_result_offset)); - - chunk - .entry_point - .set_entry_point_id(route_bundle_index.get()); - } - // Sized AFTER - // Pass 1 so server/client bitsets cover files just inserted by `receive_chunk`. - *ctx.gts = dev.init_graph_trace_state(if n_css > 0 { - input_file_sources.len() - } else { - 0 - })?; - ctx.server_seen_bit_set = DynamicBitSet::init_empty(dev.server_graph.bundled_files.len())?; + // Sized AFTER + // Pass 1 so server/client bitsets cover files just inserted by `receive_chunk`. + *ctx.gts = dev.init_graph_trace_state(if n_css > 0 { + input_file_sources.len() + } else { + 0 + })?; + ctx.server_seen_bit_set = DynamicBitSet::init_empty(dev.server_graph.bundled_files.len())?; - dev.incremental_result.had_adjusted_edges = false; + dev.incremental_result.had_adjusted_edges = false; - dev.prepare_and_log_resolution_failures()?; - dev.report_html_routes_without_html()?; + dev.prepare_and_log_resolution_failures()?; + dev.report_html_routes_without_html()?; - // Pass 2, update the graph's edges by performing import diffing on each - // changed file, removing dependencies. This pass also flags what routes - // have been modified. - for part_range in result.chunks[0] - .content - .javascript() - .parts_in_chunk_in_order - .iter() - { - match targets[part_range.source_index.get() as usize].bake_graph() { - bake::Graph::Server | bake::Graph::Ssr => { - dev.server_graph_mut().process_chunk_dependencies( + // Pass 2, update the graph's edges by performing import diffing on each + // changed file, removing dependencies. This pass also flags what routes + // have been modified. + for part_range in result.chunks[0] + .content + .javascript() + .parts_in_chunk_in_order + .iter() + { + match targets[part_range.source_index.get() as usize].bake_graph() { + bake::Graph::Server | bake::Graph::Ssr => { + dev.server_graph_mut().process_chunk_dependencies( + &mut ctx, + incremental_graph::ProcessMode::Normal, + part_range.source_index, + )? + } + bake::Graph::Client => dev.client_graph_mut().process_chunk_dependencies( &mut ctx, incremental_graph::ProcessMode::Normal, part_range.source_index, - )? + )?, } - bake::Graph::Client => dev.client_graph_mut().process_chunk_dependencies( + } + for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter() { + let index = bun_ast::Index::init(chunk.entry_point.source_index()); + dev.client_graph_mut().process_chunk_dependencies( &mut ctx, incremental_graph::ProcessMode::Normal, - part_range.source_index, - )?, + index, + )?; + } + for chunk in result.chunks[1..1 + n_css].iter() { + let entry_index = bun_ast::Index::init(chunk.entry_point.source_index()); + dev.client_graph_mut().process_chunk_dependencies( + &mut ctx, + incremental_graph::ProcessMode::Css, + entry_index, + )?; } - } - for chunk in result.chunks[1 + n_css..1 + n_css + n_html].iter() { - let index = bun_ast::Index::init(chunk.entry_point.source_index()); - dev.client_graph_mut().process_chunk_dependencies( - &mut ctx, - incremental_graph::ProcessMode::Normal, - index, - )?; - } - for chunk in result.chunks[1..1 + n_css].iter() { - let entry_index = bun_ast::Index::init(chunk.entry_point.source_index()); - dev.client_graph_mut().process_chunk_dependencies( - &mut ctx, - incremental_graph::ProcessMode::Css, - entry_index, - )?; - } - // Index all failed files now that the incremental graph has been updated. - if !dev.incremental_result.failures_removed.is_empty() - || !dev.incremental_result.failures_added.is_empty() - { - *had_sent_hmr_event = true; - } - dev.index_failures()?; + // Index all failed files now that the incremental graph has been updated. + if !dev.incremental_result.failures_removed.is_empty() + || !dev.incremental_result.failures_added.is_empty() + { + *had_sent_hmr_event = true; + } + dev.index_failures()?; - dev.client_graph.ensure_stale_bit_capacity(false)?; - dev.server_graph.ensure_stale_bit_capacity(false)?; + dev.client_graph.ensure_stale_bit_capacity(false)?; + dev.server_graph.ensure_stale_bit_capacity(false)?; - dev.generation = dev.generation.wrapping_add(1); - if Environment::ENABLE_LOGS { - debug_log!( - "Bundle Round {}: {} server, {} client, {} ms", - dev.generation, - dev.server_graph.current_chunk_parts.len(), - dev.client_graph.current_chunk_parts.len(), - current_bundle!().timer.elapsed().as_millis(), - ); - } + dev.generation = dev.generation.wrapping_add(1); + if Environment::ENABLE_LOGS { + debug_log!( + "Bundle Round {}: {} server, {} client, {} ms", + dev.generation, + dev.server_graph.current_chunk_parts.len(), + dev.client_graph.current_chunk_parts.len(), + dev.current_bundle + .as_ref() + .expect("infallible: bundle active") + .timer + .elapsed() + .as_millis(), + ); + } - // Load all new chunks into the server runtime. - if !dev.frontend_only && dev.server_graph.current_chunk_len > 0 { + // Load all new chunks into the server runtime. + if dev.frontend_only || dev.server_graph.current_chunk_len == 0 { + return Ok(None); + } // Generate a script_id for server bundles // Get the source map if available and render to JSON let source_map_json = if !dev.server_graph.current_chunk_source_maps.is_empty() { - 'json: { - // Create a temporary source map entry to render - let mut source_map_entry = source_map_store::Entry { - ref_count: 1, - ..Default::default() - }; + // Create a temporary source map entry to render + let mut source_map_entry = source_map_store::Entry { + ref_count: 1, + ..Default::default() + }; - // Fill the source map entry - dev.server_graph.take_source_map(&mut source_map_entry)?; - let source_map_entry = scopeguard::guard(source_map_entry, |mut entry| { - entry.ref_count = 0; - entry.deinit(); - }); + // Fill the source map entry + dev.server_graph.take_source_map(&mut source_map_entry)?; + let source_map_entry = scopeguard::guard(source_map_entry, |mut entry| { + entry.ref_count = 0; + entry.deinit(); + }); - let json_data = - source_map_entry.render_json(ChunkKind::HmrChunk, bake::Side::Server)?; - break 'json Some(json_data); - } + Some(source_map_entry.render_json(ChunkKind::HmrChunk, bake::Side::Server)?) } else { None }; - let server_bundle = dev.server_graph.take_js_bundle_server( + let code = dev.server_graph.take_js_bundle_server( &incremental_graph::TakeJSBundleOptionsServer { kind: ChunkKind::HmrChunk, }, )?; let global = dev.global(); - let server_modules = if let Some(json) = source_map_json { - // This memory will be owned by the `DevServerSourceProvider` in C++ - match c::bake_load_server_hmr_patch_with_source_map( + Ok(Some(ServerPatch { + code, + source_map_json, + register_update: dev.server_register_update_callback.get().unwrap(), + client_components_added: dev.make_array_for_server_components_patch( global, - BunString::clone_utf8(&server_bundle), - json.into_boxed_slice(), - ) { - Ok(v) => v, - Err(err) => { - dev.vm_mut() - .print_error_like_object_to_console(global.take_exception(err)); - // Note: `panic!()` would unwind through the - // `extern "C"` boundary above (`nounwind` UB), so abort. - bun_core::Output::panic(format_args!( - "Error thrown while evaluating server code. This is always a bug in the bundler." - )); - } - } - } else { - match c::bake_load_server_hmr_patch(global, BunString::clone_latin1(&server_bundle)) { - Ok(v) => v, - Err(err) => { - dev.vm_mut() - .print_error_like_object_to_console(global.take_exception(err)); - // Note: `panic!()` would unwind through the - // `extern "C"` boundary above (`nounwind` UB), so abort. - bun_core::Output::panic(format_args!( - "Error thrown while evaluating server code. This is always a bug in the bundler." - )); - } - } - }; - let errors = match dev.server_register_update_callback.get().unwrap().call( - global, - global.to_js_value(), - &[ - server_modules, - dev.make_array_for_server_components_patch( - global, - &dev.incremental_result.client_components_added, - )?, - dev.make_array_for_server_components_patch( - global, - &dev.incremental_result.client_components_removed, - )?, - ], - ) { - Ok(v) => v, - Err(err) => { - dev.vm_mut() - .print_error_like_object_to_console(global.take_exception(err)); - panic!( - "Error thrown in Hot-module-replacement code. This is always a bug in the HMR runtime." - ); - } - }; - let _ = errors; // TODO: + &dev.incremental_result.client_components_added, + )?, + client_components_removed: dev.make_array_for_server_components_patch( + global, + &dev.incremental_result.client_components_removed, + )?, + })) + })?; + + if let Some(patch) = server_patch { + patch.load(cell.get().global()); } - let mut route_bits = DynamicBitSet::init_empty(dev.route_bundles.len())?; - let mut route_bits_client = DynamicBitSet::init_empty(dev.route_bundles.len())?; + // Phase 2: work out what changed and tell the HMR clients. + let outcome = cell.with_mut(|dev| -> JsResult { + let _lock = dev.graph_safety_lock.guard(); + macro_rules! current_bundle { + () => { + dev.current_bundle + .as_mut() + .expect("infallible: bundle active") + }; + } - let mut has_route_bits_set = false; + let mut route_bits = DynamicBitSet::init_empty(dev.route_bundles.len())?; + let mut route_bits_client = DynamicBitSet::init_empty(dev.route_bundles.len())?; - let mut hot_update_payload: Vec = Vec::with_capacity(65536); - hot_update_payload.push(MessageId::HotUpdate.char()); + let mut has_route_bits_set = false; - // The writer used for the hot_update payload - macro_rules! w_int { - ($t:ty, $v:expr) => { - hot_update_payload.extend_from_slice(&<$t>::to_le_bytes($v)) - }; - } - macro_rules! w_all { - ($s:expr) => { - hot_update_payload.extend_from_slice($s) - }; - } + let mut hot_update_payload: Vec = Vec::with_capacity(65536); + hot_update_payload.push(MessageId::HotUpdate.char()); - // It was discovered that if a tree falls with nobody around it, it does not - // make any sound. Let's avoid writing into `w` if no sockets are open. - let hot_update_subscribers = dev.num_subscribers(HmrTopic::HotUpdate); - let will_hear_hot_update = hot_update_subscribers > 0; + // The writer used for the hot_update payload + macro_rules! w_int { + ($t:ty, $v:expr) => { + hot_update_payload.extend_from_slice(&<$t>::to_le_bytes($v)) + }; + } + macro_rules! w_all { + ($s:expr) => { + hot_update_payload.extend_from_slice($s) + }; + } - // This list of routes affected excludes client code. - if will_hear_hot_update - && current_bundle!().had_reload_event - && (dev.incremental_result.framework_routes_affected.len() - + dev.incremental_result.html_routes_hard_affected.len()) - > 0 - && dev.bundling_failures.is_empty() - { - has_route_bits_set = true; + // It was discovered that if a tree falls with nobody around it, it does not + // make any sound. Let's avoid writing into `w` if no sockets are open. + let hot_update_subscribers = dev.num_subscribers(HmrTopic::HotUpdate); + let will_hear_hot_update = hot_update_subscribers > 0; + + // This list of routes affected excludes client code. + if will_hear_hot_update + && current_bundle!().had_reload_event + && (dev.incremental_result.framework_routes_affected.len() + + dev.incremental_result.html_routes_hard_affected.len()) + > 0 + && dev.bundling_failures.is_empty() + { + has_route_bits_set = true; - for request in &dev.incremental_result.framework_routes_affected { - let route = dev.router.route_ptr(request.route_index()); - if let Some(id) = route.bundle { - route_bits.set(id.get() as usize); + for request in &dev.incremental_result.framework_routes_affected { + let route = dev.router.route_ptr(request.route_index()); + if let Some(id) = route.bundle { + route_bits.set(id.get() as usize); + } + if request.should_recurse_when_visiting() { + mark_all_route_children( + &dev.router, + &mut [&mut route_bits], + request.route_index(), + ); + } } - if request.should_recurse_when_visiting() { - mark_all_route_children(&dev.router, &mut [&mut route_bits], request.route_index()); + for route_bundle_index in &dev.incremental_result.html_routes_hard_affected { + route_bits.set(route_bundle_index.get() as usize); + route_bits_client.set(route_bundle_index.get() as usize); } - } - for route_bundle_index in &dev.incremental_result.html_routes_hard_affected { - route_bits.set(route_bundle_index.get() as usize); - route_bits_client.set(route_bundle_index.get() as usize); - } - // List 1 - let mut it = route_bits.iterator::(); - while let Some(bundled_route_index) = it.next() { - let bundle = &dev.route_bundles[bundled_route_index]; - if bundle.active_viewers == 0 { - continue; + // List 1 + let mut it = route_bits.iterator::(); + while let Some(bundled_route_index) = it.next() { + let bundle = &dev.route_bundles[bundled_route_index]; + if bundle.active_viewers == 0 { + continue; + } + w_int!(i32, i32::try_from(bundled_route_index).expect("int cast")); } - w_int!(i32, i32::try_from(bundled_route_index).expect("int cast")); } - } - w_int!(i32, -1); + w_int!(i32, -1); - // When client component roots get updated, the `client_components_affected` - // list contains the server side versions of these roots. - if !dev.incremental_result.client_components_affected.is_empty() { - has_route_bits_set = true; + // When client component roots get updated, the `client_components_affected` + // list contains the server side versions of these roots. + if !dev.incremental_result.client_components_affected.is_empty() { + has_route_bits_set = true; - dev.incremental_result.framework_routes_affected.clear(); - dev.incremental_result.html_routes_hard_affected.clear(); - dev.incremental_result.html_routes_soft_affected.clear(); - ctx.gts.clear(); + dev.incremental_result.framework_routes_affected.clear(); + dev.incremental_result.html_routes_hard_affected.clear(); + dev.incremental_result.html_routes_soft_affected.clear(); + ctx.gts.clear(); - for index in dev.incremental_result.client_components_affected.clone() { - dev.server_graph_mut().trace_dependencies( - index, - ctx.gts, - incremental_graph::TraceDependencyGoal::NoStop, - index, - )?; - } + for index in dev.incremental_result.client_components_affected.clone() { + dev.server_graph_mut().trace_dependencies( + index, + ctx.gts, + incremental_graph::TraceDependencyGoal::NoStop, + index, + )?; + } - for request in &dev.incremental_result.framework_routes_affected { - let route = dev.router.route_ptr(request.route_index()); - if let Some(id) = route.bundle { - route_bits.set(id.get() as usize); - route_bits_client.set(id.get() as usize); + for request in &dev.incremental_result.framework_routes_affected { + let route = dev.router.route_ptr(request.route_index()); + if let Some(id) = route.bundle { + route_bits.set(id.get() as usize); + route_bits_client.set(id.get() as usize); + } + if request.should_recurse_when_visiting() { + mark_all_route_children( + &dev.router, + &mut [&mut route_bits, &mut route_bits_client], + request.route_index(), + ); + } } - if request.should_recurse_when_visiting() { - mark_all_route_children( - &dev.router, - &mut [&mut route_bits, &mut route_bits_client], - request.route_index(), - ); + + // Free old bundles + let mut it = route_bits_client.iterator::(); + while let Some(bundled_route_index) = it.next() { + dev.route_bundles[bundled_route_index] + .invalidate_client_bundle(&mut dev.source_maps); + } + } else if !dev.incremental_result.html_routes_hard_affected.is_empty() { + // Free old bundles + let mut it = route_bits_client.iterator::(); + while let Some(bundled_route_index) = it.next() { + dev.route_bundles[bundled_route_index] + .invalidate_client_bundle(&mut dev.source_maps); } } - // Free old bundles - let mut it = route_bits_client.iterator::(); - while let Some(bundled_route_index) = it.next() { - dev.route_bundles[bundled_route_index].invalidate_client_bundle(&mut dev.source_maps); - } - } else if !dev.incremental_result.html_routes_hard_affected.is_empty() { - // Free old bundles - let mut it = route_bits_client.iterator::(); - while let Some(bundled_route_index) = it.next() { - dev.route_bundles[bundled_route_index].invalidate_client_bundle(&mut dev.source_maps); + // Softly affected HTML routes only need the bundle invalidated. + if !dev.incremental_result.html_routes_soft_affected.is_empty() { + for index in &dev.incremental_result.html_routes_soft_affected { + dev.route_bundles[index.get() as usize] + .invalidate_client_bundle(&mut dev.source_maps); + route_bits.set(index.get() as usize); + } + has_route_bits_set = true; } - } - // Softly affected HTML routes only need the bundle invalidated. - if !dev.incremental_result.html_routes_soft_affected.is_empty() { - for index in &dev.incremental_result.html_routes_soft_affected { - dev.route_bundles[index.get() as usize].invalidate_client_bundle(&mut dev.source_maps); - route_bits.set(index.get() as usize); - } - has_route_bits_set = true; - } - - // `route_bits` will have all of the routes that were modified. - if has_route_bits_set && (will_hear_hot_update || dev.incremental_result.had_adjusted_edges) { - // Note: copy out before the loop so the `&mut RouteBundle` borrow - // below doesn't overlap a `&dev.incremental_result` read. - let had_adjusted_edges = dev.incremental_result.had_adjusted_edges; - let mut it = route_bits.iterator::(); - // List 2 - while let Some(i) = it.next() { - let route_bundle = &mut dev.route_bundles[i]; - if had_adjusted_edges { - match &mut route_bundle.data { - route_bundle::Data::Framework(fw_bundle) => { - fw_bundle.cached_css_file_array.clear_without_deallocation() + // `route_bits` will have all of the routes that were modified. + if has_route_bits_set && (will_hear_hot_update || dev.incremental_result.had_adjusted_edges) + { + // Note: copy out before the loop so the `&mut RouteBundle` borrow + // below doesn't overlap a `&dev.incremental_result` read. + let had_adjusted_edges = dev.incremental_result.had_adjusted_edges; + let mut it = route_bits.iterator::(); + // List 2 + while let Some(i) = it.next() { + let route_bundle = &mut dev.route_bundles[i]; + if had_adjusted_edges { + match &mut route_bundle.data { + route_bundle::Data::Framework(fw_bundle) => { + fw_bundle.cached_css_file_array.clear_without_deallocation() + } + route_bundle::Data::Html(html) => html.cached_response = None, } - route_bundle::Data::Html(html) => html.cached_response = None, + } + if route_bundle.active_viewers == 0 || !will_hear_hot_update { + continue; + } + let trace_key = route_bundle.trace_key(); + w_int!(i32, i32::try_from(i).expect("int cast")); + + // If no edges were changed, then it is impossible to + // change the list of CSS files. + if had_adjusted_edges { + ctx.gts.clear(); + dev.client_graph.current_css_files.clear(); + dev.trace_all_route_imports(trace_key, ctx.gts, TraceImportGoal::FindCss)?; + let css_ids = &dev.client_graph.current_css_files; + + w_int!(i32, i32::try_from(css_ids.len()).expect("int cast")); + for css_id in css_ids { + let mut hex = [0u8; 16]; + let n = bun_core::fmt::bytes_to_hex_lower(&css_id.to_ne_bytes(), &mut hex); + w_all!(&hex[..n]); + } + } else { + w_int!(i32, -1); } } - if route_bundle.active_viewers == 0 || !will_hear_hot_update { - continue; - } - let trace_key = route_bundle.trace_key(); - w_int!(i32, i32::try_from(i).expect("int cast")); - - // If no edges were changed, then it is impossible to - // change the list of CSS files. - if had_adjusted_edges { - ctx.gts.clear(); - dev.client_graph.current_css_files.clear(); - dev.trace_all_route_imports(trace_key, ctx.gts, TraceImportGoal::FindCss)?; - let css_ids = &dev.client_graph.current_css_files; - - w_int!(i32, i32::try_from(css_ids.len()).expect("int cast")); - for css_id in css_ids { + } + w_int!(i32, -1); + + let css_chunks = &result.chunks[1..1 + n_css]; + if will_hear_hot_update { + if dev.client_graph.current_chunk_len > 0 || !css_chunks.is_empty() { + // Send CSS mutations + dev.assets.reindex_if_needed()?; + w_int!(u32, u32::try_from(css_chunks.len()).expect("int cast")); + use bun_bundler::Graph::InputFileColumns as _; + let sources = bv2.graph.input_files.items_source(); + for chunk in css_chunks { + let key = sources[chunk.entry_point.source_index() as usize] + .path + .key_for_incremental_graph(); + let content_hash = hash(key); let mut hex = [0u8; 16]; - let n = bun_core::fmt::bytes_to_hex_lower(&css_id.to_ne_bytes(), &mut hex); + let n = + bun_core::fmt::bytes_to_hex_lower(&content_hash.to_ne_bytes(), &mut hex); w_all!(&hex[..n]); + let css_data: &[u8] = match dev.assets.get(content_hash) { + Some(route) => &route.blob.internal_blob().bytes, + None => b"", + }; + w_int!(u32, u32::try_from(css_data.len()).expect("int cast")); + w_all!(css_data); } - } else { - w_int!(i32, -1); - } - } - } - w_int!(i32, -1); - - let css_chunks = &result.chunks[1..1 + n_css]; - if will_hear_hot_update { - if dev.client_graph.current_chunk_len > 0 || !css_chunks.is_empty() { - // Send CSS mutations - dev.assets.reindex_if_needed()?; - w_int!(u32, u32::try_from(css_chunks.len()).expect("int cast")); - use bun_bundler::Graph::InputFileColumns as _; - let sources = bv2.graph.input_files.items_source(); - for chunk in css_chunks { - let key = sources[chunk.entry_point.source_index() as usize] - .path - .key_for_incremental_graph(); - let content_hash = hash(key); - let mut hex = [0u8; 16]; - let n = bun_core::fmt::bytes_to_hex_lower(&content_hash.to_ne_bytes(), &mut hex); - w_all!(&hex[..n]); - let css_data: &[u8] = match dev.assets.get(content_hash) { - Some(route) => &route.blob.internal_blob().bytes, - None => b"", - }; - w_int!(u32, u32::try_from(css_data.len()).expect("int cast")); - w_all!(css_data); - } - // Send the JS chunk - if dev.client_graph.current_chunk_len > 0 { - let script_id = 'h: { - // Matches the bundler's `ContentHasher` hash (XxHash64). - let mut source_map_hash = bun_hash::XxHash64Streaming::new(0x4b12); - let keys = dev.client_graph.bundled_files.keys(); - let values = dev.client_graph.bundled_files.values(); - for part in &dev.client_graph.current_chunk_parts { - source_map_hash.update(&keys[part.get() as usize]); - if let Some(map) = values[part.get() as usize].source_map.get() { - source_map_hash.update(map.vlq()); + // Send the JS chunk + if dev.client_graph.current_chunk_len > 0 { + let script_id = 'h: { + // Matches the bundler's `ContentHasher` hash (XxHash64). + let mut source_map_hash = bun_hash::XxHash64Streaming::new(0x4b12); + let keys = dev.client_graph.bundled_files.keys(); + let values = dev.client_graph.bundled_files.values(); + for part in &dev.client_graph.current_chunk_parts { + source_map_hash.update(&keys[part.get() as usize]); + if let Some(map) = values[part.get() as usize].source_map.get() { + source_map_hash.update(map.vlq()); + } } - } - // Set the bottom bit. - break 'h source_map_store::Key::init(source_map_hash.digest() | 1); - }; - let mut sockets: u32 = 0; - for socket in dev.active_websocket_connections.values() { - if socket.is_subscribed(HmrTopic::HotUpdate) { - let is_new = socket.referenced_source_maps.with_mut(|maps| { - !maps.get_or_put(script_id).expect("oom").found_existing - }); - if is_new { - sockets += 1; + // Set the bottom bit. + break 'h source_map_store::Key::init(source_map_hash.digest() | 1); + }; + let mut sockets: u32 = 0; + for socket in dev.active_websocket_connections.values() { + if socket.is_subscribed(HmrTopic::HotUpdate) { + let is_new = socket.referenced_source_maps.with_mut(|maps| { + !maps.get_or_put(script_id).expect("oom").found_existing + }); + if is_new { + sockets += 1; + } } } + map_log!("inc {:x}, for {} sockets", script_id.get(), sockets); + let entry = match dev + .source_maps + .put_or_increment_ref_count(script_id, sockets)? + { + source_map_store::PutOrIncrementRefCount::Uninitialized(entry) => 'brk: { + dev.client_graph.take_source_map(entry)?; + break 'brk entry; + } + source_map_store::PutOrIncrementRefCount::Shared(entry) => entry, + }; + w_int!(u32, entry.overlapping_memory_cost); + + // Build and send the source chunk + let console_log = dev.should_receive_console_log_from_browser(); + dev.client_graph_mut().take_js_bundle_to_list( + &mut hot_update_payload, + &incremental_graph::TakeJSBundleOptionsClient { + kind: crate::bake::dev_server::ChunkKind::HmrChunk, + script_id, + console_log, + ..Default::default() + }, + )?; } - map_log!("inc {:x}, for {} sockets", script_id.get(), sockets); - let entry = match dev - .source_maps - .put_or_increment_ref_count(script_id, sockets)? - { - source_map_store::PutOrIncrementRefCount::Uninitialized(entry) => 'brk: { - dev.client_graph.take_source_map(entry)?; - break 'brk entry; - } - source_map_store::PutOrIncrementRefCount::Shared(entry) => entry, - }; - w_int!(u32, entry.overlapping_memory_cost); - - // Build and send the source chunk - let console_log = dev.should_receive_console_log_from_browser(); - dev.client_graph_mut().take_js_bundle_to_list( - &mut hot_update_payload, - &incremental_graph::TakeJSBundleOptionsClient { - kind: crate::bake::dev_server::ChunkKind::HmrChunk, - script_id, - console_log, - ..Default::default() - }, - )?; + } else { + w_int!(i32, 0); } - } else { - w_int!(i32, 0); + + dev.publish(HmrTopic::HotUpdate, &hot_update_payload, Opcode::BINARY); + *had_sent_hmr_event = true; } - dev.publish(HmrTopic::HotUpdate, &hot_update_payload, Opcode::BINARY); - *had_sent_hmr_event = true; - } + if !dev.incremental_result.failures_added.is_empty() { + dev.bundles_since_last_error = 0; - if !dev.incremental_result.failures_added.is_empty() { - dev.bundles_since_last_error = 0; + // Only the first failure payload sent notifies the inspector. + let mut notify_inspector = dev.inspector().is_some(); + let mut release_after = Vec::new(); + if current_bundle!().promise.strong.has_value() { + dev.set_current_bundle_route_states(route_bundle::State::PossibleBundlingFailures); + let global = dev.global(); + let promise = current_bundle!().promise.strong.take(); + current_bundle!().promise.reset(); + dev.send_serialized_failures( + DevResponse::Promise(PromiseResponse { promise, global }), + dev.bundling_failures.values(), + dev.inspector() + .filter(|_| ::core::mem::take(&mut notify_inspector)), + )?; + } - // Only the first failure payload sent notifies the inspector. - let mut notify_inspector = dev.inspector().is_some(); - if current_bundle!().promise.strong.has_value() { - dev.set_current_bundle_route_states(route_bundle::State::PossibleBundlingFailures); - let global = dev.global(); - let promise = current_bundle!().promise.strong.take(); - current_bundle!().promise.reset(); - dev.send_serialized_failures( - DevResponse::Promise(PromiseResponse { promise, global }), - dev.bundling_failures.values(), - dev.inspector() - .filter(|_| ::core::mem::take(&mut notify_inspector)), - )?; - } + while let Some(req) = current_bundle!().requests.pop() { + let req = scopeguard::guard(req, DeferredRequest::release); - while let Some(req) = current_bundle!().requests.pop() { - let req = scopeguard::guard(req, DeferredRequest::release); + dev.route_bundle_ptr(req.route_bundle_index).server_state = + route_bundle::State::PossibleBundlingFailures; - dev.route_bundle_ptr(req.route_bundle_index).server_state = - route_bundle::State::PossibleBundlingFailures; - - let resp: DevResponse = match req.handler.replace(Handler::Aborted) { - Handler::Aborted => continue, - Handler::ServerHandler(mut saved) => { - let resp = saved.response; - // Releases the ref taken in `defer_request`... - saved.deinit(); - // ...and the request context's own: the error page is - // written to the raw response below, so nothing hands the - // context to a JS handler that would finish it. - saved.ctx.deref(); - DevResponse::Http(resp) - } - Handler::BundledHtmlPage(ram) => DevResponse::Http(ram.response), - }; + let resp: DevResponse = match req.handler.replace(Handler::Aborted) { + Handler::Aborted => continue, + Handler::ServerHandler(mut saved) => { + let resp = saved.response; + // Releases the ref taken in `defer_request`; the request + // context's own is released once the error page is out + // (nothing hands the context to a JS handler that would + // finish it). + release_after.push(saved.ctx); + saved.deinit(); + DevResponse::Http(resp) + } + Handler::BundledHtmlPage(ram) => DevResponse::Http(ram.response), + }; - dev.send_serialized_failures( - resp, - dev.bundling_failures.values(), - dev.inspector() - .filter(|_| ::core::mem::take(&mut notify_inspector)), - )?; - } - if notify_inspector { - let mut buf: Vec = Vec::new(); - dev.encode_serialized_failures( - dev.bundling_failures.values(), - &mut buf, - dev.inspector(), - )?; - } + dev.send_serialized_failures( + resp, + dev.bundling_failures.values(), + dev.inspector() + .filter(|_| ::core::mem::take(&mut notify_inspector)), + )?; + } + if notify_inspector { + let mut buf: Vec = Vec::new(); + dev.encode_serialized_failures( + dev.bundling_failures.values(), + &mut buf, + dev.inspector(), + )?; + } - return Ok(()); - } + return Ok(BundleOutcome::Failed(release_after)); + } - if dev.bundling_failures.is_empty() { - if current_bundle!().had_reload_event { - let clear_terminal = !bun_output::scope_is_visible!(DevServer) - && !dev - .vm() - .env_loader() - .has_set_no_clear_terminal_on_reload(false); - if clear_terminal { - Output::disable_buffering(); - Output::reset_terminal_all(); - Output::enable_buffering(); - } + if dev.bundling_failures.is_empty() { + if current_bundle!().had_reload_event { + let clear_terminal = !bun_output::scope_is_visible!(DevServer) + && !dev + .vm() + .env_loader() + .has_set_no_clear_terminal_on_reload(false); + if clear_terminal { + Output::disable_buffering(); + Output::reset_terminal_all(); + Output::enable_buffering(); + } - dev.print_memory_line(); + dev.print_memory_line(); - dev.bundles_since_last_error += 1; - if dev.bundles_since_last_error > 1 { - bun_core::pretty_error!("[x{}] ", dev.bundles_since_last_error); + dev.bundles_since_last_error += 1; + if dev.bundles_since_last_error > 1 { + bun_core::pretty_error!("[x{}] ", dev.bundles_since_last_error); + } + } else { + dev.bundles_since_last_error = 0; + dev.print_memory_line(); } - } else { - dev.bundles_since_last_error = 0; - dev.print_memory_line(); - } - let ms_elapsed = u64::try_from(current_bundle!().timer.elapsed().as_millis()).unwrap(); + let ms_elapsed = u64::try_from(current_bundle!().timer.elapsed().as_millis()).unwrap(); - bun_core::pretty_error!( - "{} in {}ms", - if current_bundle!().had_reload_event { - "Reloaded" - } else { - "Bundled page" - }, - ms_elapsed, - ); + bun_core::pretty_error!( + "{} in {}ms", + if current_bundle!().had_reload_event { + "Reloaded" + } else { + "Bundled page" + }, + ms_elapsed, + ); - // Intentionally creating a new scope here so we can limit the lifetime - // of the `relative_path_buf` - { - let mut buf = paths::path_buffer_pool::get(); + // Intentionally creating a new scope here so we can limit the lifetime + // of the `relative_path_buf` + { + let mut buf = paths::path_buffer_pool::get(); - // Compute a file name to display - let file_name: Option<&[u8]> = if current_bundle!().had_reload_event { - if !bv2.graph.entry_points.is_empty() { - Some(dev.relative_path(&mut *buf, { - use bun_bundler::Graph::InputFileColumns as _; - bv2.graph.input_files.items_source() - [bv2.graph.entry_points[0].get() as usize] - .path - .text - })) + // Compute a file name to display + let file_name: Option<&[u8]> = if current_bundle!().had_reload_event { + if !bv2.graph.entry_points.is_empty() { + Some(dev.relative_path(&mut *buf, { + use bun_bundler::Graph::InputFileColumns as _; + bv2.graph.input_files.items_source() + [bv2.graph.entry_points[0].get() as usize] + .path + .text + })) + } else { + None // TODO: How does this happen + } } else { - None // TODO: How does this happen - } - } else { - 'brk: { - let route_bundle_index = 'rbi: { - if let Some(first) = current_bundle!().requests.last() { - break 'rbi first.route_bundle_index; - } - let route_bundle_indices = - current_bundle!().promise.route_bundle_indices.keys(); - if route_bundle_indices.is_empty() { - break 'brk None; - } - break 'rbi route_bundle_indices[0]; - }; + 'brk: { + let route_bundle_index = 'rbi: { + if let Some(first) = current_bundle!().requests.last() { + break 'rbi first.route_bundle_index; + } + let route_bundle_indices = + current_bundle!().promise.route_bundle_indices.keys(); + if route_bundle_indices.is_empty() { + break 'brk None; + } + break 'rbi route_bundle_indices[0]; + }; + + // Note: index `route_bundles` immutably so `dev.relative_path` + // / `dev.router` / `dev.server_graph` reads below stay disjoint. + break 'brk match &dev.route_bundles[route_bundle_index.get() as usize].data + { + route_bundle::Data::Html(html) => { + Some(dev.relative_path(&mut *buf, &html.html_bundle.bundle.path)) + } + route_bundle::Data::Framework(fw) => 'file_name: { + let route = dev.router.route_ptr(fw.route_index); + let opaque_id = match route.file_page.or(route.file_layout) { + Some(id) => id, + None => break 'file_name None, + }; + let server_index = + from_opaque_file_id::<{ bake::Side::Server }>(opaque_id); + let abs_path = &dev.server_graph.bundled_files.keys() + [server_index.get() as usize]; + break 'file_name Some(dev.relative_path(&mut *buf, abs_path)); + } + }; + } + }; - // Note: index `route_bundles` immutably so `dev.relative_path` - // / `dev.router` / `dev.server_graph` reads below stay disjoint. - break 'brk match &dev.route_bundles[route_bundle_index.get() as usize].data { - route_bundle::Data::Html(html) => { - Some(dev.relative_path(&mut *buf, &html.html_bundle.bundle.path)) - } - route_bundle::Data::Framework(fw) => 'file_name: { - let route = dev.router.route_ptr(fw.route_index); - let opaque_id = match route.file_page.or(route.file_layout) { - Some(id) => id, - None => break 'file_name None, - }; - let server_index = - from_opaque_file_id::<{ bake::Side::Server }>(opaque_id); - let abs_path = - &dev.server_graph.bundled_files.keys()[server_index.get() as usize]; - break 'file_name Some(dev.relative_path(&mut *buf, abs_path)); - } - }; + let total_count = bv2.graph.entry_points.len(); + if let Some(name) = file_name { + bun_core::pretty_error!(": {}", bstr::BStr::new(name)); + if total_count > 1 { + bun_core::pretty_error!(" + {} more", total_count - 1); + } } - }; + } + bun_core::pretty_error!("\n"); + Output::flush(); - let total_count = bv2.graph.entry_points.len(); - if let Some(name) = file_name { - bun_core::pretty_error!(": {}", bstr::BStr::new(name)); - if total_count > 1 { - bun_core::pretty_error!(" + {} more", total_count - 1); - } + if let Some(agent) = dev.inspector() { + agent.notify_bundle_complete(dev.inspector_server_id, ms_elapsed as f64); } } - bun_core::pretty_error!("\n"); - Output::flush(); - if let Some(agent) = dev.inspector() { - agent.notify_bundle_complete(dev.inspector_server_id, ms_elapsed as f64); + Ok(BundleOutcome::Serve) + })?; + + match outcome { + BundleOutcome::Failed(contexts) => { + for ctx in contexts { + ctx.deref(); + } + Ok(()) } + BundleOutcome::Serve => finalize_bundle_serve_requests(cell), } - - // Release the lock because the underlying handler may acquire one. - dev.graph_safety_lock.unlock(); - let served = finalize_bundle_serve_requests(dev); - dev.graph_safety_lock.lock(); - served } /// The tail of `finalize_bundle`: hand the finished bundle to every request -/// that was waiting on it. Runs with `graph_safety_lock` released. -fn finalize_bundle_serve_requests(dev: &mut DevServer) -> JsResult<()> { - macro_rules! current_bundle { - () => { - dev.current_bundle - .as_mut() - .expect("infallible: bundle active") +/// that was waiting on it. The framework's request handler runs outside the +/// dev server borrow. +fn finalize_bundle_serve_requests(cell: &DevServerCell) -> JsResult<()> { + cell.with_mut(|dev| -> JsResult<()> { + // Set all the deferred routes to the .loaded state up front + let Some(current_bundle) = dev.current_bundle.as_ref() else { + unreachable!("infallible: bundle active") }; - } + for i in 0..current_bundle.requests.len() { + let rbi = dev.current_bundle.as_ref().unwrap().requests[i].route_bundle_index; + dev.route_bundle_ptr(rbi).server_state = route_bundle::State::Loaded; + } - // Set all the deferred routes to the .loaded state up front - for i in 0..current_bundle!().requests.len() { - let rbi = current_bundle!().requests[i].route_bundle_index; - dev.route_bundle_ptr(rbi).server_state = route_bundle::State::Loaded; - } + if dev + .current_bundle + .as_ref() + .unwrap() + .promise + .strong + .has_value() + { + dev.set_current_bundle_route_states(route_bundle::State::Loaded); + let global = dev.global(); + let mut promise = + scopeguard::guard(&mut dev.current_bundle.as_mut().unwrap().promise, |p| { + p.deinit_idempotently() + }); + promise.strong.resolve(global, JSValue::TRUE)?; + } + Ok(()) + })?; - if current_bundle!().promise.strong.has_value() { - dev.set_current_bundle_route_states(route_bundle::State::Loaded); - let vm = dev.vm; - let _exit = vm.enter_event_loop_scope(); - let mut promise = - scopeguard::guard(&mut current_bundle!().promise, |p| p.deinit_idempotently()); - promise.strong.resolve(vm.global(), JSValue::TRUE)?; - } + loop { + // Pop the next request and get it ready under the borrow... + let next = cell.with_mut(|dev| -> JsResult> { + let Some(req) = dev + .current_bundle + .as_mut() + .expect("infallible: bundle active") + .requests + .pop() + else { + return Ok(None); + }; + let req = scopeguard::guard(req, DeferredRequest::release); - while let Some(req) = current_bundle!().requests.pop() { + dev.route_bundle_ptr(req.route_bundle_index).server_state = route_bundle::State::Loaded; + + // Note: `SavedRequest` is move-only (`Strong` field). Take the + // handler by value so the `Saved` payload moves into the union; the + // request is being released regardless. + let call = match req.handler.replace(Handler::Aborted) { + Handler::Aborted => None, + Handler::ServerHandler(saved) => { + let url = match saved.request() { + Some(r) => bun_core::StringView::new(r.url.get()), + None => bun_core::StringView::EMPTY, + }; + let call = dev + .prepare_framework_request(req.route_bundle_index, url.to_utf8().slice()); + Some((call, saved)) + } + Handler::BundledHtmlPage(ram) => { + dev.on_html_request_with_bundle( + req.route_bundle_index, + ram.response, + ram.method, + ); + None + } + }; + Ok(Some(( + scopeguard::ScopeGuard::into_inner(req), + call, + dev.server, + ))) + })?; + let Some((req, call, server)) = next else { + return Ok(()); + }; + // ...then run the framework's handler and release it outside. let req = scopeguard::guard(req, DeferredRequest::release); - - dev.route_bundle_ptr(req.route_bundle_index).server_state = route_bundle::State::Loaded; - - // Note: `SavedRequest` is move-only (`Strong` field). Take the - // handler by value so the `Saved` payload moves into the union; the - // request is being released regardless. - match req.handler.replace(Handler::Aborted) { - Handler::Aborted => continue, - Handler::ServerHandler(saved) => { - let response = saved.response; - let ctx = saved.ctx; - // Note: `saved` is moved out (so `release` sees `Aborted`); - // `js_request: StrongOptional` releases on Drop, but - // `ctx: AnyRequestContext` is `Copy` — explicitly balance the - // `ctx.ref_()` from `defer_request` here so the request - // context's `on_request_complete` (and thus the server's - // `pending_requests--`) eventually fires. Without this the - // bake-harness graceful-exit deinit check ("Failed to trigger - // deinit") never sees DevServer Drop. - scopeguard::defer! { ctx.deref() }; - let url = match saved.request() { - Some(r) => bun_core::StringView::new(r.url.get()), - None => bun_core::StringView::EMPTY, - }; - let call = - dev.prepare_framework_request(req.route_bundle_index, url.to_utf8().slice())?; - call.run(dev.server, SavedRequestUnion::Saved(saved), response); - } - Handler::BundledHtmlPage(ram) => { - dev.on_html_request_with_bundle(req.route_bundle_index, ram.response, ram.method) - } + if let Some((call, saved)) = call { + let response = saved.response; + let ctx = saved.ctx; + // Note: `saved` is moved out (so `release` sees `Aborted`); + // `js_request: StrongOptional` releases on Drop, but + // `ctx: AnyRequestContext` is `Copy` — explicitly balance the + // `ctx.ref_()` from `defer_request` here so the request + // context's `on_request_complete` (and thus the server's + // `pending_requests--`) eventually fires. Without this the + // bake-harness graceful-exit deinit check ("Failed to trigger + // deinit") never sees DevServer Drop. + scopeguard::defer! { ctx.deref() }; + call?.run(server, SavedRequestUnion::Saved(saved), response); } + drop(req); } - Ok(()) } impl DevServer { @@ -4241,7 +4463,10 @@ impl DevServer { } } - fn start_next_bundle_if_present(&mut self) { + /// If requests, a reload event or a `bundleNewRoute` promise are waiting + /// on the next bundle, returns that bundle for `start_async_bundle`. + #[must_use] + fn start_next_bundle_if_present(&mut self) -> Option { debug_assert!(self.magic == Magic::Valid); // Clear the current bundle debug_assert!(self.current_bundle.is_none()); @@ -4273,13 +4498,17 @@ impl DevServer { .expect("oom"); } + self.next_bundle.route_queue.clear_retaining_capacity(); + if !entry_points.set.is_empty() { - self.start_async_bundle(entry_points, is_reload, timer) - .expect("oom"); + return Some(BundleRequest { + entry_points, + had_reload_event: is_reload, + timer, + }); } - - self.next_bundle.route_queue.clear_retaining_capacity(); } + None } /// Note: The log is not consumed here @@ -4484,15 +4713,20 @@ impl From for OpaqueFileIdOrOptional { } fn on_request( - dev: &mut DevServer, + cell: &DevServerCell, req: &mut Request, mut resp: AnyResponse, ) -> JsResult { - let mut params: framework_router::MatchedParams = Default::default(); - if let Some(route_index) = dev.router.match_slow(req.url(), &mut params) { - let route_bundle_index = dev - .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) - .expect("oom"); + let route_bundle_index = cell.with_mut(|dev| { + let mut params: framework_router::MatchedParams = Default::default(); + dev.router + .match_slow(req.url(), &mut params) + .map(|route_index| { + dev.get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) + .expect("oom") + }) + }); + if let Some(route_bundle_index) = route_bundle_index { let mut ctx = RequestEnsureRouteBundledCtx { req: ReqOrSaved::Req(req), resp, @@ -4500,8 +4734,7 @@ fn on_request( route_bundle_index, framework_call: None, }; - let rbi = ctx.route_bundle_index; - return match ensure_route_is_bundled(dev, rbi, &mut ctx) { + return match ensure_route_is_bundled(cell, route_bundle_index, &mut ctx) { Err(jsc::JsError::OutOfMemory) => bun_core::out_of_memory(), Err(err) => Err(err), Ok(()) => Ok(match ctx.framework_call { @@ -4511,7 +4744,7 @@ fn on_request( }; } - if !dev.server.config().on_request.is_empty() { + if !cell.get().server.config().on_request.is_empty() { return Ok(RequestAction::UserHandler); } @@ -4520,18 +4753,20 @@ fn on_request( } impl DevServer { + /// `Bun.serve`'s HTML-import routes in development: serve the bundled + /// page for `html`. pub(crate) fn respond_for_html_bundle( - &mut self, + cell: &DevServerCell, html: bun_ptr::ThisPtr, req: &mut Request, resp: AnyResponse, ) -> Result<(), AllocError> { - if !is_allowed_dev_host(self, req) { + if !is_allowed_dev_host(cell.get(), req) { host_forbidden(resp); return Ok(()); } - let route_bundle_index = self - .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Html(html)) + let route_bundle_index = cell + .with_mut(|dev| dev.get_or_put_route_bundle(route_bundle::UnresolvedIndex::Html(html))) .map_err(|_| AllocError)?; let mut ctx = RequestEnsureRouteBundledCtx { req: ReqOrSaved::Req(req), @@ -4540,8 +4775,7 @@ impl DevServer { route_bundle_index, framework_call: None, }; - let rbi = ctx.route_bundle_index; - match ensure_route_is_bundled(self, rbi, &mut ctx) { + match ensure_route_is_bundled(cell, route_bundle_index, &mut ctx) { Ok(()) => {} // This is the dev server's entry from Bun.serve's static-route // trampoline (`StaticRouteLike`, which otherwise never enters @@ -5463,24 +5697,36 @@ impl DevServer { } pub(crate) fn on_plugins_resolved( - &mut self, + cell: &DevServerCell, plugins: Option<*mut crate::api::js_bundler::Plugin>, ) -> crate::Result<()> { - self.bundler_options.plugin = plugins.and_then(::core::ptr::NonNull::new); - self.plugin_state = PluginState::Loaded; - self.start_next_bundle_if_present(); - Ok(()) + let next_bundle = cell.with_mut(|dev| { + dev.bundler_options.plugin = plugins.and_then(::core::ptr::NonNull::new); + dev.plugin_state = PluginState::Loaded; + dev.start_next_bundle_if_present() + }); + match next_bundle { + Some(next_bundle) => Self::start_async_bundle(cell, next_bundle), + None => Ok(()), + } } - pub(crate) fn on_plugins_rejected(&mut self) -> crate::Result<()> { + /// Returns the requests that were waiting on the plugins; the caller + /// aborts them (`abort_deferred_requests`) once the dev server borrow ends, + /// since aborting a request runs its abort-signal handlers. + pub(crate) fn on_plugins_rejected(&mut self) -> deferred_request::List { self.plugin_state = PluginState::Err; - while let Some(d) = self.next_bundle.requests.pop() { - d.abort(); - DeferredRequest::release(d); - } + let requests = ::core::mem::take(&mut self.next_bundle.requests); self.next_bundle.route_queue.clear_retaining_capacity(); // TODO: allow recovery from this state - Ok(()) + requests + } + + pub(crate) fn abort_deferred_requests(requests: deferred_request::List) { + for req in requests.into_iter().rev() { + req.abort(); + DeferredRequest::release(req); + } } } @@ -5683,7 +5929,7 @@ pub fn bundle_new_route_js_function_impl( let vm = DevServerCell::get(&dev_cell).vm; let _exit = vm.enter_event_loop_scope(); - let (route_bundle_index, ctx) = dev_cell.with_mut(|dev| { + let route_bundle_index = dev_cell.with_mut(|dev| { let mut params: framework_router::MatchedParams = Default::default(); let Some(route_index) = dev.router.match_slow(pathname, &mut params) else { return Err(global.throw(format_args!( @@ -5691,20 +5937,17 @@ pub fn bundle_new_route_js_function_impl( bstr::BStr::new(pathname) ))); }; - - let route_bundle_index = dev + Ok(dev .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) - .expect("oom"); - let mut ctx = PromiseEnsureRouteBundledCtx { - global, - promise: None, - route_bundle_index, - settled: false, - }; - - ensure_route_is_bundled(dev, route_bundle_index, &mut ctx)?; - Ok((route_bundle_index, ctx)) + .expect("oom")) })?; + let mut ctx = PromiseEnsureRouteBundledCtx { + global, + promise: None, + route_bundle_index, + settled: false, + }; + ensure_route_is_bundled(&dev_cell, route_bundle_index, &mut ctx)?; if ctx.settled { VirtualMachine::get_mut().drain_microtasks(); } diff --git a/src/runtime/bake/dev_server/assets.rs b/src/runtime/bake/dev_server/assets.rs index 6dfa47e1aad5..55357a55fdd1 100644 --- a/src/runtime/bake/dev_server/assets.rs +++ b/src/runtime/bake/dev_server/assets.rs @@ -31,9 +31,6 @@ pub struct Assets { pub(crate) needs_reindex: bool, } -// SAFETY: `Assets` is only ever constructed as the `assets` field of -// `DevServer` (which is `Box`-allocated and never moved post-init). - impl Assets { pub(crate) fn get_hash(&self, path: &[u8]) -> Option { self.path_map diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 2db3e70969f5..39ef11e77e08 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -175,6 +175,7 @@ impl HmrSocket { let _ = ws.send(&response, Opcode::Binary, false, true); } x if x == IncomingMessageId::TestingBatchEvents as u8 => { + let mut bundle = None; let close = dev_cell.with_mut(|dev| match &dev.testing_batch_events { super::TestingBatchEvents::Disabled => { if dev.current_bundle.is_some() { @@ -212,12 +213,17 @@ impl HmrSocket { return false; } - let timer = std::time::Instant::now(); - dev.start_async_bundle(event.entry_points, true, timer) - .expect("OOM"); + bundle = Some(super::BundleRequest { + entry_points: event.entry_points, + had_reload_event: true, + timer: std::time::Instant::now(), + }); false } }); + if let Some(bundle) = bundle { + DevServer::start_async_bundle(&dev_cell, bundle).expect("OOM"); + } if close { ws.close(); } diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index 5e5cd22302f3..2c2c9f20980a 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -40,6 +40,7 @@ pub(crate) const CLIENT_PREFIX: &str = "/_bun/client"; // blocks and submodules name a single type. Re-export so // `crate::bake::dev_server::DevServer` (the public path used by `server/`, // `dispatch.rs`, …) resolves to that one struct. +pub(crate) use super::dev_server_body::BundleRequest; pub use super::dev_server_body::{ CacheEntry, CurrentBundle, DeferredPromise, DeferredRequest, DevServer, DevServerCell, EntryPointList, HTMLRouter, Magic, NextBundle, Options, PluginState, RouteIndexAndRecurseFlag, @@ -357,12 +358,18 @@ pub struct HotReloadEvent { impl HotReloadEvent { fn new() -> HotReloadEvent { HotReloadEvent { - data: bun_threading::Guarded::new(HotReloadFiles { - files: Default::default(), - dirs: Default::default(), - extra_files: Vec::new(), - timer: std::time::Instant::now(), - }), + data: bun_threading::Guarded::new(HotReloadFiles::default()), + } + } +} + +impl Default for HotReloadFiles { + fn default() -> Self { + HotReloadFiles { + files: Default::default(), + dirs: Default::default(), + extra_files: Vec::new(), + timer: std::time::Instant::now(), } } } @@ -554,8 +561,10 @@ impl HotReloadTask { return; }; let first = self.first; - dev.get() - .with_mut(|dev| dev.on_hot_reload_task(&self.shared, first)); + let cell = dev.get(); + if let Some(bundle) = cell.with_mut(|dev| dev.on_hot_reload_task(&self.shared, first)) { + let _ = DevServer::start_async_bundle(cell, bundle); + } } /// The task was still queued when its VM shut down; nothing to release @@ -564,7 +573,8 @@ impl HotReloadTask { } impl DevServer { - fn on_hot_reload_task(&mut self, shared: &HotReloadShared, first: u8) { + /// Returns the bundle to start for this event, if any. + fn on_hot_reload_task(&mut self, shared: &HotReloadShared, first: u8) -> Option { debug_assert!(self.magic == Magic::Valid); bun_core::scoped_log!(DevServer, "HMR Task start"); scopeguard::defer! { @@ -573,14 +583,14 @@ impl DevServer { if self.current_bundle.is_some() { self.next_bundle.reload_event = Some(first); - return; + return None; } let mut entry_points = EntryPointList::default(); let timer = self.drain_hot_reload_events(shared, first, &mut entry_points); if entry_points.set.count() == 0 { - return; + return None; } match &mut self.testing_batch_events { @@ -592,14 +602,16 @@ impl DevServer { &[MessageId::TestingWatchSynchronization.char(), 1], bun_uws::Opcode::BINARY, ); - return; + return None; } TestingBatchEvents::EnableAfterBundle => debug_assert!(false), } - if let Err(_err) = self.start_async_bundle(entry_points, true, timer) { - return; - } + Some(BundleRequest { + entry_points, + had_reload_event: true, + timer, + }) } /// Process the event in slot `first` and every event the watcher queued @@ -613,11 +625,13 @@ impl DevServer { let mut current = first; let mut timer = None; loop { - { - let mut files = shared.events[current as usize].data.lock(); - files.process_file_list(self, entry_points); - timer.get_or_insert(files.timer); - } + // Taken out so the slot's lock is not held across graph work + // (which may take `Watcher.mutex`; the watcher thread takes the + // two in the other order). + let mut files = ::core::mem::take(&mut *shared.events[current as usize].data.lock()); + files.process_file_list(self, entry_points); + timer.get_or_insert(files.timer); + *shared.events[current as usize].data.lock() = files; match shared.recycle_event_from_dev_server(current) { Some(next) => current = next, None => break, @@ -1000,8 +1014,7 @@ bun_bundler::link_impl_DevServerHandle! { // `'static` is a stand-in for the DevServer-self lifetime — see // the comment on `CurrentBundle.bv2`. let (bv2, result) = (&mut *bv2.cast(), &mut *result); - (*this) - .with_mut(|dev| super::dev_server_body::finalize_bundle(dev, bv2, result)) + super::dev_server_body::finalize_bundle(&*this, bv2, result) .map_err(|e| bun_bundler::Error::from(crate::Error::from(e))) }, handle_parse_task_failure(err, graph, abs_path, log, bv2) => { @@ -1076,7 +1089,9 @@ impl DevServer { /// `Transpiler.options.dev_server` / `LinkerContext.dev_server`. #[inline] pub(crate) fn bundler_handle(&self) -> bun_bundler::dispatch::DevServerHandle { - bun_bundler::dispatch::DevServerHandle::from_this(self.this()) + // Held by `CurrentBundle.bv2`, which this dev server (the cell's + // contents) owns and drops first. + bun_bundler::dispatch::DevServerHandle::from_owner(self.this.expect("set by init()")) } } diff --git a/src/runtime/server/HTMLBundle.rs b/src/runtime/server/HTMLBundle.rs index b4a4ed4910be..9484bf29579a 100644 --- a/src/runtime/server/HTMLBundle.rs +++ b/src/runtime/server/HTMLBundle.rs @@ -221,26 +221,27 @@ impl Route { }; if server.config().is_development() { - let handled = server.with_dev_server_mut(|dev| { + if let Some(dev) = server.dev_server_cell() { // DevServer's HMR path is *uws.Request-typed; H3 isn't routed // there (no h3_app on plain-HTTP debug servers in practice), // but stay defensive. match req { AnyRequest::H1(h1) => { // S008: `uws::Request` is an `opaque_ffi!` ZST — safe deref. - bun_core::handle_oom(dev.respond_for_html_bundle( - this, - bun_opaque::opaque_deref_mut(h1), - resp, - )); + bun_core::handle_oom( + crate::bake::DevServer::DevServer::respond_for_html_bundle( + &dev, + this, + bun_opaque::opaque_deref_mut(h1), + resp, + ), + ); } AnyRequest::H3(_) => { resp.write_status(b"503 Service Unavailable"); resp.end(b"DevServer HMR is HTTP/1.1 only", true); } } - }); - if handled.is_some() { return; } diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 0df04ad652bc..66113d1252b0 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -3469,8 +3469,14 @@ impl AnyServer { }) } - /// Run `f` on the DevServer (when configured). HTMLBundle's request path - /// mutates DevServer state (`respond_for_html_bundle`). + /// The attached DevServer's cell. + pub(crate) fn dev_server_cell( + &self, + ) -> Option> { + any_server_dispatch!(self, |s| s.dev_server_cell()) + } + + /// Run `f` on the DevServer (when configured). pub(crate) fn with_dev_server_mut( &self, f: impl FnOnce(&mut crate::bake::DevServer::DevServer) -> R, diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 9aca169b36e9..0a71d9a43904 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -1149,10 +1149,11 @@ impl ServePlugins { )); } if let Some(server) = dev_server { - if let Some(r) = - server.with_dev_server_mut(|dev| dev.on_plugins_resolved(Some(plugin.as_ptr()))) - { - bun_core::handle_oom(r); + if let Some(dev) = server.dev_server_cell() { + bun_core::handle_oom(crate::bake::DevServer::DevServer::on_plugins_resolved( + &dev, + Some(plugin.as_ptr()), + )); } } } @@ -1176,8 +1177,8 @@ impl ServePlugins { bun_core::handle_oom(route.on_plugins_rejected()); } if let Some(server) = dev_server { - if let Some(r) = server.with_dev_server_mut(|dev| dev.on_plugins_rejected()) { - bun_core::handle_oom(r); + if let Some(requests) = server.with_dev_server_mut(|dev| dev.on_plugins_rejected()) { + crate::bake::DevServer::DevServer::abort_deferred_requests(requests); } } From f8e17919d085f041cd81eb1a58275b49089f6319 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 21:29:41 +0000 Subject: [PATCH 04/12] bake: release failed-bundle request contexts on error paths; keep resolver log settings when taking entry-point failures --- src/bundler/bundle_v2.rs | 20 +++++++++++--------- src/runtime/bake/DevServer.rs | 13 +++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index c20182a5b59e..225b3dbff4cf 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3301,7 +3301,14 @@ pub mod bv2_impl { Ok(r) => r, Err(err) => { // The dev server handles these once `start_from_bake_dev_server` - // returns (it is mid-borrow while this runs). + // returns; handling them needs `&mut self`, which the dispatch + // handle must not alias. + // SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here. + let log = unsafe { &mut *(*transpiler).log }; + let mut taken = bun_ast::Log::init(); + taken.level = log.level; + taken.clone_line_text = log.clone_line_text; + core::mem::swap(&mut taken, log); resolve_failures.push(EntryPointResolveFailure { err, graph: if flags.client() { @@ -3310,11 +3317,7 @@ pub mod bv2_impl { bake::Graph::Server }, abs_path: Box::from(&**abs_path), - // SAFETY: `transpiler.log` is the `'a`-owned log; sole writer here. - log: core::mem::replace( - unsafe { &mut *(*transpiler).log }, - bun_ast::Log::init(), - ), + log: taken, }); continue; } @@ -5468,9 +5471,8 @@ pub mod bv2_impl { /// Dev Server uses this instead to run a subset of the transpiler, and to run it asynchronously. /// Entry points that failed to resolve are returned for the dev server - /// to record (`DevServer::handle_parse_task_failure`) rather than - /// reported through the dispatch handle, because the dev server is - /// mid-borrow while this runs. + /// to record (`DevServer::handle_parse_task_failure`, which needs this + /// `BundleV2` mutably) rather than reported through the dispatch handle. pub fn start_from_bake_dev_server( &mut self, bake_entry_points: &bake_types::EntryPointList, diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 8f929bb8a14e..37a117c75c7c 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -4171,7 +4171,14 @@ fn finalize_bundle_phases( // Only the first failure payload sent notifies the inspector. let mut notify_inspector = dev.inspector().is_some(); - let mut release_after = Vec::new(); + // Released on the error paths too (guard), but normally handed out + // so the final releases happen after this borrow. + let mut release_after = + scopeguard::guard(Vec::::new(), |contexts| { + for ctx in contexts { + ctx.deref(); + } + }); if current_bundle!().promise.strong.has_value() { dev.set_current_bundle_route_states(route_bundle::State::PossibleBundlingFailures); let global = dev.global(); @@ -4222,7 +4229,9 @@ fn finalize_bundle_phases( )?; } - return Ok(BundleOutcome::Failed(release_after)); + return Ok(BundleOutcome::Failed(scopeguard::ScopeGuard::into_inner( + release_after, + ))); } if dev.bundling_failures.is_empty() { From 8dccef8ca83be030406679e4288d7577243ab5ca Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 22:03:58 +0000 Subject: [PATCH 05/12] DevServer: bind heap before bv2 so an early return drops the bundle before its arena; dedupe finalize_bundle doc --- src/runtime/bake/DevServer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 37a117c75c7c..06945ec56600 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -2642,10 +2642,12 @@ impl DevServer { had_reload_event, timer, } = request; + // Bound in this order so that on an early return `bv2` (which borrows + // `*heap`) drops before `heap`. let BundleSetup { - mut bv2, - ast_memory_store, heap, + ast_memory_store, + mut bv2, } = cell.with_mut(|dev| dev.start_async_bundle_setup(&entry_points))?; // AST nodes built while the entry points are enqueued live exactly as @@ -3363,8 +3365,6 @@ fn finalize_bundle_cleanup( dev.start_next_bundle_if_present() } -/// Called at the end of BundleV2 to index bundle contents into the `IncrementalGraph`s -/// This function does not recover DevServer state if it fails (allocation failure) /// Called at the end of BundleV2 to index bundle contents into the /// `IncrementalGraph`s. This function does not recover DevServer state if it /// fails (allocation failure). From e8a8a9f72299f0b66240f0e75d14aa8887743669 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Aug 2026 22:27:20 +0000 Subject: [PATCH 06/12] bake_body: the framework projection is an Arc now, not arena-borrowed --- src/runtime/bake/bake_body.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runtime/bake/bake_body.rs b/src/runtime/bake/bake_body.rs index f6ae531049e5..fa37c0860dcc 100644 --- a/src/runtime/bake/bake_body.rs +++ b/src/runtime/bake/bake_body.rs @@ -1162,9 +1162,7 @@ impl Framework { out.options.minify_identifiers = minify_identifiers.unwrap_or(mode != Mode::Development); out.options.minify_whitespace = minify_whitespace.unwrap_or(mode != Mode::Development); out.options.css_chunking = true; - // The bundler crate (lower tier) carries a TYPE_ONLY projection - // (`bake_types::Framework`); construct it here and give it arena - // lifetime so `BundleOptions<'a>` can borrow it for the bundle pass. + // The bundler crate (lower tier) carries a TYPE_ONLY projection (`bake_types::Framework`). out.options.framework = Some(std::sync::Arc::new(self.as_bundler_view())); out.options.inline_entrypoint_import_meta_main = true; if let Some(ignore) = bundler_options.ignore_dce_annotations { From 1c01e4bbe4d40113129313f97b8394fc46d6136c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 25 Aug 2026 10:18:43 +0000 Subject: [PATCH 07/12] dev_server: the watcher slot guard is only written in debug builds --- src/runtime/bake/dev_server/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index 2c2c9f20980a..96f5f5abeaa2 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -772,6 +772,7 @@ impl HotReloadShared { /// /// Called from watcher thread. pub(crate) fn watcher_acquire_event(&self) -> u8 { + #[cfg_attr(not(debug_assertions), allow(unused_mut))] // written only by the debug slot check let mut w = self.watcher.lock(); let mut available = [true; 3]; if let Some(i) = w.current_event { From 424f93760b6f864fdef6003a07fdb8fb5e84a9c3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:25:05 +0000 Subject: [PATCH 08/12] [autofix.ci] apply automated fixes --- src/runtime/bake/dev_server/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index 96f5f5abeaa2..0ff9385a7a97 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -772,7 +772,8 @@ impl HotReloadShared { /// /// Called from watcher thread. pub(crate) fn watcher_acquire_event(&self) -> u8 { - #[cfg_attr(not(debug_assertions), allow(unused_mut))] // written only by the debug slot check + #[cfg_attr(not(debug_assertions), allow(unused_mut))] + // written only by the debug slot check let mut w = self.watcher.lock(); let mut available = [true; 3]; if let Some(i) = w.current_event { From 8482fb3e09417f3408d0daa303a3ab2cff2e4330 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 25 Aug 2026 11:10:24 +0000 Subject: [PATCH 09/12] WebSocketHandlerRef doc: link the one upgrade_ref that exists --- src/uws_sys/WebSocket.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uws_sys/WebSocket.rs b/src/uws_sys/WebSocket.rs index 3f52b3fbf72b..031afd5c00d3 100644 --- a/src/uws_sys/WebSocket.rs +++ b/src/uws_sys/WebSocket.rs @@ -322,8 +322,8 @@ pub trait WebSocketHandler: Sized + 'static { } /// A [`WebSocketHandler`] that is an intrusively ref-counted heap object. -/// [`AnyWebSocket::upgrade_ref`] / [`uws::response::Response::upgrade_ref`] -/// move one ref into the socket's user-data slot; every callback runs under a +/// [`Response::upgrade_ref`](crate::response::Response::upgrade_ref) moves one +/// ref into the socket's user-data slot; every callback runs under a /// fresh ref guard (so `ws.close()` from inside a handler — which dispatches /// `on_close` synchronously — cannot free the object under the running /// handler); the socket's own ref is released after `on_close` returns. From 8146ed74b5ba282b21357f50df9ad672b90a2bec Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 27 Aug 2026 08:49:11 +0000 Subject: [PATCH 10/12] bake: adopt RefPtr RAII release from main --- src/collections/zig_hash_map.rs | 7 +++++++ src/http_jsc/headers_jsc.rs | 4 +--- src/jsc/FetchHeaders.rs | 4 ++-- src/runtime/bake/DevServer.rs | 11 ++++------- src/runtime/bake/dev_server/hmr_socket.rs | 4 +--- src/runtime/server/AnyRequestContext.rs | 6 ++---- src/runtime/server/RequestContext.rs | 9 +-------- src/uws_sys/WebSocket.rs | 16 +++++++--------- 8 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/collections/zig_hash_map.rs b/src/collections/zig_hash_map.rs index f1e5fcc0cc7c..6c98c2c291b8 100644 --- a/src/collections/zig_hash_map.rs +++ b/src/collections/zig_hash_map.rs @@ -637,6 +637,13 @@ impl<'a, K, V, C> IntoIterator for &'a mut HashMap { self.iter_mut() } } +impl IntoIterator for HashMap { + type Item = (K, V); + type IntoIter = core::iter::Flatten>>; + fn into_iter(self) -> Self::IntoIter { + self.slots.into_iter().flatten() + } +} // ─── Entry API ───────────────────────────────────────────────────────────── diff --git a/src/http_jsc/headers_jsc.rs b/src/http_jsc/headers_jsc.rs index 7bee55d65fae..5eed073ef893 100644 --- a/src/http_jsc/headers_jsc.rs +++ b/src/http_jsc/headers_jsc.rs @@ -149,7 +149,6 @@ pub fn to_fetch_headers_ref( global: &JSGlobalObject, ) -> JsResult { use bun_http_types::ETag::HeaderEntryColumns; - use bun_jsc::JsError; if this.entries.len() == 0 { return Ok(bun_jsc::fetch_headers::HeadersRef::create_empty()); } @@ -159,9 +158,8 @@ pub fn to_fetch_headers_ref( this.entries.items_value(), // `from_bytes` scans for non-ASCII and tags UTF-8; `init` would leave // the buffer Latin-1 and mojibake any UTF-8 header value bytes ≥0x80. - &ZigString::from_bytes(this.buf.as_slice()), + &EncodedSlice::from_bytes(this.buf.as_slice()), ) - .ok_or(JsError::Thrown) } struct H2TestingAPIs; diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index b157452e45dc..bd889c582a32 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -344,8 +344,8 @@ impl HeadersRef { global: &JSGlobalObject, names: &[StringPointer], values: &[StringPointer], - buf: &ZigString, - ) -> Option { + buf: &EncodedSlice, + ) -> JsResult { assert_eq!(names.len(), values.len()); // C++ only reads the columns. FetchHeaders::create( diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 06945ec56600..72a0a290de5f 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -917,12 +917,11 @@ impl Drop for DevServer { // WebSockets should be deinitialized before other parts. Each socket is // detached from this dev server first (its `on_close`, dispatched // synchronously by `close()`, then only releases the socket itself). - for socket in ::core::mem::take(&mut self.active_websocket_connections).values() { + for (_, socket) in ::core::mem::take(&mut self.active_websocket_connections) { socket.detach_from_dev_server(self); if let Some(websocket) = socket.underlying.get() { websocket.close(); } - socket.deref(); } if self.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { @@ -1381,7 +1380,7 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D let socket = bun_ptr::RefPtr::new(HmrSocket::new(bun_ptr::BackRef::new(this.get()))); this.get().with_mut(|dev| { dev.active_websocket_connections - .insert(socket.as_ptr() as usize, socket.dupe_ref()) + .insert(socket.as_ptr() as usize, socket.clone()) }); // `res.upgrade_ref(..)` synchronously runs `HmrSocket::on_open`, which // enters the dev server itself, so no `with_mut` may span it. @@ -1861,9 +1860,7 @@ impl DevServer { Handler::ServerHandler(saved) => { saved.ctx.ref_(); saved.ctx.set_additional_on_abort_callback(Some( - crate::server::any_request_context::AdditionalOnAbortCallback( - deferred.dupe_ref(), - ), + crate::server::any_request_context::AdditionalOnAbortCallback(deferred.clone()), )); } Handler::Aborted => unreachable!(), @@ -2555,7 +2552,7 @@ impl DeferredRequest { #[allow(clippy::needless_pass_by_value)] // consumes the list's ref pub(crate) fn release(this: bun_ptr::RefPtr) { this.retire(); - this.deref(); + drop(this); } fn retire(&self) { diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 39ef11e77e08..78e8e322fc52 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -346,9 +346,7 @@ impl HmrSocket { .remove(&(std::ptr::from_ref(self) as usize)); debug_assert!(removed.is_some()); self.detach_from_dev_server(dev); - if let Some(map_ref) = removed { - map_ref.deref(); - } + drop(removed); }); } } diff --git a/src/runtime/server/AnyRequestContext.rs b/src/runtime/server/AnyRequestContext.rs index 97bd980b739b..900be2cb8534 100644 --- a/src/runtime/server/AnyRequestContext.rs +++ b/src/runtime/server/AnyRequestContext.rs @@ -154,10 +154,8 @@ macro_rules! dispatch { impl AnyRequestContext { pub(crate) fn set_additional_on_abort_callback(self, cb: Option) { dispatch!(self, (), |_T, ctx| { - if let Some(old) = ctx.additional_on_abort.replace(cb) { - debug_assert!(false, "additional_on_abort set twice"); - old.deref(); - } + let old = ctx.additional_on_abort.replace(cb); + debug_assert!(old.is_none(), "additional_on_abort set twice"); }) } diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index dc3a7e56df5c..ca1176bf9e4b 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -34,10 +34,6 @@ impl AdditionalOnAbortCallback { fn fire(&self) { self.0.on_abort_from_request_context(); } - /// Release the ref the callback holds. - pub fn deref(self) { - self.0.deref(); - } } // NOTE (transport selection): the response/request handle types vary with @@ -987,9 +983,7 @@ where self.request_body_take_unref(); - if let Some(cb) = self.additional_on_abort.replace(None) { - cb.deref(); - } + drop(self.additional_on_abort.replace(None)); if let Some(server) = self.server.take() { drop(self.pool_slot.take()); @@ -1484,7 +1478,6 @@ where let abort = this.additional_on_abort.replace(None); if let Some(abort) = abort { abort.fire(); - abort.deref(); } this.detach_response(); diff --git a/src/uws_sys/WebSocket.rs b/src/uws_sys/WebSocket.rs index 031afd5c00d3..0e5f6e30fb9f 100644 --- a/src/uws_sys/WebSocket.rs +++ b/src/uws_sys/WebSocket.rs @@ -327,9 +327,7 @@ pub trait WebSocketHandler: Sized + 'static { /// fresh ref guard (so `ws.close()` from inside a handler — which dispatches /// `on_close` synchronously — cannot free the object under the running /// handler); the socket's own ref is released after `on_close` returns. -pub trait WebSocketHandlerRef: - bun_ptr::AnyRefCounted + Sized + 'static -{ +pub trait WebSocketHandlerRef: bun_ptr::AnyRefCounted + Sized + 'static { const HAS_ON_MESSAGE: bool = true; const HAS_ON_DRAIN: bool = true; const HAS_ON_PING: bool = true; @@ -352,31 +350,31 @@ impl WebSocketHandler for T { unsafe fn on_open(this: *mut Self, ws: AnyWebSocket) { // SAFETY: trait contract — `this` is the socket's user-data, which holds a ref (`upgrade_ref`). let this = unsafe { bun_ptr::ThisPtr::new(this) }; - let _keep = this.ref_guard(); + let _keep = bun_ptr::RefPtr::from_this(this); ::on_open(this, ws) } unsafe fn on_message(this: *mut Self, ws: AnyWebSocket, message: &[u8], opcode: Opcode) { // SAFETY: see `on_open`. let this = unsafe { bun_ptr::ThisPtr::new(this) }; - let _keep = this.ref_guard(); + let _keep = bun_ptr::RefPtr::from_this(this); ::on_message(this, ws, message, opcode) } unsafe fn on_drain(this: *mut Self, ws: AnyWebSocket) { // SAFETY: see `on_open`. let this = unsafe { bun_ptr::ThisPtr::new(this) }; - let _keep = this.ref_guard(); + let _keep = bun_ptr::RefPtr::from_this(this); ::on_drain(this, ws) } unsafe fn on_ping(this: *mut Self, ws: AnyWebSocket, message: &[u8]) { // SAFETY: see `on_open`. let this = unsafe { bun_ptr::ThisPtr::new(this) }; - let _keep = this.ref_guard(); + let _keep = bun_ptr::RefPtr::from_this(this); ::on_ping(this, ws, message) } unsafe fn on_pong(this: *mut Self, ws: AnyWebSocket, message: &[u8]) { // SAFETY: see `on_open`. let this = unsafe { bun_ptr::ThisPtr::new(this) }; - let _keep = this.ref_guard(); + let _keep = bun_ptr::RefPtr::from_this(this); ::on_pong(this, ws, message) } unsafe fn on_close(this: *mut Self, ws: AnyWebSocket, code: i32, message: &[u8]) { @@ -384,7 +382,7 @@ impl WebSocketHandler for T { // calls back into this user-data afterwards, so the socket's ref is released here. let socket_ref = unsafe { bun_ptr::RefPtr::::from_raw(this) }; ::on_close(socket_ref.this_ptr(), ws, code, message); - socket_ref.deref(); + drop(socket_ref); } } From 4ac0750af5e1e6fe4856076628e9a821587f205c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 31 Aug 2026 10:15:00 +0000 Subject: [PATCH 11/12] hmr_socket: unsubscribing the memory visualizer checks its own subscriber count before removing the timer --- src/runtime/bake/dev_server/hmr_socket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/bake/dev_server/hmr_socket.rs b/src/runtime/bake/dev_server/hmr_socket.rs index 78e8e322fc52..b976593fd112 100644 --- a/src/runtime/bake/dev_server/hmr_socket.rs +++ b/src/runtime/bake/dev_server/hmr_socket.rs @@ -297,7 +297,7 @@ impl HmrSocket { } if field.contains(HmrTopic::MemoryVisualizer.as_bit()) { dev.emit_memory_visualizer_events -= 1; - if dev.emit_incremental_visualizer_events == 0 + if dev.emit_memory_visualizer_events == 0 && dev.memory_visualizer_timer.state == EventLoopTimerState::ACTIVE { crate::jsc_hooks::timer_all_mut().remove(&raw mut dev.memory_visualizer_timer); From 408cbf5be0c0aa74b09d0b148b74bf9f65108542 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 8 Sep 2026 12:29:42 +0000 Subject: [PATCH 12/12] DevServer: read Sec-WebSocket-Extensions (plural) on the HMR upgrade --- src/runtime/bake/DevServer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 72a0a290de5f..4ab1e9dafd6c 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -1388,7 +1388,7 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D socket, req.header(b"sec-websocket-key").unwrap_or(b""), req.header(b"sec-websocket-protocol").unwrap_or(b""), - req.header(b"sec-websocket-extension").unwrap_or(b""), + req.header(b"sec-websocket-extensions").unwrap_or(b""), Some(upgrade_ctx), ); }