Skip to content

Commit fb19a12

Browse files
committed
feat(client): add getEnv tracked option
1 parent 397eb93 commit fb19a12

10 files changed

Lines changed: 125 additions & 25 deletions

File tree

crates/vite_task/src/session/execute/cache_update.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,18 @@ fn collect_tracked_envs(
191191
let fingerprinted = &metadata.spawn_fingerprint.env_fingerprints().fingerprinted_envs;
192192
let mut tracked_envs = BTreeMap::new();
193193

194-
for (name, value) in &reports.env_records {
194+
for (name, record) in &reports.env_records {
195+
if !record.tracked {
196+
continue;
197+
}
195198
let name_str = name
196199
.to_str()
197200
.ok_or_else(|| anyhow::anyhow!("tracked env name {name:?} is not valid UTF-8"))?;
198201
if fingerprinted.contains_key(name_str) {
199202
continue;
200203
}
201-
let value = value
204+
let value = record
205+
.value
202206
.as_ref()
203207
.map(|value| {
204208
let value_str = value.to_str().ok_or_else(|| {

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/scripts/fetch_env.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { getEnv } from '@voidzero-dev/vite-task-client';
22

3-
const names = process.argv.slice(2);
3+
const args = process.argv.slice(2);
4+
const tracked = args[0] === '--untracked' ? false : true;
5+
const names = tracked ? args : args.slice(1);
46

57
if (names.length === 0) {
6-
throw new Error('usage: fetch_env.mjs <NAME> [...]');
8+
throw new Error('usage: fetch_env.mjs [--untracked] <NAME> [...]');
79
}
810

9-
const served = names.map((name) => `${name}=${getEnv(name) ?? '(unset)'}`);
11+
const served = names.map((name) => `${name}=${getEnv(name, { tracked }) ?? '(unset)'}`);
1012
const own = names.map((name) => `${name}=${process.env[name] ?? '(unset)'}`);
1113

1214
console.log(`served ${served.join(' ')}`);

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,32 @@ steps = [
171171
],
172172
], comment = "cache miss: tracked env changed despite input auto-inference being disabled" },
173173
]
174+
175+
[[e2e]]
176+
name = "fetch_env_untracked_does_not_invalidate"
177+
comment = """
178+
Exercises `getEnv(name, { tracked: false })`. The runner still serves the env value, but the value does not enter the post-run fingerprint, so changing it later replays the cached output.
179+
"""
180+
ignore = true
181+
steps = [
182+
{ argv = [
183+
"vt",
184+
"run",
185+
"fetch-env-untracked",
186+
], envs = [
187+
[
188+
"PROBE_ENV",
189+
"first",
190+
],
191+
], comment = "first run serves PROBE_ENV without tracking it" },
192+
{ argv = [
193+
"vt",
194+
"run",
195+
"fetch-env-untracked",
196+
], envs = [
197+
[
198+
"PROBE_ENV",
199+
"second",
200+
],
201+
], comment = "cache hit: PROBE_ENV changed but was requested with tracked: false" },
202+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# fetch_env_untracked_does_not_invalidate
2+
3+
Exercises `getEnv(name, { tracked: false })`. The runner still serves the env value, but the value does not enter the post-run fingerprint, so changing it later replays the cached output.
4+
5+
## `PROBE_ENV=first vt run fetch-env-untracked`
6+
7+
first run serves PROBE_ENV without tracking it
8+
9+
```
10+
$ node scripts/fetch_env.mjs --untracked PROBE_ENV
11+
served PROBE_ENV=first
12+
process.env PROBE_ENV=(unset)
13+
```
14+
15+
## `PROBE_ENV=second vt run fetch-env-untracked`
16+
17+
cache hit: PROBE_ENV changed but was requested with tracked: false
18+
19+
```
20+
$ node scripts/fetch_env.mjs --untracked PROBE_ENV ◉ cache hit, replaying
21+
served PROBE_ENV=first
22+
process.env PROBE_ENV=(unset)
23+
24+
---
25+
vt run: cache hit.
26+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"input": [],
1919
"cache": true
2020
},
21+
"fetch-env-untracked": {
22+
"command": "node scripts/fetch_env.mjs --untracked PROBE_ENV",
23+
"cache": true
24+
},
2125
"fetch-prefixed-env": {
2226
"command": "PREFIXED_ENV=from-command node scripts/fetch_env.mjs PREFIXED_ENV",
2327
"cache": true

crates/vite_task_client/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ impl Client {
6363
/// # Errors
6464
///
6565
/// Returns an error if the request or response fails.
66-
pub fn get_env(&self, name: &OsStr) -> io::Result<Option<Arc<OsStr>>> {
66+
pub fn get_env(&self, name: &OsStr, tracked: bool) -> io::Result<Option<Arc<OsStr>>> {
6767
let name = Box::<NativeStr>::from(name);
6868

69-
self.send(&Request::GetEnv { name: &name })?;
69+
self.send(&Request::GetEnv { name: &name, tracked })?;
7070
let response: GetEnvResponse = self.recv()?;
7171
Ok(response
7272
.env_value

crates/vite_task_client_napi/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ impl RunnerClient {
9393
}
9494

9595
#[napi]
96-
pub fn get_env(&self, name: String, _options: Option<GetEnvOptions>) -> Result<Option<String>> {
96+
pub fn get_env(&self, name: String, options: Option<GetEnvOptions>) -> Result<Option<String>> {
97+
let tracked = options.and_then(|o| o.tracked).unwrap_or(true);
9798
let value = self
9899
.client
99-
.get_env(OsStr::new(&name))
100+
.get_env(OsStr::new(&name), tracked)
100101
.map_err(|err| err_string(vite_str::format!("{err}")))?;
101102
value.map_or(Ok(None), |value| {
102103
value.to_str().map(|s| Some(s.to_owned())).ok_or_else(|| {

crates/vite_task_ipc_shared/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub const NODE_CLIENT_PATH_ENV_NAME: &str = "VP_RUN_NODE_CLIENT_PATH";
2626
/// will still consume every buffered frame.
2727
#[derive(Debug, SchemaWrite, SchemaRead)]
2828
pub enum Request<'a> {
29-
GetEnv { name: &'a NativeStr },
29+
GetEnv { name: &'a NativeStr, tracked: bool },
3030
DisableCache,
3131
}
3232

crates/vite_task_server/src/lib.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use wincode::{SchemaWrite, config::DefaultConfig};
1414

1515
pub trait Handler {
1616
fn disable_cache(&mut self);
17-
fn get_env(&mut self, name: &OsStr) -> Option<Arc<OsStr>>;
17+
fn get_env(&mut self, name: &OsStr, tracked: bool) -> Option<Arc<OsStr>>;
1818
}
1919

2020
/// A protocol-level failure observed while servicing a client.
@@ -40,17 +40,27 @@ pub enum Error {
4040
/// recover the collected [`Reports`].
4141
pub struct Recorder {
4242
cache_disabled: bool,
43-
env_records: FxHashMap<Arc<OsStr>, Option<Arc<OsStr>>>,
43+
env_records: FxHashMap<Arc<OsStr>, EnvRecord>,
4444
/// The envs `get_env` resolves against. The runner supplies these for the
4545
/// spawned task; the server never re-reads the live process env.
4646
envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
4747
}
4848

49+
/// A record of an env value requested via `get_env`.
50+
///
51+
/// `tracked` is the monotonic OR of every `tracked` flag sent for this name:
52+
/// once true, it stays true.
53+
#[derive(Debug, Clone, PartialEq, Eq)]
54+
pub struct EnvRecord {
55+
pub tracked: bool,
56+
pub value: Option<Arc<OsStr>>,
57+
}
58+
4959
/// The data collected by a [`Recorder`] over the server's lifetime.
5060
#[derive(Debug, Default)]
5161
pub struct Reports {
5262
pub cache_disabled: bool,
53-
pub env_records: FxHashMap<Arc<OsStr>, Option<Arc<OsStr>>>,
63+
pub env_records: FxHashMap<Arc<OsStr>, EnvRecord>,
5464
}
5565

5666
impl Recorder {
@@ -70,12 +80,16 @@ impl Handler for Recorder {
7080
self.cache_disabled = true;
7181
}
7282

73-
fn get_env(&mut self, name: &OsStr) -> Option<Arc<OsStr>> {
83+
fn get_env(&mut self, name: &OsStr, tracked: bool) -> Option<Arc<OsStr>> {
7484
match self.env_records.entry(name.into()) {
75-
std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
85+
std::collections::hash_map::Entry::Occupied(mut entry) => {
86+
let record = entry.get_mut();
87+
record.tracked |= tracked;
88+
record.value.clone()
89+
}
7690
std::collections::hash_map::Entry::Vacant(entry) => {
7791
let value = self.envs.get(name).cloned();
78-
entry.insert(value.clone());
92+
entry.insert(EnvRecord { tracked, value: value.clone() });
7993
value
8094
}
8195
}
@@ -303,8 +317,8 @@ async fn handle_client<H: Handler>(mut stream: Stream, handler: &RefCell<H>) ->
303317
Request::DisableCache => {
304318
handler.borrow_mut().disable_cache();
305319
}
306-
Request::GetEnv { name } => {
307-
let value = handler.borrow_mut().get_env(name.to_cow_os_str().as_ref());
320+
Request::GetEnv { name, tracked } => {
321+
let value = handler.borrow_mut().get_env(name.to_cow_os_str().as_ref(), tracked);
308322
let response = GetEnvResponse { env_value: value.as_deref().map(Into::into) };
309323
write_response(&mut stream, &response).await.map_err(Error::WriteResponse)?;
310324
}

crates/vite_task_server/tests/integration.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fn connect(envs: &[(&'static OsStr, OsString)]) -> Client {
5353
/// read sequentially, so once the server answers a `get_env` everything
5454
/// before it must already have been dispatched to the handler.
5555
fn flush(client: &Client) {
56-
let _ = client.get_env(OsStr::new("__VP_TEST_FLUSH__")).unwrap();
56+
let _ = client.get_env(OsStr::new("__VP_TEST_FLUSH__"), false).unwrap();
5757
}
5858

5959
#[test]
@@ -72,19 +72,38 @@ fn single_client_fire_and_forget() {
7272
fn get_env_found_and_not_found() {
7373
let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |envs| {
7474
let client = connect(&envs);
75-
let present = client.get_env(OsStr::new("NODE_ENV")).unwrap();
75+
let present = client.get_env(OsStr::new("NODE_ENV"), true).unwrap();
7676
assert_eq!(present.as_deref(), Some(OsStr::new("production")));
77-
let missing = client.get_env(OsStr::new("MISSING")).unwrap();
77+
let missing = client.get_env(OsStr::new("MISSING"), false).unwrap();
7878
assert!(missing.is_none());
7979
})
8080
.expect("driver returned error");
8181

8282
assert!(!reports.cache_disabled);
8383
let node = reports.env_records.get(OsStr::new("NODE_ENV")).expect("NODE_ENV recorded");
84-
assert_eq!(node.as_deref(), Some(OsStr::new("production")));
84+
assert!(node.tracked);
85+
assert_eq!(node.value.as_deref(), Some(OsStr::new("production")));
8586

8687
let missing = reports.env_records.get(OsStr::new("MISSING")).expect("MISSING recorded");
87-
assert!(missing.is_none());
88+
assert!(!missing.tracked);
89+
assert!(missing.value.is_none());
90+
}
91+
92+
#[test]
93+
fn get_env_tracked_upgrade_is_monotonic() {
94+
let reports = run_with_server(env_map(&[("NODE_ENV", "production")]), |envs| {
95+
let client = connect(&envs);
96+
let a = client.get_env(OsStr::new("NODE_ENV"), false).unwrap();
97+
let b = client.get_env(OsStr::new("NODE_ENV"), true).unwrap();
98+
let c = client.get_env(OsStr::new("NODE_ENV"), false).unwrap();
99+
for v in [a, b, c] {
100+
assert_eq!(v.as_deref(), Some(OsStr::new("production")));
101+
}
102+
})
103+
.expect("driver returned error");
104+
105+
let node = reports.env_records.get(OsStr::new("NODE_ENV")).expect("recorded");
106+
assert!(node.tracked, "tracked must remain true once set");
88107
}
89108

90109
#[test]
@@ -95,7 +114,7 @@ fn concurrent_clients() {
95114
let envs = envs.clone();
96115
thread::spawn(move || {
97116
let client = connect(&envs);
98-
let value = client.get_env(OsStr::new("SHARED")).unwrap();
117+
let value = client.get_env(OsStr::new("SHARED"), true).unwrap();
99118
assert_eq!(value.as_deref(), Some(OsStr::new("value")));
100119
})
101120
})
@@ -108,5 +127,6 @@ fn concurrent_clients() {
108127

109128
assert!(!reports.cache_disabled);
110129
let shared = reports.env_records.get(OsStr::new("SHARED")).expect("recorded");
111-
assert_eq!(shared.as_deref(), Some(OsStr::new("value")));
130+
assert!(shared.tracked);
131+
assert_eq!(shared.value.as_deref(), Some(OsStr::new("value")));
112132
}

0 commit comments

Comments
 (0)