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';
4
5/** TTL in seconds for each cache namespace */
6const NAMESPACE_TTL = {
7 blento: 60 * 60 * 24, // 24 hours
8 identity: 60 * 60 * 24 * 7, // 7 days
9 github: 60 * 60 * 12, // 12 hours
10 'gh-contrib': 60 * 60 * 12, // 12 hours
11 lastfm: 60 * 60, // 1 hour (default, overridable per-put)
12 npmx: 60 * 60 * 12, // 12 hours
13 meta: 0 // no auto-expiry
14} as const;
15
16export type CacheNamespace = keyof typeof NAMESPACE_TTL;
17
18export class CacheService {
19 constructor(private kv: KVNamespace) {}
20
21 // === Generic namespaced operations ===
22
23 async get(namespace: CacheNamespace, key: string): Promise<string | null> {
24 return this.kv.get(`${namespace}:${key}`);
25 }
26
27 async put(
28 namespace: CacheNamespace,
29 key: string,
30 value: string,
31 ttlSeconds?: number
32 ): Promise<void> {
33 const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0;
34 await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined);
35 }
36
37 async delete(namespace: CacheNamespace, key: string): Promise<void> {
38 await this.kv.delete(`${namespace}:${key}`);
39 }
40
41 async list(namespace: CacheNamespace): Promise<string[]> {
42 const prefix = `${namespace}:`;
43 const result = await this.kv.list({ prefix });
44 return result.keys.map((k) => k.name.slice(prefix.length));
45 }
46
47 // === JSON convenience ===
48
49 async getJSON<T = unknown>(namespace: CacheNamespace, key: string): Promise<T | null> {
50 const raw = await this.get(namespace, key);
51 if (!raw) return null;
52 return JSON.parse(raw) as T;
53 }
54
55 async putJSON(
56 namespace: CacheNamespace,
57 key: string,
58 value: unknown,
59 ttlSeconds?: number
60 ): Promise<void> {
61 await this.put(namespace, key, JSON.stringify(value), ttlSeconds);
62 }
63
64 // === blento data (keyed by DID, with handle↔did resolution) ===
65 async getBlento(identifier: ActorIdentifier): Promise<string | null> {
66 const did = await this.resolveDid(identifier);
67 if (!did) return null;
68 return this.get('blento', did);
69 }
70
71 async putBlento(did: string, handle: string, data: string): Promise<void> {
72 await Promise.all([
73 this.put('blento', did, data),
74 this.put('identity', `h:${handle}`, did),
75 this.put('identity', `d:${did}`, handle)
76 ]);
77 }
78
79 async listBlentos(): Promise<string[]> {
80 return this.list('blento');
81 }
82
83 // === Identity resolution ===
84 async resolveDid(identifier: ActorIdentifier): Promise<string | null> {
85 if (isDid(identifier)) return identifier;
86 return this.get('identity', `h:${identifier}`);
87 }
88
89 async resolveHandle(did: Did): Promise<string | null> {
90 return this.get('identity', `d:${did}`);
91 }
92}
93
94export function createCache(platform?: App.Platform): CacheService | undefined {
95 const kv = platform?.env?.USER_DATA_CACHE;
96 if (!kv) return undefined;
97 return new CacheService(kv);
98}