Skip to content

Commit 6485f05

Browse files
rsbhclaude
andcommitted
fix: include file mtime in cache key and suppress biome lint
Cache key now includes source file mtime — replacing an image invalidates stale cache entries. Added biome-ignore for useStorage in warmupImageCache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 97ee33d commit 6485f05

2 files changed

Lines changed: 21 additions & 7 deletions

File tree

packages/chronicle/src/server/api/image.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ describe('cacheKey', () => {
4848
expect(a).not.toBe(b);
4949
});
5050

51+
test('returns different keys for different mtime', () => {
52+
const a = cacheKey('/_content/img.png', 640, 75, 'webp', 1000);
53+
const b = cacheKey('/_content/img.png', 640, 75, 'webp', 2000);
54+
expect(a).not.toBe(b);
55+
});
56+
5157
test('key ends with format extension', () => {
5258
expect(cacheKey('/_content/img.png', 640, 75, 'webp')).toMatch(/\.webp$/);
5359
expect(cacheKey('/_content/img.png', 640, 75, 'avif')).toMatch(/\.avif$/);

packages/chronicle/src/server/api/image.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export const MIME: Record<string, string> = {
2828
'.webp': 'image/webp',
2929
}
3030

31-
export function cacheKey(url: string, w: number, q: number, format: OutputFormat): string {
32-
const hash = crypto.createHash('sha256').update(`${url}:${w}:${q}:${format}`).digest('hex').slice(0, 16)
31+
export function cacheKey(url: string, w: number, q: number, format: OutputFormat, mtime?: number): string {
32+
const hash = crypto.createHash('sha256').update(`${url}:${w}:${q}:${format}:${mtime ?? 0}`).digest('hex').slice(0, 16)
3333
return `${hash}.${format}`
3434
}
3535

@@ -95,7 +95,11 @@ export default defineHandler(async event => {
9595
const originalMime = MIME[ext] ?? 'application/octet-stream'
9696
const contentType = format === 'original' ? originalMime : `image/${format}`
9797

98-
const key = cacheKey(url, w, q, format)
98+
const stat = await fs.stat(filePath).catch(() => null)
99+
if (!stat) {
100+
throw new HTTPError({ status: StatusCodes.NOT_FOUND, message: 'Not Found' })
101+
}
102+
const key = cacheKey(url, w, q, format, stat.mtimeMs)
99103

100104
const cached = await storage.getItemRaw<Buffer>(key)
101105
if (cached) {
@@ -154,6 +158,7 @@ export default defineHandler(async event => {
154158

155159
export async function warmupImageCache() {
156160
const { getPages, getPageImages } = await import('@/lib/source');
161+
// biome-ignore lint/correctness/useHookAtTopLevel: useStorage is a Nitro DI accessor, not a React hook
157162
const storage = useStorage(STORAGE_KEY);
158163
const contentDir = __CHRONICLE_CONTENT_DIR__;
159164
const format = 'webp' as const;
@@ -169,14 +174,17 @@ export async function warmupImageCache() {
169174
if (!isLocalImage(url) || isSvg(url) || seen.has(url)) continue;
170175
seen.add(url);
171176

172-
const key = cacheKey(url, w, q, format);
173-
const cached = await storage.getItemRaw(key);
174-
if (cached) continue;
175-
176177
const relativePath = url.replace(/^\/_content\//, '');
177178
const filePath = safePath(contentDir, `/${relativePath}`);
178179
if (!filePath) continue;
179180

181+
const stat = await fs.stat(filePath).catch(() => null);
182+
if (!stat) continue;
183+
184+
const key = cacheKey(url, w, q, format, stat.mtimeMs);
185+
const cached = await storage.getItemRaw(key);
186+
if (cached) continue;
187+
180188
try {
181189
const optimized = await optimizeImage(filePath, w, q, format);
182190
await storage.setItemRaw(key, optimized);

0 commit comments

Comments
 (0)