Skip to content

Commit 9b7317c

Browse files
authored
feat(client): add tracked option for env IPC (#458)
## Motivation Some tools need to ask the runner for env values without making those reads cache dependencies. The default should stay safe, but callers need an explicit opt-out for env reads that only affect diagnostics or optional behavior. ## Scope Add a `tracked` flag to both `getEnv` and `getEnvs` IPC requests and Rust/Node clients. The option defaults to `true`; `tracked: false` still serves the env value or match set but filters that read out of the post-run cache fingerprint. IPC records keep the tracked flag monotonic so a later tracked read of the same name or pattern wins over an earlier untracked read. This PR is intentionally placed after the getEnvs fingerprint PR so the opt-out is implemented against both env APIs at once. ## Verification - `cargo check -p vite_task` - `cargo test -p vite_task_server --test integration` - `cargo test -p vite_task_bin --test e2e_snapshots fetch_env_untracked_does_not_invalidate -- --ignored` - `cargo test -p vite_task_bin --test e2e_snapshots fetch_envs_untracked_does_not_invalidate -- --ignored` - `cargo clippy --locked --all-targets --all-features -- -D warnings`
1 parent dfdf0b3 commit 9b7317c

12 files changed

Lines changed: 245 additions & 65 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ fn collect_tracked_envs(
211211
let fingerprinted = &metadata.spawn_fingerprint.env_fingerprints().fingerprinted_envs;
212212
let mut tracked_envs = BTreeMap::new();
213213

214-
for (name, value) in &reports.env_records {
214+
for (name, value) in &reports.tracked_get_env {
215215
let name_str =
216216
name.to_str().ok_or_else(|| anyhow::anyhow!("tracked env name is not valid UTF-8"))?;
217217
if fingerprinted.contains_key(name_str) {
@@ -237,7 +237,7 @@ fn collect_tracked_envs(
237237
fn collect_tracked_env_globs(reports: &Reports) -> anyhow::Result<TrackedEnvGlobValues> {
238238
let mut tracked_env_globs = BTreeMap::new();
239239

240-
for (pattern, record) in &reports.env_glob_records {
240+
for (pattern, record) in &reports.tracked_get_envs {
241241
let mut matches = BTreeMap::new();
242242
for (name, value) in &record.matches {
243243
let name_str = name

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/scripts/fetch_envs.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getEnvs } from '@voidzero-dev/vite-task-client';
22

3-
const matches = getEnvs('PROBE_*');
3+
const tracked = process.argv[2] === '--untracked' ? false : true;
4+
const matches = getEnvs('PROBE_*', { tracked });
45
const sorted = Object.entries(matches).sort(([a], [b]) => a.localeCompare(b));
56

67
for (const [key, value] of sorted) {

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,73 @@ steps = [
299299
],
300300
], comment = "non-matching noise: UNRELATED does not match PROBE_* -> cache hit" },
301301
]
302+
303+
[[e2e]]
304+
name = "fetch_env_untracked_does_not_invalidate"
305+
comment = """
306+
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.
307+
"""
308+
ignore = true
309+
steps = [
310+
{ argv = [
311+
"vt",
312+
"run",
313+
"fetch-env-untracked",
314+
], envs = [
315+
[
316+
"PROBE_ENV",
317+
"first",
318+
],
319+
], comment = "first run serves PROBE_ENV without tracking it" },
320+
{ argv = [
321+
"vt",
322+
"run",
323+
"fetch-env-untracked",
324+
], envs = [
325+
[
326+
"PROBE_ENV",
327+
"second",
328+
],
329+
], comment = "cache hit: PROBE_ENV changed but was requested with tracked: false" },
330+
]
331+
332+
[[e2e]]
333+
name = "fetch_envs_untracked_does_not_invalidate"
334+
comment = """
335+
Exercises `getEnvs(pattern, { tracked: false })`. The runner still serves the matching envs, but the match set does not enter the post-run fingerprint, so changing it later replays the cached output.
336+
"""
337+
ignore = true
338+
steps = [
339+
{ argv = [
340+
"vt",
341+
"run",
342+
"fetch-envs-untracked",
343+
], envs = [
344+
[
345+
"PROBE_A",
346+
"a",
347+
],
348+
[
349+
"PROBE_B",
350+
"b",
351+
],
352+
], comment = "first run serves the PROBE_* match set without tracking it" },
353+
{ argv = [
354+
"vt",
355+
"run",
356+
"fetch-envs-untracked",
357+
], envs = [
358+
[
359+
"PROBE_A",
360+
"changed",
361+
],
362+
[
363+
"PROBE_B",
364+
"b",
365+
],
366+
[
367+
"PROBE_C",
368+
"c",
369+
],
370+
], comment = "cache hit: changed match set was requested with tracked: false" },
371+
]
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+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# fetch_envs_untracked_does_not_invalidate
2+
3+
Exercises `getEnvs(pattern, { tracked: false })`. The runner still serves the matching envs, but the match set does not enter the post-run fingerprint, so changing it later replays the cached output.
4+
5+
## `PROBE_A=a PROBE_B=b vt run fetch-envs-untracked`
6+
7+
first run serves the PROBE_* match set without tracking it
8+
9+
```
10+
$ node scripts/fetch_envs.mjs --untracked
11+
PROBE_A=a
12+
PROBE_B=b
13+
```
14+
15+
## `PROBE_A=changed PROBE_B=b PROBE_C=c vt run fetch-envs-untracked`
16+
17+
cache hit: changed match set was requested with tracked: false
18+
19+
```
20+
$ node scripts/fetch_envs.mjs --untracked ◉ cache hit, replaying
21+
PROBE_A=a
22+
PROBE_B=b
23+
24+
---
25+
vt run: cache hit.
26+
```

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424
"command": "node scripts/fetch_envs.mjs",
2525
"cache": true
2626
},
27+
"fetch-env-untracked": {
28+
"command": "node scripts/fetch_env.mjs --untracked PROBE_ENV",
29+
"cache": true
30+
},
31+
"fetch-envs-untracked": {
32+
"command": "node scripts/fetch_envs.mjs --untracked",
33+
"cache": true
34+
},
2735
"fetch-prefixed-env": {
2836
"command": "PREFIXED_ENV=from-command node scripts/fetch_env.mjs PREFIXED_ENV",
2937
"cache": true

crates/vite_task_client/src/lib.rs

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

70-
self.send(&Request::GetEnv { name: &name })?;
70+
self.send(&Request::GetEnv { name: &name, tracked })?;
7171
let response: GetEnvResponse = self.recv()?;
7272
Ok(response
7373
.env_value
@@ -81,8 +81,12 @@ impl Client {
8181
///
8282
/// Returns an error if the request or response fails, or if the server
8383
/// rejects the pattern as an invalid glob.
84-
pub fn get_envs(&self, pattern: &str) -> io::Result<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
85-
self.send(&Request::GetEnvs { pattern })?;
84+
pub fn get_envs(
85+
&self,
86+
pattern: &str,
87+
tracked: bool,
88+
) -> io::Result<FxHashMap<Arc<OsStr>, Arc<OsStr>>> {
89+
self.send(&Request::GetEnvs { pattern, tracked })?;
8690
let response: GetEnvsResponse = self.recv()?;
8791
Ok(response
8892
.entries

crates/vite_task_client_napi/src/lib.rs

Lines changed: 9 additions & 5 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(|| {
@@ -109,10 +110,13 @@ impl RunnerClient {
109110
pub fn get_envs(
110111
&self,
111112
pattern: String,
112-
_options: Option<GetEnvOptions>,
113+
options: Option<GetEnvOptions>,
113114
) -> Result<HashMap<String, String>> {
114-
let matches =
115-
self.client.get_envs(&pattern).map_err(|err| err_string(vite_str::format!("{err}")))?;
115+
let tracked = options.and_then(|o| o.tracked).unwrap_or(true);
116+
let matches = self
117+
.client
118+
.get_envs(&pattern, tracked)
119+
.map_err(|err| err_string(vite_str::format!("{err}")))?;
116120
let mut result = HashMap::with_capacity(matches.len());
117121
for (name, value) in matches {
118122
let name = name.to_str().ok_or_else(|| {

crates/vite_task_ipc_shared/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ pub const NODE_CLIENT_PATH_ENV_NAME: &str = "VP_RUN_NODE_CLIENT_PATH";
2727
/// will still consume every buffered frame.
2828
#[derive(Debug, SchemaWrite, SchemaRead)]
2929
pub enum Request<'a> {
30-
GetEnv { name: &'a NativeStr },
31-
GetEnvs { pattern: &'a str },
30+
GetEnv { name: &'a NativeStr, tracked: bool },
31+
GetEnvs { pattern: &'a str, tracked: bool },
3232
DisableCache,
3333
}
3434

0 commit comments

Comments
 (0)