Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

oven: auto-invalidate core bundle cache on ac-source rsync

Three infra hardening changes so the bundler can't get out of date:

1. Auto-invalidate `coreBundleCache` when `lib/disk.mjs` mtime is newer
than the cache build time. sync-source.sh rsyncs ac-source without
restarting the Node process, so the cache otherwise serves
stale-bundler output until /bundle-prewarm is hit (or the process
restarts). Stat'ing one hot file as a sentinel is cheap and catches
every push.

2. Wire `nocache=1` through /pack-html → createBundle/createJSPieceBundle
→ getCoreBundle(forceRefresh). The route was reading the flag but
never propagating it, so &nocache=1 only busted the HTTP layer, not
the in-memory cache. Keep flow already passes &rebake=1&nocache=1, so
end users get a fresh bake on demand.

3. Restore the missing `?` exclusion in the parenthesized-import path
regex (regression introduced after f8c0d717c). Matches the other three
regexes in rewriteImports — `[^'"]+` was silently swallowing query
strings into the path capture for `("./foo.mjs?v=1")` shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+28 -10
+24 -6
oven/bundler.mjs
··· 57 57 58 58 let coreBundleCache = null; 59 59 let coreBundleCacheCommit = null; 60 + let coreBundleCacheBuiltAt = 0; 60 61 let skipMinification = false; 61 62 62 63 // ─── Brotli WASM decoder (loaded once at startup) ────────────────── ··· 450 451 } 451 452 ); 452 453 code = code.replace( 453 - /\(\s*['"](\.\.?\/[^'"]+\.m?js)(\?[^'"]+)?['"]\s*\)/g, 454 + /\(\s*['"](\.\.?\/[^'"?]+\.m?js)(\?[^'"]*)?['"]\s*\)/g, 454 455 (match, p) => { 455 456 const resolved = resolvePath(filepath, p); 456 457 return '("' + resolved + '")'; ··· 543 544 async function getCoreBundle(onProgress = () => {}, forceRefresh = false) { 544 545 const acDir = AC_SOURCE_DIR; 545 546 546 - if (!forceRefresh && coreBundleCache && coreBundleCacheCommit === GIT_COMMIT) { 547 + // Auto-invalidate when ac-source has been rsynced since the last cache build 548 + // (sync-source.sh runs without restarting this Node process, so the cache 549 + // would otherwise serve stale-bundler output until prewarm is hit). Stat one 550 + // hot file as a low-cost proxy for "the source tree changed." 551 + let stale = false; 552 + if (coreBundleCache && coreBundleCacheBuiltAt > 0) { 553 + try { 554 + const sentinel = path.join(acDir, "lib/disk.mjs"); 555 + const mtime = fsSync.statSync(sentinel).mtimeMs; 556 + if (mtime > coreBundleCacheBuiltAt) { 557 + stale = true; 558 + console.log(`[bundler] cache stale (disk.mjs mtime ${new Date(mtime).toISOString()} > built ${new Date(coreBundleCacheBuiltAt).toISOString()}) — rebuilding`); 559 + } 560 + } catch { /* ignore stat errors, fall through to normal cache check */ } 561 + } 562 + 563 + if (!forceRefresh && !stale && coreBundleCache && coreBundleCacheCommit === GIT_COMMIT) { 547 564 console.log(`[bundler] cache hit for ${GIT_COMMIT}`); 548 565 onProgress({ stage: "cache-hit", message: "Using cached core files..." }); 549 566 return coreBundleCache; ··· 672 689 673 690 coreBundleCache = coreFiles; 674 691 coreBundleCacheCommit = GIT_COMMIT; 692 + coreBundleCacheBuiltAt = Date.now(); 675 693 console.log(`[bundler] cached ${Object.keys(coreFiles).length} core files`); 676 694 return coreFiles; 677 695 } 678 696 679 697 // ─── KidLisp bundle ───────────────────────────────────────────────── 680 698 681 - export async function createBundle(pieceName, onProgress = () => {}, nocompress = false, density = null, brotli = false, noboxart = false, keeplabel = false) { 699 + export async function createBundle(pieceName, onProgress = () => {}, nocompress = false, density = null, brotli = false, noboxart = false, keeplabel = false, forceRefresh = false) { 682 700 const PIECE_NAME_NO_DOLLAR = pieceName.replace(/^\$/, ""); 683 701 const PIECE_NAME = "$" + PIECE_NAME_NO_DOLLAR; 684 702 ··· 697 715 }); 698 716 const bundleTimestamp = timestamp(); 699 717 700 - const coreFiles = await getCoreBundle(onProgress); 718 + const coreFiles = await getCoreBundle(onProgress, forceRefresh); 701 719 const files = { ...coreFiles }; 702 720 703 721 // Inject lightweight stubs for skipped files ··· 770 788 771 789 // ─── JS piece bundle ──────────────────────────────────────────────── 772 790 773 - export async function createJSPieceBundle(pieceName, onProgress = () => {}, nocompress = false, density = null, brotli = false, noboxart = false, keeplabel = false, forceDaw = false) { 791 + export async function createJSPieceBundle(pieceName, onProgress = () => {}, nocompress = false, density = null, brotli = false, noboxart = false, keeplabel = false, forceDaw = false, forceRefresh = false) { 774 792 const acDir = AC_SOURCE_DIR; 775 793 onProgress({ stage: "init", message: `Bundling ${pieceName}...` }); 776 794 ··· 781 799 }); 782 800 const bundleTimestamp = timestamp(); 783 801 784 - const coreFiles = await getCoreBundle(onProgress); 802 + const coreFiles = await getCoreBundle(onProgress, forceRefresh); 785 803 const files = { ...coreFiles }; 786 804 787 805 // Inject lightweight stubs for skipped files
+4 -4
oven/server.mjs
··· 2958 2958 try { 2959 2959 const onProgress = (p) => sendEvent('progress', p); 2960 2960 const { html, filename, sizeKB } = isJSPiece 2961 - ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel) 2962 - : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel); 2961 + ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, false, nocache) 2962 + : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, nocache); 2963 2963 sendEvent('complete', { filename, content: Buffer.from(html).toString('base64'), sizeKB }); 2964 2964 } catch (error) { 2965 2965 console.error('Bundle failed:', error); ··· 2973 2973 const progressLog = []; 2974 2974 const onProgress = (p) => { progressLog.push(p.message); console.log(`[bundler] ${p.stage}: ${p.message}`); }; 2975 2975 const result = isJSPiece 2976 - ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel) 2977 - : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel); 2976 + ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, false, nocache) 2977 + : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, nocache); 2978 2978 const { html, filename, sizeKB, mainSource, authorHandle, userCode, packDate, depCount } = result; 2979 2979 2980 2980 if (format === 'json' || format === 'base64') {