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