Fix: Cache AI fallback images for 90 days like every other image

They were kept for 24h so a later search could replace them, but that
regenerated them daily for no real gain. Searches where the AI image also
fails are still remembered for 24h as bad.webp. With one TTL for all images
again, the per-image ttl stored in KV and R2 is no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 03:06:31 +00:00
co-authored by Claude Opus 5.5
parent 6fb64e9d7b
commit 865b9d951d
3 changed files with 15 additions and 17 deletions
+10 -12
View File
@@ -6,8 +6,6 @@ import { aiImage, parseAiSource, DEFAULT_AI_SOURCE } from "./_utils/ai.js";
import * as LIMITS from "../assets/rate-limit.js";
const TTL_SECONDS = 90 * 24 * 60 * 60;
// Failed searches, and the AI images served in their place, are kept for a day so a later search can still succeed
const MISS_TTL_SECONDS = 24 * 60 * 60;
const MAX_INDEX = 20;
// How long a search may hold its query's lock; longer than the 20s image deadline plus the search itself
const LOCK_SECONDS = 45;
@@ -60,7 +58,7 @@ export async function onRequest(context) {
const obj = await env.R2_IMAGES.get(r2Key);
if (obj) {
const nowSec = Math.floor(Date.now() / 1000);
const remainingSec = Math.max(0, (cached.t + (cached.ttl || TTL_SECONDS)) - nowSec);
const remainingSec = Math.max(0, (cached.t + TTL_SECONDS) - nowSec);
return new Response(obj.body, { headers: imageHeaders(cached.ct, remainingSec * 1000) });
}
}
@@ -162,7 +160,7 @@ COMMIT TRANSACTION;`;
if (!await db(`RETURN lock:\`${keyHash}\`.exp > time::now();`).then(r => r.json()).then(d => d[0].result).catch(() => true)) break;
}
const obj = await env.R2_IMAGES.get(r2Key);
return obj ? new Response(obj.body, { headers: imageHeaders(obj.httpMetadata.contentType, (+obj.customMetadata?.ttl || TTL_SECONDS) * 1000) }) : asset("bad.webp");
return obj ? new Response(obj.body, { headers: imageHeaders(obj.httpMetadata.contentType, TTL_SECONDS * 1000) }) : asset("bad.webp");
}
if (count > limit) {
@@ -175,20 +173,20 @@ COMMIT TRANSACTION;`;
// Called once the result (image or error) is stored, so waiters serve it right away instead of at lock expiry
const release = () => locked && context.waitUntil(db(`DELETE lock:\`${keyHash}\`;`).catch(() => {}));
// ttl is stored with the image (KV and R2) so cache hits and deduped waiters send a matching max-age
const store = async ({ buffer, contentType }, ttl = TTL_SECONDS) => {
await env.R2_IMAGES.put(r2Key, buffer, { httpMetadata: { contentType }, customMetadata: { ttl: `${ttl}` } });
await env.DIRECT_IMG_CACHE.put(cacheKey, JSON.stringify({ t: Math.floor(Date.now() / 1000), ct: contentType, ...(ttl !== TTL_SECONDS && { ttl }) }), { expirationTtl: ttl });
const store = async ({ buffer, contentType }) => {
await env.R2_IMAGES.put(r2Key, buffer, { httpMetadata: { contentType } });
await env.DIRECT_IMG_CACHE.put(cacheKey, JSON.stringify({ t: Math.floor(Date.now() / 1000), ct: contentType }), { expirationTtl: TTL_SECONDS });
release();
return new Response(buffer, { headers: imageHeaders(contentType, ttl * 1000) });
return new Response(buffer, { headers: imageHeaders(contentType, TTL_SECONDS * 1000) });
};
// Nothing found: serve an AI image of the query instead of bad.webp. bad.webp is only left for when that fails too.
// Nothing found: serve an AI image of the query instead, cached like any other image. bad.webp (remembered for 24h)
// is only left for when that fails too.
const fail = async (t, m, tag, p) => {
const fallback = await aiImage(DEFAULT_AI_SOURCE, query, i, env);
context.waitUntil(notify(env, { title: fallback.success ? `${t}, Served AI Image` : t, message: fallback.success ? m : `${m}\nAI fallback failed: ${fallback.reason}`, tags: tag, priority: p }));
if (fallback.success) return await store(fallback, MISS_TTL_SECONDS);
await env.DIRECT_IMG_CACHE.put(cacheKey, JSON.stringify({ t: Math.floor(Date.now() / 1000), err: true }), { expirationTtl: MISS_TTL_SECONDS });
if (fallback.success) return await store(fallback);
await env.DIRECT_IMG_CACHE.put(cacheKey, JSON.stringify({ t: Math.floor(Date.now() / 1000), err: true }), { expirationTtl: 86400 });
release();
return asset("bad.webp");
};