Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dev-runtime-nitro-prune-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Protect dev-runtime snapshots referenced by generated Nitro artifacts from stale snapshot pruning, so `eve dev` routes do not lose boot-time compiled-artifact imports after rebuild churn.
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,58 @@ describe("development runtime artifact snapshots", () => {
expect(existsSync(staleSnapshotRoot)).toBe(false);
});

it("preserves snapshots referenced by generated Nitro files", async () => {
const appRoot = await createScratchDirectory("eve-dev-runtime-artifacts-prune-nitro-");
const snapshotsRoot = join(appRoot, ".eve", "dev-runtime", "snapshots");
const activeSnapshotRoot = join(snapshotsRoot, "active");
const nitroReferencedSnapshotRoot = join(snapshotsRoot, "nitro-referenced");
const staleSnapshotRoot = join(snapshotsRoot, "stale");
const oldSnapshotTime = new Date(1_000);

for (const snapshotRoot of [
activeSnapshotRoot,
nitroReferencedSnapshotRoot,
staleSnapshotRoot,
]) {
await mkdir(snapshotRoot, { recursive: true });
await writeFile(join(snapshotRoot, "marker.txt"), snapshotRoot);
await utimes(snapshotRoot, oldSnapshotTime, oldSnapshotTime);
}

await activateDevelopmentRuntimeArtifactsSnapshot({
appRoot,
snapshot: {
runtimeAppRoot: join(activeSnapshotRoot, "source", "app"),
snapshotRoot: activeSnapshotRoot,
snapshotSourceRoot: join(activeSnapshotRoot, "source"),
},
});
await mkdir(join(appRoot, ".eve", "nitro", "workflow"), { recursive: true });
await writeFile(
join(appRoot, ".eve", "nitro", "workflow", "steps.mjs"),
`import "${join(
nitroReferencedSnapshotRoot,
"source",
"app",
".eve",
"compile",
"compiled-artifacts-bootstrap.mjs",
).replaceAll("\\", "/")}";\n`,
);

await pruneDevelopmentRuntimeArtifactsSnapshots({
appRoot,
now: 1_000_000,
recentWindowMs: 0,
retainCount: 0,
});

await expect(readdir(snapshotsRoot)).resolves.toEqual(
expect.arrayContaining(["active", "nitro-referenced"]),
);
expect(existsSync(staleSnapshotRoot)).toBe(false);
});

it("removes a partially staged snapshot when staging fails", async () => {
const appRoot = await createScratchDirectory("eve-dev-runtime-artifacts-failed-stage-");
const agentRoot = join(appRoot, "agent");
Expand Down
32 changes: 21 additions & 11 deletions packages/eve/src/internal/nitro/dev-runtime-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DEV_RUNTIME_ARTIFACTS_DIRECTORY = "dev-runtime";
const DEV_RUNTIME_ARTIFACTS_POINTER_VERSION = 2;
const DEV_RUNTIME_SNAPSHOT_RECENT_WINDOW_MS = 15 * 60 * 1000;
const DEV_RUNTIME_SNAPSHOT_RETAIN_COUNT = 5;
const DEV_RUNTIME_WORKFLOW_DATA_MAX_SCAN_BYTES = 1024 * 1024;
const DEV_RUNTIME_PROTECTED_REFERENCE_MAX_SCAN_BYTES = 1024 * 1024;
const TERMINAL_WORKFLOW_RUN_STATUSES = new Set(["completed", "failed", "cancelled", "canceled"]);

interface DevelopmentRuntimeArtifactsPointerV1 {
Expand Down Expand Up @@ -184,7 +184,10 @@ export async function pruneDevelopmentRuntimeArtifactsSnapshots(input: {
);
const protectedPaths = [
...collectProtectedSnapshotPaths(pointer),
...(await collectWorkflowDataSnapshotPaths({ appRoot: input.appRoot, snapshotsDirectory })),
...(await collectProtectedReferenceSnapshotPaths({
appRoot: input.appRoot,
snapshotsDirectory,
})),
];
const now = input.now ?? Date.now();
const recentWindowMs = input.recentWindowMs ?? DEV_RUNTIME_SNAPSHOT_RECENT_WINDOW_MS;
Expand Down Expand Up @@ -297,18 +300,25 @@ function collectProtectedSnapshotPaths(
return [pointer.runtimeAppRoot, pointer.snapshotRoot];
}

async function collectWorkflowDataSnapshotPaths(input: {
async function collectProtectedReferenceSnapshotPaths(input: {
readonly appRoot: string;
readonly snapshotsDirectory: string;
}): Promise<readonly string[]> {
const workflowDataDirectory = join(input.appRoot, ".workflow-data");
const referenceDirectories = [
join(input.appRoot, ".workflow-data"),
join(input.appRoot, ".eve", "nitro"),
];
const snapshotPaths = new Set<string>();

await collectSnapshotPathsFromDirectory({
directory: workflowDataDirectory,
snapshotPaths,
snapshotsDirectory: input.snapshotsDirectory,
});
await Promise.all(
referenceDirectories.map((directory) =>
collectSnapshotPathsFromDirectory({
directory,
snapshotPaths,
snapshotsDirectory: input.snapshotsDirectory,
}),
),
);

return [...snapshotPaths];
}
Expand Down Expand Up @@ -348,8 +358,8 @@ async function collectSnapshotPathsFromDirectory(input: {
}

const fileStats = await stat(path);
// Local workflow run payloads are small; keep this best-effort scan cheap.
if (fileStats.size > DEV_RUNTIME_WORKFLOW_DATA_MAX_SCAN_BYTES) {
// Protected-reference files are small; keep this best-effort scan cheap.
if (fileStats.size > DEV_RUNTIME_PROTECTED_REFERENCE_MAX_SCAN_BYTES) {
return;
}

Expand Down