your personal website on atproto - mirror blento.app
at stickers-poc 224 lines 6.3 kB view raw
1import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2import { CardDefinitionsByType } from '$lib/cards'; 3import type { CacheService } from '$lib/cache'; 4import type { Item, WebsiteData } from '$lib/types'; 5import { error } from '@sveltejs/kit'; 6import type { ActorIdentifier, Did } from '@atcute/lexicons'; 7 8import { isDid, isHandle } from '@atcute/lexicons/syntax'; 9import { fixAllCollisions, compactItems } from '$lib/layout'; 10 11const CURRENT_CACHE_VERSION = 2; 12 13export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) { 14 try { 15 const cachedResult = await cache?.getBlento(identifier); 16 17 if (!cachedResult) return; 18 const result = JSON.parse(cachedResult); 19 20 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 21 console.log('skipping cache because of version mismatch'); 22 return; 23 } 24 25 result.page = 'blento.' + page; 26 27 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( 28 (v) => parseUri(v.uri)?.rkey === result.page 29 )?.value; 30 result.publication ??= { 31 name: result.profile?.displayName || result.profile?.handle, 32 description: result.profile?.description 33 }; 34 35 delete result['publications']; 36 37 return checkData(result); 38 } catch (error) { 39 console.log('getting cached result failed', error); 40 } 41} 42 43export async function loadData( 44 handle: ActorIdentifier, 45 cache: CacheService | undefined, 46 forceUpdate: boolean = false, 47 page: string = 'self', 48 env?: Record<string, string | undefined> 49): Promise<WebsiteData> { 50 if (!handle) throw error(404); 51 if (handle === 'favicon.ico') throw error(404); 52 53 if (!forceUpdate) { 54 const cachedResult = await getCache(handle, page, cache); 55 56 if (cachedResult) return cachedResult; 57 } 58 59 let did: Did | undefined = undefined; 60 if (isHandle(handle)) { 61 did = await resolveHandle({ handle }); 62 } else if (isDid(handle)) { 63 did = handle; 64 } else { 65 throw error(404); 66 } 67 68 const [cards, mainPublication, pages, profile, stickers] = await Promise.all([ 69 listRecords({ did, collection: 'app.blento.card' }).catch((e) => { 70 console.error('error getting records for collection app.blento.card', e); 71 return [] as Awaited<ReturnType<typeof listRecords>>; 72 }), 73 getRecord({ 74 did, 75 collection: 'site.standard.publication', 76 rkey: 'blento.self' 77 }).catch(() => { 78 console.error('error getting record for collection site.standard.publication'); 79 return undefined; 80 }), 81 listRecords({ did, collection: 'app.blento.page' }).catch(() => { 82 console.error('error getting records for collection app.blento.page'); 83 return [] as Awaited<ReturnType<typeof listRecords>>; 84 }), 85 getDetailedProfile({ did }), 86 listRecords({ did, collection: 'app.blento.sticker' }).catch(() => { 87 console.error('error getting records for collection app.blento.sticker'); 88 return [] as Awaited<ReturnType<typeof listRecords>>; 89 }) 90 ]); 91 92 const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]); 93 const cardTypesArray = Array.from(cardTypes); 94 95 const additionDataPromises: Record<string, Promise<unknown>> = {}; 96 97 const loadOptions = { did, handle, cache }; 98 99 for (const cardType of cardTypesArray) { 100 const cardDef = CardDefinitionsByType[cardType]; 101 102 const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; 103 104 try { 105 if (cardDef?.loadDataServer) { 106 additionDataPromises[cardType] = cardDef.loadDataServer(items, { 107 ...loadOptions, 108 env 109 }); 110 } else if (cardDef?.loadData) { 111 additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); 112 } 113 } catch { 114 console.error('error getting additional data for', cardType); 115 } 116 } 117 118 await Promise.all(Object.values(additionDataPromises)); 119 120 const additionalData: Record<string, unknown> = {}; 121 for (const [key, value] of Object.entries(additionDataPromises)) { 122 try { 123 additionalData[key] = await value; 124 } catch (error) { 125 console.log('error loading', key, error); 126 } 127 } 128 129 const result = { 130 page: 'blento.' + page, 131 handle, 132 did, 133 cards: (cards.map((v) => { 134 return { ...v.value }; 135 }) ?? []) as Item[], 136 stickers: stickers.map((v) => ({ ...v.value })), 137 publications: [mainPublication, ...pages].filter((v) => v), 138 additionalData, 139 profile, 140 updatedAt: Date.now(), 141 version: CURRENT_CACHE_VERSION 142 }; 143 144 // Only cache results that have cards to avoid caching PDS errors 145 if (result.cards.length > 0) { 146 const stringifiedResult = JSON.stringify(result); 147 await cache?.putBlento(did, handle as string, stringifiedResult); 148 } 149 150 const parsedResult = structuredClone(result) as any; 151 152 parsedResult.publication = ( 153 parsedResult.publications as Awaited<ReturnType<typeof listRecords>> 154 ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value; 155 parsedResult.publication ??= { 156 name: profile?.displayName || profile?.handle, 157 description: profile?.description 158 }; 159 160 delete parsedResult['publications']; 161 162 return checkData(parsedResult); 163} 164 165function migrateFromV0ToV1(data: WebsiteData): WebsiteData { 166 for (const card of data.cards) { 167 if (card.version) continue; 168 card.x *= 2; 169 card.y *= 2; 170 card.h *= 2; 171 card.w *= 2; 172 card.mobileX *= 2; 173 card.mobileY *= 2; 174 card.mobileH *= 2; 175 card.mobileW *= 2; 176 card.version = 1; 177 } 178 179 return data; 180} 181 182function migrateFromV1ToV2(data: WebsiteData): WebsiteData { 183 for (const card of data.cards) { 184 if (!card.version || card.version < 2) { 185 card.page = 'blento.self'; 186 card.version = 2; 187 } 188 } 189 return data; 190} 191 192function migrateCards(data: WebsiteData): WebsiteData { 193 for (const card of data.cards) { 194 const cardDef = CardDefinitionsByType[card.cardType]; 195 196 if (!cardDef?.migrate) continue; 197 198 cardDef.migrate(card); 199 } 200 return data; 201} 202 203function checkData(data: WebsiteData): WebsiteData { 204 data = migrateData(data); 205 206 const cards = data.cards.filter((v) => v.page === data.page); 207 208 if (cards.length > 0) { 209 fixAllCollisions(cards, false); 210 fixAllCollisions(cards, true); 211 212 compactItems(cards, false); 213 compactItems(cards, true); 214 } 215 216 data.cards = cards; 217 data.stickers = (data.stickers ?? []).filter((v) => v.page === data.page); 218 219 return data; 220} 221 222function migrateData(data: WebsiteData): WebsiteData { 223 return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data))); 224}