Skip to content

Commit 9341058

Browse files
wan9chiclaude
andcommitted
feat(server): add Recorder — the runtime Handler used by vite_task
Recorder is a generic `Handler` impl parameterized by an env-lookup closure. It records: - ignored inputs / outputs (FxHashSet<Arc<AbsolutePath>>) - cache_disabled flag - env_records: name -> (tracked, resolved value), with `tracked` monotonically OR-ed across repeated get_env calls (once true, stays true) The runtime caller (vite_task in step 5) constructs `Recorder::new(|name| ...)` with a lookup closure over its own env source; after the driver resolves, `recorder.into_reports()` yields the `Reports` for the cache-update phase. Tests swap the ad-hoc `RecordingHandler` for `Recorder` — exercising the same code path runtime code will use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e95dfd6 commit 9341058

3 files changed

Lines changed: 142 additions & 60 deletions

File tree

crates/vite_task_server/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@ rust-version.workspace = true
1010
futures = { workspace = true }
1111
interprocess = { workspace = true, features = ["tokio"] }
1212
native_str = { workspace = true }
13+
rustc-hash = { workspace = true }
1314
tempfile = { workspace = true }
1415
tokio = { workspace = true, features = ["io-util", "net", "rt", "macros"] }
1516
tokio-util = { workspace = true }
1617
tracing = { workspace = true }
1718
vite_path = { workspace = true }
19+
vite_str = { workspace = true }
1820
vite_task_ipc_shared = { workspace = true }
1921
wincode = { workspace = true, features = ["derive"] }
2022

2123
[target.'cfg(windows)'.dependencies]
2224
uuid = { workspace = true, features = ["v4"] }
23-
vite_str = { workspace = true }
2425

2526
[dev-dependencies]
26-
rustc-hash = { workspace = true }
2727
tokio = { workspace = true, features = ["io-util", "net", "rt", "macros", "time"] }
28-
vite_str = { workspace = true }
2928
vite_task_client = { workspace = true }
3029

3130
[lints]

crates/vite_task_server/src/lib.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::{
2+
cell::{Cell, RefCell},
23
ffi::{OsStr, OsString},
34
io,
45
sync::Arc,
@@ -10,9 +11,11 @@ use interprocess::local_socket::{
1011
tokio::{Listener, Stream, prelude::*},
1112
};
1213
use native_str::NativeStr;
14+
use rustc_hash::{FxHashMap, FxHashSet};
1315
use tokio::io::{AsyncReadExt, AsyncWriteExt};
1416
use tokio_util::sync::CancellationToken;
1517
use vite_path::AbsolutePath;
18+
use vite_str::Str;
1619
use vite_task_ipc_shared::{IPC_ENV_NAME, Request, Response, ResponseBody};
1720

1821
pub trait Handler {
@@ -22,6 +25,92 @@ pub trait Handler {
2225
fn get_env(&self, name: &str, tracked: bool) -> Option<Arc<OsStr>>;
2326
}
2427

28+
/// A [`Handler`] that records every report and resolves `get_env` via a
29+
/// user-provided lookup closure.
30+
///
31+
/// Call [`Recorder::into_reports`] after the driver future completes to
32+
/// 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,
39+
}
40+
41+
/// A record of an env value requested via `get_env`.
42+
///
43+
/// `tracked` is the monotonic OR of every `tracked` flag sent for this name
44+
/// — once `true`, it stays `true`.
45+
#[derive(Debug, Clone, PartialEq, Eq)]
46+
pub struct EnvRecord {
47+
pub tracked: bool,
48+
pub value: Option<Arc<OsStr>>,
49+
}
50+
51+
/// The data collected by a [`Recorder`] over the server's lifetime.
52+
#[derive(Debug, Default)]
53+
pub struct Reports {
54+
pub ignored_inputs: FxHashSet<Arc<AbsolutePath>>,
55+
pub ignored_outputs: FxHashSet<Arc<AbsolutePath>>,
56+
pub cache_disabled: bool,
57+
pub env_records: FxHashMap<Str, EnvRecord>,
58+
}
59+
60+
impl<F> Recorder<F>
61+
where
62+
F: Fn(&str) -> Option<Arc<OsStr>>,
63+
{
64+
pub fn new(env_lookup: F) -> Self {
65+
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,
71+
}
72+
}
73+
74+
#[must_use]
75+
pub fn into_reports(self) -> Reports {
76+
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(),
81+
}
82+
}
83+
}
84+
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));
91+
}
92+
93+
fn ignore_output(&self, path: &Arc<AbsolutePath>) {
94+
self.ignored_outputs.borrow_mut().insert(Arc::clone(path));
95+
}
96+
97+
fn disable_cache(&self) {
98+
self.cache_disabled.set(true);
99+
}
100+
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) {
103+
existing.tracked |= tracked;
104+
return existing.value.clone();
105+
}
106+
let value = (self.env_lookup)(name);
107+
self.env_records
108+
.borrow_mut()
109+
.insert(name.into(), EnvRecord { tracked, value: value.clone() });
110+
value
111+
}
112+
}
113+
25114
/// Handle to a running IPC server.
26115
///
27116
/// `driver` must be polled to accept clients and handle messages. It resolves
Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,30 @@
11
use std::{
2-
cell::RefCell,
32
ffi::{OsStr, OsString},
43
sync::Arc,
54
thread,
65
};
76

