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 fix/layout-stuff 149 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 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}