Skip to content

Commit 0ec8058

Browse files
stack72claude
andauthored
fix: use >= for bundle cache mtime comparison to handle equal timestamps (#1155)
## Summary - Changed bundle cache mtime comparison from strict `>` to `>=` in all 5 user extension loaders (models, reports, datastores, vaults, drivers) - When source and bundle files share the same mtime (e.g. written in the same second during a pre-commit hook), the cached bundle is now correctly used instead of triggering a rebundle that may fail - Added unit test verifying cached bundle is served when source and bundle have equal mtimes Closes systeminit/swamp-club#38 ## Test Plan - [x] New unit test: `bundleWithCache uses cache when source and bundle have equal mtimes` — sets source and bundle to identical mtime via `Deno.utime`, verifies cache is used without rebundling - [x] All 51 existing `user_model_loader_test.ts` tests pass - [x] Verified fix against manual reproduction in `/tmp/swamp-repro-issue-38` — with equal mtimes the compiled binary now logs `Using cached bundle` instead of rebundling - [x] `deno check`, `deno lint`, `deno fmt` all pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0283af0 commit 0ec8058

6 files changed

Lines changed: 71 additions & 5 deletions

File tree

src/domain/datastore/user_datastore_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export class UserDatastoreLoader {
251251
},
252252
null,
253253
);
254-
if (newestSourceMtime && bundleStat.mtime > newestSourceMtime) {
254+
if (newestSourceMtime && bundleStat.mtime >= newestSourceMtime) {
255255
logger.debug`Using cached datastore bundle for ${relativePath}`;
256256
return await Deno.readTextFile(bundlePath);
257257
}

src/domain/drivers/user_driver_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export class UserDriverLoader {
276276
},
277277
null,
278278
);
279-
if (newestSourceMtime && bundleStat.mtime > newestSourceMtime) {
279+
if (newestSourceMtime && bundleStat.mtime >= newestSourceMtime) {
280280
logger.debug`Using cached driver bundle for ${relativePath}`;
281281
return await Deno.readTextFile(bundlePath);
282282
}

src/domain/models/user_model_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1156,7 +1156,7 @@ export class UserModelLoader {
11561156
},
11571157
null,
11581158
);
1159-
if (newestSourceMtime && bundleStat.mtime > newestSourceMtime) {
1159+
if (newestSourceMtime && bundleStat.mtime >= newestSourceMtime) {
11601160
logger.debug`Using cached bundle for ${relativePath}`;
11611161
return await Deno.readTextFile(bundlePath);
11621162
}

src/domain/models/user_model_loader_test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,6 +2539,72 @@ export const model = {
25392539
}
25402540
});
25412541

2542+
Deno.test("UserModelLoader bundleWithCache uses cache when source and bundle have equal mtimes", async () => {
2543+
const ts = Date.now();
2544+
const modelCode = `
2545+
import { z } from "npm:zod@4";
2546+
2547+
export const model = {
2548+
type: "@user/equal-mtime-${ts}",
2549+
version: "2026.02.09.1",
2550+
methods: {
2551+
run: {
2552+
description: "Run",
2553+
arguments: z.object({}),
2554+
execute: async () => ({ dataHandles: [] }),
2555+
},
2556+
},
2557+
};
2558+
`;
2559+
2560+
const repoDir = await Deno.makeTempDir({
2561+
prefix: "swamp_equal_mtime_repo_",
2562+
});
2563+
const modelsDir = await Deno.makeTempDir({
2564+
prefix: "swamp_equal_mtime_models_",
2565+
});
2566+
2567+
try {
2568+
// Write model and load to produce cached bundle
2569+
await Deno.writeTextFile(join(modelsDir, "model.ts"), modelCode);
2570+
const loader1 = new UserModelLoader(testDenoRuntime, repoDir);
2571+
await loader1.loadModels(modelsDir);
2572+
2573+
const ns = bundleNamespace(modelsDir, repoDir);
2574+
const bundlePath = join(repoDir, ".swamp", "bundles", ns, "model.js");
2575+
const cachedBundle = await Deno.readTextFile(bundlePath);
2576+
2577+
// Set source and bundle to the exact same mtime
2578+
const sharedMtime = new Date("2026-01-01T00:00:00Z");
2579+
await Deno.utime(join(modelsDir, "model.ts"), sharedMtime, sharedMtime);
2580+
await Deno.utime(bundlePath, sharedMtime, sharedMtime);
2581+
2582+
// Load again — should use cached bundle, not rebundle
2583+
const loader2 = new UserModelLoader(testDenoRuntime, repoDir);
2584+
await loader2.loadModels(modelsDir);
2585+
2586+
const bundleAfter = await Deno.readTextFile(bundlePath);
2587+
2588+
// Bundle content should be identical (cache was used, not rebundled)
2589+
assertEquals(
2590+
cachedBundle,
2591+
bundleAfter,
2592+
"Bundle content should be unchanged when source and bundle have equal mtimes",
2593+
);
2594+
2595+
// Bundle mtime should still be the shared time (not updated by a rebundle)
2596+
const bundleStatAfter = await Deno.stat(bundlePath);
2597+
assertEquals(
2598+
bundleStatAfter.mtime?.getTime(),
2599+
sharedMtime.getTime(),
2600+
"Bundle mtime should be unchanged — cache was used, no rebundle occurred",
2601+
);
2602+
} finally {
2603+
await Deno.remove(repoDir, { recursive: true });
2604+
await Deno.remove(modelsDir, { recursive: true });
2605+
}
2606+
});
2607+
25422608
Deno.test("UserModelLoader: accepts optional DatastorePathResolver", () => {
25432609
// Verify the constructor accepts a resolver without errors
25442610
const mockResolver = {

src/domain/reports/user_report_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ export class UserReportLoader {
686686
},
687687
null,
688688
);
689-
if (newestSourceMtime && bundleStat.mtime > newestSourceMtime) {
689+
if (newestSourceMtime && bundleStat.mtime >= newestSourceMtime) {
690690
logger.debug`Using cached report bundle for ${relativePath}`;
691691
return await Deno.readTextFile(bundlePath);
692692
}

src/domain/vaults/user_vault_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export class UserVaultLoader {
277277
},
278278
null,
279279
);
280-
if (newestSourceMtime && bundleStat.mtime > newestSourceMtime) {
280+
if (newestSourceMtime && bundleStat.mtime >= newestSourceMtime) {
281281
logger.debug`Using cached vault bundle for ${relativePath}`;
282282
return await Deno.readTextFile(bundlePath);
283283
}

0 commit comments

Comments
 (0)