87
use rustc_hash::FxHashMap;
98
use tokio::runtime::Builder;
10-
use vite_path::{AbsolutePath, AbsolutePathBuf};
11-
use vite_str::Str;
9+
use vite_path::AbsolutePathBuf;
1210
use vite_task_client::Client;
13-
use vite_task_server::{Handler, ServerHandle, serve};
14-
15-
#[derive(Default)]
16-
struct RecordingHandler {
17-
ignore_inputs: RefCell<Vec<Arc<AbsolutePath>>>,
18-
ignore_outputs: RefCell<Vec<Arc<AbsolutePath>>>,
19-
disable_cache_count: RefCell<u32>,
20-
env_calls: RefCell<Vec<(Str, bool)>>,
21-
env_map: FxHashMap<&'static str, &'static str>,
22-
}
23-
24-
impl Handler for RecordingHandler {
25-
fn ignore_input(&self, path: &Arc<AbsolutePath>) {
26-
self.ignore_inputs.borrow_mut().push(Arc::clone(path));
27-
}
28-
29-
fn ignore_output(&self, path: &Arc<AbsolutePath>) {
30-
self.ignore_outputs.borrow_mut().push(Arc::clone(path));
31-
}
32-
33-
fn disable_cache(&self) {
34-
*self.disable_cache_count.borrow_mut() += 1;
35-
}
36-
37-
fn get_env(&self, name: &str, tracked: bool) -> Option<Arc<OsStr>> {
38-
self.env_calls.borrow_mut().push((name.into(), tracked));
39-
self.env_map.get(name).map(|value| Arc::<OsStr>::from(OsStr::new(value)))
40-
}
41-
}
11+
use vite_task_server::{Recorder, Reports, ServerHandle, serve};
4212

4313
fn abs(path: &str) -> AbsolutePathBuf {
4414
AbsolutePathBuf::new(path.into()).expect("absolute path literal")
4515
}
4616

47-
fn run_with_server<F>(handler: RecordingHandler, client_work: F) -> RecordingHandler
17+
fn run_with_server<F>(env_map: FxHashMap<&'static str, &'static str>, client_work: F) -> Reports
4818
where
4919
F: FnOnce(OsString) + Send + 'static,
5020
{
21+
let recorder = Recorder::new(move |name: &str| {
22+
env_map.get(name).map(|value| Arc::<OsStr>::from(OsStr::new(value)))
23+
});
24+
5125
let rt = Builder::new_current_thread().enable_all().build().unwrap();
5226
rt.block_on(async move {
53-
let (envs, ServerHandle { driver, stop_accepting }) = serve(handler).expect("bind server");
27+
let (envs, ServerHandle { driver, stop_accepting }) = serve(recorder).expect("bind server");
5428
let name = envs.into_iter().next().expect("serve should yield an IPC env").1;
5529

5630
let client = async move {
@@ -61,55 +35,73 @@ where
6135
};
6236

6337
let (recorder, ()) = tokio::join!(driver, client);
64-
recorder
38+
recorder.into_reports()
6539
})
6640
}
6741

