Skip to content

Commit 70b9d16

Browse files
authored
Allow Evaluator.invalidate() to accept rest arguments (#492)
1 parent 9fb6296 commit 70b9d16

File tree

4 files changed

+97
-19
lines changed

4 files changed

+97
-19
lines changed

.changeset/curvy-terms-turn.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
---
3+

packages/next-yak/isolated-source-eval/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ const evaluator = await createEvaluator();
1212
const result = await evaluator.evaluate("/absolute/path/to/theme.ts");
1313

1414
if (result.ok) {
15-
result.value; // { default: ..., namedExport: ... }
15+
result.value; // { default: ..., namedExport: ... }
1616
result.dependencies; // ["/absolute/path/to/theme.ts", "/absolute/path/to/tokens.ts"]
1717
} else {
18-
result.error; // { message: string, stack: string }
18+
result.error; // { message: string, stack: string }
1919
}
2020
```
2121

@@ -127,15 +127,16 @@ type EvaluateResult =
127127

128128
Concurrent calls are queued and executed one at a time to keep dependency tracking accurate.
129129

130-
### `evaluator.invalidate(absolutePath)`
130+
### `evaluator.invalidate(...absolutePaths)`
131131

132-
Clears cached results for every entry point that transitively depends on the given file, then swaps workers to ensure a clean module cache. If an evaluation is in-flight, it's transparently retried on the fresh worker.
132+
Clears cached results for every entry point that transitively depends on the given file(s), then swaps workers to ensure a clean module cache. If an evaluation is in-flight, it's transparently retried on the fresh worker. Accepts one or more paths.
133133

134134
```ts
135135
evaluator.invalidate("/project/src/tokens.ts");
136+
evaluator.invalidate("/project/src/a.ts", "/project/src/b.ts");
136137
```
137138

138-
Silent no-op if the path isn't in any tracked dependency set.
139+
Silent no-op if no paths are provided or none are in any tracked dependency set.
139140

140141
### `evaluator.invalidateAll()`
141142

packages/next-yak/isolated-source-eval/__tests__/evaluator.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,68 @@ describe("invalidation", () => {
316316
const result2 = await evaluator.evaluate(fixture("simple-theme.ts"));
317317
expect(result1).not.toBe(result2); // Different reference
318318
});
319+
320+
it("invalidate with no args is a no-op", async () => {
321+
evaluator = await createEvaluator();
322+
const result1 = await evaluator.evaluate(fixture("simple-theme.ts"));
323+
324+
evaluator.invalidate();
325+
326+
const result2 = await evaluator.evaluate(fixture("simple-theme.ts"));
327+
expect(result1).toBe(result2); // Same reference — cache untouched
328+
});
329+
330+
it("invalidate with multiple args clears only affected entry points", async () => {
331+
evaluator = await createEvaluator();
332+
333+
// Evaluate two unrelated modules
334+
const themeResult = await evaluator.evaluate(
335+
fixture("transitive-theme.ts"),
336+
);
337+
const simpleResult = await evaluator.evaluate(fixture("simple-theme.ts"));
338+
339+
// Invalidate tokens.ts — only transitive-theme.ts depends on it
340+
evaluator.invalidate(fixture("tokens.ts"));
341+
342+
const themeResult2 = await evaluator.evaluate(
343+
fixture("transitive-theme.ts"),
344+
);
345+
const simpleResult2 = await evaluator.evaluate(fixture("simple-theme.ts"));
346+
347+
expect(themeResult2).not.toBe(themeResult); // Re-evaluated
348+
expect(simpleResult2).toBe(simpleResult); // Cache survived
349+
});
350+
351+
it("invalidate with multiple args handles shared dependents", async () => {
352+
evaluator = await createEvaluator();
353+
354+
// Both depend on tokens.ts
355+
await evaluator.evaluate(fixture("transitive-theme.ts"));
356+
await evaluator.evaluate(fixture("alt-theme.ts"));
357+
358+
expect(evaluator.getDependentsOf(fixture("tokens.ts"))).toHaveLength(2);
359+
360+
// Batch invalidate
361+
evaluator.invalidate(fixture("tokens.ts"));
362+
expect(evaluator.getDependentsOf(fixture("tokens.ts"))).toEqual([]);
363+
364+
// Both re-evaluate correctly
365+
const r1 = await evaluator.evaluate(fixture("transitive-theme.ts"));
366+
const r2 = await evaluator.evaluate(fixture("alt-theme.ts"));
367+
expect(r1.ok).toBe(true);
368+
expect(r2.ok).toBe(true);
369+
expect(evaluator.getDependentsOf(fixture("tokens.ts"))).toHaveLength(2);
370+
});
371+
372+
it("invalidate with untracked files is a no-op", async () => {
373+
evaluator = await createEvaluator();
374+
const result1 = await evaluator.evaluate(fixture("simple-theme.ts"));
375+
376+
evaluator.invalidate("/some/untracked/file.ts");
377+
378+
const result2 = await evaluator.evaluate(fixture("simple-theme.ts"));
379+
expect(result1).toBe(result2); // Cache untouched
380+
});
319381
});
320382

321383
describe("getDependentsOf", () => {

packages/next-yak/isolated-source-eval/evaluator.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export type EvaluateResult =
1111
export interface Evaluator {
1212
/** Evaluate a module and return its exports plus the full transitive dependency list. */
1313
evaluate(absolutePath: string): Promise<EvaluateResult>;
14-
/** Clear cached results for every entry point that transitively depends on the given file. */
15-
invalidate(absolutePath: string): void;
14+
/** Clear cached results for every entry point that transitively depends on the given file(s). */
15+
invalidate(...absolutePaths: string[]): void;
1616
/** Clear the entire result cache and swap workers. */
1717
invalidateAll(): void;
1818
/** Return entry point paths that would be affected by invalidate(absolutePath). */
@@ -406,23 +406,35 @@ export async function createEvaluator(): Promise<Evaluator> {
406406
return promise;
407407
}
408408

409-
function invalidate(absolutePath: string): void {
410-
// Always track the invalidation so that in-flight evaluations whose
411-
// dependency graph isn't populated yet (cold start) can detect staleness
412-
// when they complete in handleResult().
413-
invalidatedDuringEval.add(absolutePath);
409+
function invalidate(...absolutePaths: string[]): void {
410+
if (absolutePaths.length === 0) return;
414411

415-
const entryPoints = reverseDeps.get(absolutePath);
416-
if (!entryPoints || entryPoints.size === 0) return;
412+
// Track all invalidations for cold-start detection (in-flight evaluations
413+
// whose dependency graph isn't populated yet).
414+
for (const path of absolutePaths) {
415+
invalidatedDuringEval.add(path);
416+
}
417+
418+
// Collect all affected entry points across all changed files.
419+
const allEntryPoints = new Set<string>();
420+
for (const path of absolutePaths) {
421+
const entryPoints = reverseDeps.get(path);
422+
if (entryPoints) {
423+
for (const entry of entryPoints) {
424+
allEntryPoints.add(entry);
425+
}
426+
}
427+
}
428+
429+
// If no cached entry points are affected, return early. The cold-start
430+
// set (invalidatedDuringEval) will catch in-flight evaluations.
431+
if (allEntryPoints.size === 0) return;
417432

418-
for (const entry of entryPoints) {
433+
// Clear caches and clean dependency graphs for all affected entry points.
434+
for (const entry of allEntryPoints) {
419435
resultCache.delete(entry);
420436
inflight.delete(entry);
421437

422-
// Clean up the dependency graph for invalidated entry points.
423-
// Since we're about to swap workers and re-evaluate, the old graph
424-
// entries are stale. Cleaning them here prevents phantom reverse
425-
// entries from accumulating for removed dependencies.
426438
const deps = forwardDeps.get(entry);
427439
if (deps) {
428440
for (const dep of deps) {

0 commit comments

Comments
 (0)