Skip to content

Commit b792e0e

Browse files
wan9chiclaude
andcommitted
refactor(server): Handler methods take &mut self; Recorder takes env map
Handler trait methods now take `&mut self`. The server wraps `H` in `RefCell` internally so the N per-client futures that coexist in `FuturesUnordered` can each call &mut-self methods; on the single-threaded runtime the borrow check can't fail because handler methods are sync (no borrow spans an await). Recorder drops the generic env-lookup closure in favor of a concrete `FxHashMap<Str, Arc<OsStr>>` taken at construction. Handler impl uses plain owned fields (no interior mutability inside the handler itself). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9341058 commit b792e0e

2 files changed

Lines changed: 61 additions & 67 deletions

File tree

crates/vite_task_server/src/lib.rs

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
cell::{Cell, RefCell},
2+
cell::RefCell,
33
ffi::{OsStr, OsString},
44
io,
55
sync::Arc,
@@ -19,23 +19,23 @@ use vite_str::Str;
1919
use vite_task_ipc_shared::{IPC_ENV_NAME, Request, Response, ResponseBody};
2020

2121
pub trait Handler {
22-
fn ignore_input(&self, path: &Arc<AbsolutePath>);
23-
fn ignore_output(&self, path: &Arc<AbsolutePath>);
24-
fn disable_cache(&self);
25-
fn get_env(&self, name: &str, tracked: bool) -> Option<Arc<OsStr>>;
22+
fn ignore_input(&mut self, path: &Arc<AbsolutePath>);
23+
fn ignore_output(&mut self, path: &Arc<AbsolutePath>);
24+
fn disable_cache(&mut self);
25+
fn get_env(&mut self, name: &str, tracked: bool) -> Option<Arc<OsStr>>;
2626
}
2727

