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
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}