6842
#[test]
6943
fn single_client_fire_and_forget() {
70-
let h = run_with_server(RecordingHandler::default(), |name| {
44+
let reports = run_with_server(FxHashMap::default(), |name| {
7145
let mut client = Client::from_name(&name).expect("connect");
7246
client.ignore_input(abs("/tmp/in.txt").as_absolute_path()).unwrap();
7347
client.ignore_output(abs("/tmp/out.txt").as_absolute_path()).unwrap();
7448
client.disable_cache().unwrap();
7549
});
7650

77-
let inputs = h.ignore_inputs.into_inner();
78-
let outputs = h.ignore_outputs.into_inner();
79-
assert_eq!(inputs.len(), 1);
80-
assert_eq!(inputs[0].as_path().as_os_str(), OsStr::new("/tmp/in.txt"));
81-
assert_eq!(outputs.len(), 1);
82-
assert_eq!(outputs[0].as_path().as_os_str(), OsStr::new("/tmp/out.txt"));
83-
assert_eq!(h.disable_cache_count.into_inner(), 1);
51+
let inputs: Vec<_> = reports.ignored_inputs.iter().map(|p| p.as_path().as_os_str()).collect();
52+
let outputs: Vec<_> = reports.ignored_outputs.iter().map(|p| p.as_path().as_os_str()).collect();
53+
assert_eq!(inputs, vec![OsStr::new("/tmp/in.txt")]);
54+
assert_eq!(outputs, vec![OsStr::new("/tmp/out.txt")]);
55+
assert!(reports.cache_disabled);
8456
}
8557

8658
#[test]
8759
fn get_env_found_and_not_found() {
88-
let mut handler = RecordingHandler::default();
89-
handler.env_map.insert("NODE_ENV", "production");
60+
let mut env_map = FxHashMap::default();
61+
env_map.insert("NODE_ENV", "production");
9062

91-
let h = run_with_server(handler, |name| {
63+
let reports = run_with_server(env_map, |name| {
9264
let mut client = Client::from_name(&name).expect("connect");
9365
let present = client.get_env("NODE_ENV", true).unwrap();
9466
assert_eq!(present.as_deref(), Some(OsStr::new("production")));
9567
let missing = client.get_env("MISSING", false).unwrap();
9668
assert!(missing.is_none());
9769
});
9870

99-
let calls = h.env_calls.into_inner();
100-
assert_eq!(calls.len(), 2);
101-
assert_eq!(calls[0].0.as_str(), "NODE_ENV");
102-
assert!(calls[0].1);
103-
assert_eq!(calls[1].0.as_str(), "MISSING");
104-
assert!(!calls[1].1);
71+
let node = reports.env_records.get("NODE_ENV").expect("NODE_ENV recorded");
72+
assert!(node.tracked);
73+
assert_eq!(node.value.as_deref(), Some(OsStr::new("production")));
74+
75+
let missing = reports.env_records.get("MISSING").expect("MISSING recorded");
76+
assert!(!missing.tracked);
77+
assert!(missing.value.is_none());
78+
}
79+
80+
#[test]
81+
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| {
86+
let mut client = Client::from_name(&name).expect("connect");
87+
let a = client.get_env("NODE_ENV", false).unwrap();
88+
let b = client.get_env("NODE_ENV", true).unwrap();
89+
let c = client.get_env("NODE_ENV", false).unwrap();
90+
for v in [a, b, c] {
91+
assert_eq!(v.as_deref(), Some(OsStr::new("production")));
92+
}
93+
});
94+
95+
let node = reports.env_records.get("NODE_ENV").expect("recorded");
96+
assert!(node.tracked, "tracked must remain true once set");
10597
}
10698

10799
#[test]
108100
fn concurrent_clients() {
109-
let mut handler = RecordingHandler::default();
110-
handler.env_map.insert("SHARED", "value");
101+
let mut env_map = FxHashMap::default();
102+
env_map.insert("SHARED", "value");
111103

112-
let h = run_with_server(handler, |name| {
104+
let reports = run_with_server(env_map, |name| {
113105
let threads: Vec<_> = (0..4)
114106
.map(|i| {
115107
let name = name.clone();
@@ -127,6 +119,8 @@ fn concurrent_clients() {
127119
}
128120
});
129121

130-
assert_eq!(h.ignore_inputs.into_inner().len(), 4);
131-
assert_eq!(h.env_calls.into_inner().len(), 4);
122+
assert_eq!(reports.ignored_inputs.len(), 4);
123+
let shared = reports.env_records.get("SHARED").expect("recorded");
124+
assert!(shared.tracked);
125+
assert_eq!(shared.value.as_deref(), Some(OsStr::new("value")));
132126
}

0 commit comments

Comments
 (0)