your personal website on atproto - mirror
blento.app
1import type { ActorIdentifier, Did } from '@atcute/lexicons';
2import { isDid } from '@atcute/lexicons/syntax';
3import type { KVNamespace } from '@cloudflare/workers-types';
4import { getBlentoOrBskyProfile } from '$lib/atproto/methods';
5
6/** TTL in seconds for each cache namespace */
7const NAMESPACE_TTL = {
8 blento: 60 * 60 * 24, // 24 hours
9 identity: 60 * 60 * 24 * 7, // 7 days
10 github: 60 * 60 * 12, // 12 hours
11 'gh-contrib': 60 * 60 * 12, // 12 hours
12 lastfm: 60 * 60, // 1 hour (default, overridable per-put)
13 npmx: 60 * 60 * 12, // 12 hours
14 profile: 60 * 60 * 24, // 24 hours
15 ical: 60 * 60 * 2, // 2 hours
16 events: 60 * 60, // 1 hour
17 rsvps: 60 * 60, // 1 hour
18 meta: 0 // no auto-expiry
19} as const;
20
21export type CacheNamespace = keyof typeof NAMESPACE_TTL;
22
23export class CacheService {
24 constructor(private kv: KVNamespace) {}
25
26 // === Generic namespaced operations ===
27
28 async get(namespace: CacheNamespace, key: string): Promise<string | null> {
29 return this.kv.get(`${namespace}:${key}`);
30 }
31
32 async put(
33 namespace: CacheNamespace,
34 key: string,
35 value: string,
36 ttlSeconds?: number
37 ): Promise<void> {
38 const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0;
39 await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined);
40 }
41
42 async delete(namespace: CacheNamespace, key: string): Promise<void> {
43 await this.kv.delete(`${namespace}:${key}`);
44 }
45
46 async list(namespace: CacheNamespace): Promise<string[]> {
47 const prefix = `${namespace}:`;
48 const result = await this.kv.list({ prefix });
49 return result.keys.map((k) => k.name.slice(prefix.length));
50 }
51
52 // === JSON convenience ===
53
54 async getJSON<T = unknown>(namespace: CacheNamespace, key: string): Promise<T | null> {
55 const raw = await this.get(namespace, key);
56 if (!raw) return null;
57 return JSON.parse(raw) as T;
58 }
59
60 async putJSON(
61 namespace: CacheNamespace,
62 key: string,
63 value: unknown,
64 ttlSeconds?: number
65 ): Promise<void> {
66 await this.put(namespace, key, JSON.stringify(value), ttlSeconds);
67 }
68
69 // === blento data (keyed by DID, with handle↔did resolution) ===
70 async getBlento(identifier: ActorIdentifier): Promise<string | null> {
71 const did = await this.resolveDid(identifier);
72 if (!did) return null;
73 return this.get('blento', did);
74 }
75
76 async putBlento(did: string, handle: string, data: string): Promise<void> {
77 await Promise.all([
78 this.put('blento', did, data),
79 this.put('identity', `h:${handle}`, did),
80 this.put('identity', `d:${did}`, handle)
81 ]);
82 }
83
84 async listBlentos(): Promise<string[]> {
85 return this.list('blento');
86 }
87
88 // === Identity resolution ===
89 async resolveDid(identifier: ActorIdentifier): Promise<string | null> {
90 if (isDid(identifier)) return identifier;
91 return this.get('identity', `h:${identifier}`);
92 }
93
94 async resolveHandle(did: Did): Promise<string | null> {
95 return this.get('identity', `d:${did}`);
96 }
97
98 // === Profile cache (did → profile data) ===
99 async getProfile(did: Did): Promise<CachedProfile> {
100 const cached = await this.getJSON<CachedProfile>('profile', did);
101 if (cached) return cached;
102
103 const profile = await getBlentoOrBskyProfile({ did });
104 const data: CachedProfile = {
105 did: profile.did as string,
106 handle: profile.handle as string,
107 displayName: profile.displayName as string | undefined,
108 avatar: profile.avatar as string | undefined,
109 hasBlento: profile.hasBlento,
110 url: profile.url
111 };
112
113 await this.putJSON('profile', did, data);
114 return data;
115 }
116}
117
118export type CachedProfile = {
119 did: string;
120 handle: string;
121 displayName?: string;
122 avatar?: string;
123 hasBlento: boolean;
124 url?: string;
125};
126
127export function createCache(platform?: App.Platform): CacheService | undefined {
128 const kv = platform?.env?.USER_DATA_CACHE;
129 if (!kv) return undefined;
130 return new CacheService(kv);
131}