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