WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 77 lines 2.4 kB view raw
1export interface CachedPolicy { 2 defaultLightThemeUri: string | null; 3 defaultDarkThemeUri: string | null; 4 allowUserChoice: boolean; 5 availableThemes: Array<{ 6 uri: string; 7 /** 8 * Present for pinned refs (locked to an exact version). 9 * Absent for live refs (e.g. canonical atbb.space presets) — those 10 * resolve to the current record at the URI without CID verification. 11 */ 12 cid?: string; 13 }>; 14} 15 16export interface CachedTheme { 17 cid: string; 18 /** All token values are CSS strings at the cache boundary. */ 19 tokens: Record<string, string>; 20 cssOverrides: string | null; 21 fontUrls: string[] | null; 22} 23 24interface CacheEntry<T> { 25 data: T; 26 expiresAt: number; 27} 28 29/** 30 * In-memory TTL cache for resolved theme data on the web server. 31 * 32 * Themes change rarely. A single instance is created per server startup 33 * (inside createThemeMiddleware) and shared across all requests. 34 * 35 * Policy: single entry, keyed to the forum. 36 * Themes: map keyed by `${at-uri}:${colorScheme}` — colorScheme in the key 37 * ensures light and dark cached entries are never confused, even if the same 38 * AT-URI is used for both defaults. 39 */ 40export class ThemeCache { 41 private policy: CacheEntry<CachedPolicy> | null = null; 42 private themes = new Map<string, CacheEntry<CachedTheme>>(); 43 44 constructor(readonly ttlMs: number) {} 45 46 getPolicy(): Readonly<CachedPolicy> | null { 47 if (!this.policy || Date.now() > this.policy.expiresAt) { 48 this.policy = null; 49 return null; 50 } 51 return this.policy.data; 52 } 53 54 setPolicy(data: CachedPolicy): void { 55 this.policy = { data, expiresAt: Date.now() + this.ttlMs }; 56 } 57 58 getTheme(uri: string, colorScheme: "light" | "dark"): Readonly<CachedTheme> | null { 59 const key = `${uri}:${colorScheme}`; 60 const entry = this.themes.get(key); 61 if (!entry || Date.now() > entry.expiresAt) { 62 this.themes.delete(key); 63 return null; 64 } 65 return entry.data; 66 } 67 68 setTheme(uri: string, colorScheme: "light" | "dark", data: CachedTheme): void { 69 const key = `${uri}:${colorScheme}`; 70 this.themes.set(key, { data, expiresAt: Date.now() + this.ttlMs }); 71 } 72 73 /** Evict a stale theme entry so the next request fetches fresh data. */ 74 deleteTheme(uri: string, colorScheme: "light" | "dark"): void { 75 this.themes.delete(`${uri}:${colorScheme}`); 76 } 77}