your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

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