your personal website on atproto - mirror blento.app
at update-link-card 218 lines 6.0 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 = 1; 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] = await Promise.all([ 69 listRecords({ did, collection: 'app.blento.card', limit: 0 }).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 ]); 87 88 const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]); 89 const cardTypesArray = Array.from(cardTypes); 90 91 const additionDataPromises: Record<string, Promise<unknown>> = {}; 92 93 const loadOptions = { did, handle, cache }; 94 95 for (const cardType of cardTypesArray) { 96 const cardDef = CardDefinitionsByType[cardType]; 97 98 const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; 99 100 try { 101 if (cardDef?.loadDataServer) { 102 additionDataPromises[cardType] = cardDef.loadDataServer(items, { 103 ...loadOptions, 104 env 105 }); 106 } else if (cardDef?.loadData) { 107 additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); 108 } 109 } catch { 110 console.error('error getting additional data for', cardType); 111 } 112 } 113 114 await Promise.all(Object.values(additionDataPromises)); 115 116 const additionalData: Record<string, unknown> = {}; 117 for (const [key, value] of Object.entries(additionDataPromises)) { 118 try { 119 additionalData[key] = await value; 120 } catch (error) { 121 console.log('error loading', key, error); 122 } 123 } 124 125 const result = { 126 page: 'blento.' + page, 127 handle, 128 did, 129 cards: (cards.map((v) => { 130 return { ...v.value }; 131 }) ?? []) as Item[], 132 publications: [mainPublication, ...pages].filter((v) => v), 133 additionalData, 134 profile, 135 updatedAt: Date.now(), 136 version: CURRENT_CACHE_VERSION 137 }; 138 139 // Only cache results that have cards to avoid caching PDS errors 140 if (result.cards.length > 0) { 141 const stringifiedResult = JSON.stringify(result); 142 await cache?.putBlento(did, handle as string, stringifiedResult); 143 } 144 145 const parsedResult = structuredClone(result) as any; 146 147 parsedResult.publication = ( 148 parsedResult.publications as Awaited<ReturnType<typeof listRecords>> 149 ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value; 150 parsedResult.publication ??= { 151 name: profile?.displayName || profile?.handle, 152 description: profile?.description 153 }; 154 155 delete parsedResult['publications']; 156 157 return checkData(parsedResult); 158} 159 160function migrateFromV0ToV1(data: WebsiteData): WebsiteData { 161 for (const card of data.cards) { 162 if (card.version) continue; 163 card.x *= 2; 164 card.y *= 2; 165 card.h *= 2; 166 card.w *= 2; 167 card.mobileX *= 2; 168 card.mobileY *= 2; 169 card.mobileH *= 2; 170 card.mobileW *= 2; 171 card.version = 1; 172 } 173 174 return data; 175} 176 177function migrateFromV1ToV2(data: WebsiteData): WebsiteData { 178 for (const card of data.cards) { 179 if (!card.version || card.version < 2) { 180 card.page = 'blento.self'; 181 card.version = 2; 182 } 183 } 184 return data; 185} 186 187function migrateCards(data: WebsiteData): WebsiteData { 188 for (const card of data.cards) { 189 const cardDef = CardDefinitionsByType[card.cardType]; 190 191 if (!cardDef?.migrate) continue; 192 193 cardDef.migrate(card); 194 } 195 return data; 196} 197 198function checkData(data: WebsiteData): WebsiteData { 199 data = migrateData(data); 200 201 const cards = data.cards.filter((v) => v.page === data.page); 202 203 if (cards.length > 0) { 204 fixAllCollisions(cards, false); 205 fixAllCollisions(cards, true); 206 207 compactItems(cards, false); 208 compactItems(cards, true); 209 } 210 211 data.cards = cards; 212 213 return data; 214} 215 216function migrateData(data: WebsiteData): WebsiteData { 217 return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data))); 218}