Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/flo-bit/atmo-watch. atmo.watch
Something went wrong. Try again.
5.0 kB · 177 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178import { cloudflareKV } from '@svelte-atproto/oauth/server/stores/cloudflare';import { withTimeout } from '$lib/upstream.server';
type Awaitable<T> = T | PromiseLike<T>;
type PublicDataCache = { get(key: string): Awaitable<unknown | undefined>; set(key: string, value: unknown): Awaitable<void>;};
type MemoryEntry = { expiresAt: number; value: unknown;};
type CloudflareCacheStorage = CacheStorage & { default?: Cache;};
const KV_CACHE_BINDING = 'MEDIA_CACHE';const CACHE_VERSION = 'v1';const CACHE_URL_BASE = 'https://atmo.watch/__data-cache/';const CACHE_EXPIRY_HEADER = 'X-Atmo-Cache-Expires-At';const MAX_MEMORY_ENTRIES = 500;const memoryCache = new Map<string, MemoryEntry>();const pendingLoads = new Map<string, Promise<unknown>>();
function versionedKey(key: string) { return `${CACHE_VERSION}:${key}`;}
function cacheRequest(key: string) { return new Request(`${CACHE_URL_BASE}${encodeURIComponent(key)}`);}
export function getCloudflareCache() { try { return (globalThis.caches as CloudflareCacheStorage | undefined)?.default; } catch { return undefined; }}
function readMemoryCache(key: string) { const entry = memoryCache.get(key); if (!entry) return undefined; if (entry.expiresAt <= Date.now()) { memoryCache.delete(key); return undefined; }
// Refresh insertion order so the map behaves like a small LRU cache. memoryCache.delete(key); memoryCache.set(key, entry); return entry.value;}
function writeMemoryCache(key: string, value: unknown, ttl: number) { memoryCache.delete(key); memoryCache.set(key, { expiresAt: Date.now() + ttl * 1000, value });
while (memoryCache.size > MAX_MEMORY_ENTRIES) { const oldestKey = memoryCache.keys().next().value; if (oldestKey === undefined) break; memoryCache.delete(oldestKey); }}
/** * General public data uses the Cloudflare Cache API. Entries are local to a * Cloudflare data center, with a small per-isolate LRU in front. */export function getPublicDataCache(ttl: number): PublicDataCache { return { async get(key) { const memoryValue = readMemoryCache(key); if (memoryValue !== undefined) return memoryValue;
const cloudflareCache = getCloudflareCache(); if (!cloudflareCache) return undefined;
const response = await cloudflareCache.match(cacheRequest(key)); if (!response) return undefined;
// Do not restart the TTL when an almost-expired edge entry warms an isolate. // Older entries without this header keep their existing behavior. const expiresAt = response.headers.get(CACHE_EXPIRY_HEADER); const remainingTtl = expiresAt === null ? ttl : Math.min(ttl, (Number(expiresAt) - Date.now()) / 1000); if (!Number.isFinite(remainingTtl) || remainingTtl <= 0) return undefined;
const value = (await response.json()) as unknown; writeMemoryCache(key, value, remainingTtl); return value; }, async set(key, value) { writeMemoryCache(key, value, ttl);
const cloudflareCache = getCloudflareCache(); if (!cloudflareCache) return;
await cloudflareCache.put( cacheRequest(key), new Response(JSON.stringify(value), { headers: { 'Cache-Control': `public, max-age=${ttl}`, 'Content-Type': 'application/json; charset=utf-8', [CACHE_EXPIRY_HEADER]: String(Date.now() + ttl * 1000) } }) ); } };}
/** Use KV only for the small, slow-changing OMDb ratings dataset. */export function getPersistentPublicDataCache(ttl: number): PublicDataCache | undefined { try { return cloudflareKV<string, unknown>(KV_CACHE_BINDING, { ttl })(); } catch { return undefined; }}
export async function readPublicDataCache<T>( cache: PublicDataCache | undefined, key: string): Promise<T | undefined> { if (!cache) return undefined;
try { return (await withTimeout('Public cache read', 750, () => cache.get(versionedKey(key)))) as T | undefined; } catch { // Treat cache errors as misses. return undefined; }}
export async function writePublicDataCache( cache: PublicDataCache | undefined, key: string, value: unknown): Promise<void> { if (!cache) return;
try { await withTimeout('Public cache write', 750, () => cache.set(versionedKey(key), value)); } catch { // A failed cache write should not fail the request. }}
export async function cachePublicData<T>( cache: PublicDataCache | undefined, key: string, load: () => Promise<T>, shouldCache: (value: T) => boolean = () => true): Promise<T> { const cached = await readPublicDataCache<T>(cache, key); if (cached !== undefined) return cached;
const pendingKey = versionedKey(key); const existing = pendingLoads.get(pendingKey); if (existing) return existing as Promise<T>;
const pending = load() .then(async (value) => { if (shouldCache(value)) await writePublicDataCache(cache, key, value); return value; }) .finally(() => { pendingLoads.delete(pendingKey); }); pendingLoads.set(pendingKey, pending); return pending;}