28-
/// A [`Handler`] that records every report and resolves `get_env` via a
29-
/// user-provided lookup closure.
28+
/// A [`Handler`] that records every report and resolves `get_env` against
29+
/// a provided env map.
3030
///
3131
/// Call [`Recorder::into_reports`] after the driver future completes to
3232
/// recover the collected [`Reports`].
33-
pub struct Recorder<F> {
34-
ignored_inputs: RefCell<FxHashSet<Arc<AbsolutePath>>>,
35-
ignored_outputs: RefCell<FxHashSet<Arc<AbsolutePath>>>,
36-
cache_disabled: Cell<bool>,
37-
env_records: RefCell<FxHashMap<Str, EnvRecord>>,
38-
env_lookup: F,
33+
pub struct Recorder {
34+
ignored_inputs: FxHashSet<Arc<AbsolutePath>>,
35+
ignored_outputs: FxHashSet<Arc<AbsolutePath>>,
36+
cache_disabled: bool,
37+
env_records: FxHashMap<Str, EnvRecord>,
38+
env_map: FxHashMap<Str, Arc<OsStr>>,
3939
}
4040

4141
/// A record of an env value requested via `get_env`.
@@ -57,56 +57,49 @@ pub struct Reports {
5757
pub env_records: FxHashMap<Str, EnvRecord>,
5858
}
5959

60-
impl<F> Recorder<F>
61-
where
62-
F: Fn(&str) -> Option<Arc<OsStr>>,
63-
{
64-
pub fn new(env_lookup: F) -> Self {
60+
impl Recorder {
61+
#[must_use]
62+
pub fn new(env_map: FxHashMap<Str, Arc<OsStr>>) -> Self {
6563
Self {
66-
ignored_inputs: RefCell::default(),
67-
ignored_outputs: RefCell::default(),
68-
cache_disabled: Cell::new(false),
69-
env_records: RefCell::default(),
70-
env_lookup,
64+
ignored_inputs: FxHashSet::default(),
65+
ignored_outputs: FxHashSet::default(),
66+
cache_disabled: false,
67+
env_records: FxHashMap::default(),
68+
env_map,
7169
}
7270
}
7371

7472
#[must_use]
7573
pub fn into_reports(self) -> Reports {
7674
Reports {
77-
ignored_inputs: self.ignored_inputs.into_inner(),
78-
ignored_outputs: self.ignored_outputs.into_inner(),
79-
cache_disabled: self.cache_disabled.get(),
80-
env_records: self.env_records.into_inner(),
75+
ignored_inputs: self.ignored_inputs,
76+
ignored_outputs: self.ignored_outputs,
77+
cache_disabled: self.cache_disabled,
78+
env_records: self.env_records,
8179
}
8280
}
8381
}
8482

85-
impl<F> Handler for Recorder<F>
86-
where
87-
F: Fn(&str) -> Option<Arc<OsStr>>,
88-
{
89-
fn ignore_input(&self, path: &Arc<AbsolutePath>) {
90-
self.ignored_inputs.borrow_mut().insert(Arc::clone(path));
83+
impl Handler for Recorder {
84+
fn ignore_input(&mut self, path: &Arc<AbsolutePath>) {
85+
self.ignored_inputs.insert(Arc::clone(path));
9186
}
9287

93-
fn ignore_output(&self, path: &Arc<AbsolutePath>) {
94-
self.ignored_outputs.borrow_mut().insert(Arc::clone(path));
88+
fn ignore_output(&mut self, path: &Arc<AbsolutePath>) {
89+
self.ignored_outputs.insert(Arc::clone(path));
9590
}
9691

97-
fn disable_cache(&self) {
98-
self.cache_disabled.set(true);
92+
fn disable_cache(&mut self) {
93+
self.cache_disabled = true;
9994
}
10095

101-
fn get_env(&self, name: &str, tracked: bool) -> Option<Arc<OsStr>> {
102-
if let Some(existing) = self.env_records.borrow_mut().get_mut(name) {
96+
fn get_env(&mut self, name: &str, tracked: bool) -> Option<Arc<OsStr>> {
97+
if let Some(existing) = self.env_records.get_mut(name) {
10398
existing.tracked |= tracked;
10499
return existing.value.clone();
105100
}
106-
let value = (self.env_lookup)(name);
107-
self.env_records
108-
.borrow_mut()
109-
.insert(name.into(), EnvRecord { tracked, value: value.clone() });
101+
let value = self.env_map.get(name).cloned();
102+
self.env_records.insert(name.into(), EnvRecord { tracked, value: value.clone() });
110103
value
111104
}
112105
}
@@ -155,8 +148,15 @@ pub fn serve<'h, H: Handler + 'h>(
155148

156149
let run_stop = stop_token.clone();
157150
let driver = async move {
151+
// Multiple per-client futures coexist inside `FuturesUnordered` and each
152+
// calls `&mut self` handler methods. `RefCell` provides the interior
153+
// mutability that makes these shared-access method calls compile; at
154+
// runtime the `borrow_mut()` never conflicts because we're on a
155+
// single-threaded runtime and handler methods are synchronous (no
156+
// awaits, so no borrow spans a yield point).
157+
let handler = RefCell::new(handler);
158158
run(bound, &handler, run_stop).await;
159-
handler
159+
handler.into_inner()
160160
}
161161
.boxed_local();
162162

@@ -203,7 +203,7 @@ const fn listener_of(bound: &Bound) -> &Listener {
203203
bound
204204
}
205205

206-
async fn run<H: Handler>(bound: Bound, handler: &H, shutdown: CancellationToken) {
206+
async fn run<H: Handler>(bound: Bound, handler: &RefCell<H>, shutdown: CancellationToken) {
207207
let mut clients = FuturesUnordered::new();
208208

209209
// Accept phase: accept new clients until shutdown fires.
@@ -233,7 +233,7 @@ async fn run<H: Handler>(bound: Bound, handler: &H, shutdown: CancellationToken)
233233
while clients.next().await.is_some() {}
234234
}
235235

236-
async fn handle_client<H: Handler>(mut stream: Stream, handler: &H) {
236+
async fn handle_client<H: Handler>(mut stream: Stream, handler: &RefCell<H>) {
237237
let mut buf = Vec::new();
238238
loop {
239239
match read_frame(&mut stream, &mut buf).await {
@@ -256,17 +256,17 @@ async fn handle_client<H: Handler>(mut stream: Stream, handler: &H) {
256256
match request {
257257
Request::IgnoreInput(ns) => {
258258
if let Some(path) = native_str_to_abs_path(ns) {
259-
handler.ignore_input(&path);
259+
handler.borrow_mut().ignore_input(&path);
260260
}
261261
}
262262
Request::IgnoreOutput(ns) => {
263263
if let Some(path) = native_str_to_abs_path(ns) {
264-
handler.ignore_output(&path);
264+
handler.borrow_mut().ignore_output(&path);
265265
}
266266
}
267-
Request::DisableCache => handler.disable_cache(),
267+
Request::DisableCache => handler.borrow_mut().disable_cache(),
268268
Request::GetEnv { id, name, tracked } => {
269-
let value = handler.get_env(name, tracked);
269+
let value = handler.borrow_mut().get_env(name, tracked);
270270
let boxed: Option<Box<NativeStr>> = value.as_deref().map(Into::into);
271271
let body = ResponseBody::Env(boxed.as_deref());
272272
let response = Response { id, body };

crates/vite_task_server/tests/integration.rs

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@ use std::{
77
use rustc_hash::FxHashMap;
88
use tokio::runtime::Builder;
99
use vite_path::AbsolutePathBuf;
10+
use vite_str::Str;
1011
use vite_task_client::Client;
1112
use vite_task_server::{Recorder, Reports, ServerHandle, serve};
1213

1314
fn abs(path: &str) -> AbsolutePathBuf {
1415
AbsolutePathBuf::new(path.into()).expect("absolute path literal")
1516
}
1617

17-
fn run_with_server<F>(env_map: FxHashMap<&'static str, &'static str>, client_work: F) -> Reports
18+
fn env_map(pairs: &[(&str, &str)]) -> FxHashMap<Str, Arc<OsStr>> {
19+
pairs.iter().map(|(k, v)| (Str::from(*k), Arc::<OsStr>::from(OsStr::new(v)))).collect()
20+
}
21+
22+
fn run_with_server<F>(envs: FxHashMap<Str, Arc<OsStr>>, client_work: F) -> Reports
1823
where
1924
F: FnOnce(OsString) + Send + 'static,
2025
{
21-
let recorder = Recorder::new(move |name: &str| {
22-
env_map.get(name).map(|value| Arc::<OsStr>::from(OsStr::new(value)))
23-
});
26+
let recorder = Recorder::new(envs);
2427

2528
let rt = Builder::new_current_thread().enable_all().build().unwrap();
2629
rt.block_on(async move {
@@ -41,7 +44,7 @@ where
4144

4245
#[test]
4346
fn single_client_fire_and_forget() {
44-
let reports = run_with_server(FxHashMap::default(), |name| {
47+
let reports = run_with_server(env_map(&[]), |name| {
4548
let mut client = Client::from_name(&name).expect("connect");
4649
client.ignore_input(abs("/tmp/in.txt").as_absolute_path()).unwrap();
4750
client.ignore_output(abs("/tmp/out.txt").as_absolute_path()).unwrap();
@@ -57,10 +60,7 @@ fn single_client_fire_and_forget() {
5760

5861
#[test]
5962
fn get_env_found_and_not_found() {
60-
let mut env_map = FxHashMap::default();
61-
env_map.insert("NODE_ENV", "production");
62-
63-
let reports = run_with_server(env_map, |name| {
63+
let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |name| {
6464
let mut client = Client::from_name(&name).expect("connect");
6565
let present = client.get_env("NODE_ENV", true).unwrap();
6666
assert_eq!(present.as_deref(), Some(OsStr::new("production")));
@@ -79,10 +79,7 @@ fn get_env_found_and_not_found() {
7979

8080
#[test]
8181
fn get_env_tracked_upgrade_is_monotonic() {
82-
let mut env_map = FxHashMap::default();
83-
env_map.insert("NODE_ENV", "production");
84-
85-
let reports = run_with_server(env_map, |name| {
82+
let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |name| {
8683
let mut client = Client::from_name(&name).expect("connect");
8784
let a = client.get_env("NODE_ENV", false).unwrap();
8885
let b = client.get_env("NODE_ENV", true).unwrap();
@@ -98,10 +95,7 @@ fn get_env_tracked_upgrade_is_monotonic() {
9895

9996
#[test]
10097
fn concurrent_clients() {
101-
let mut env_map = FxHashMap::default();
102-
env_map.insert("SHARED", "value");
103-
104-
let reports = run_with_server(env_map, |name| {
98+
let reports = run_with_server(env_map(&[("SHARED", "value")]), |name| {
10599
let threads: Vec<_> = (0..4)
106100
.map(|i| {
107101
let name = name.clone();

0 commit comments

Comments
 (0)