your personal website on atproto - mirror blento.app
at fix-cached-posts 360 lines 9.1 kB view raw
1import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2import { CardDefinitionsByType } from '$lib/cards'; 3import type { CacheService } from '$lib/cache'; 4import { createEmptyCard } from '$lib/helper'; 5import type { Item, WebsiteData } from '$lib/types'; 6import { error } from '@sveltejs/kit'; 7import type { ActorIdentifier, Did } from '@atcute/lexicons'; 8 9import { isDid, isHandle } from '@atcute/lexicons/syntax'; 10import { fixAllCollisions, compactItems } from '$lib/layout'; 11 12const CURRENT_CACHE_VERSION = 1; 13 14export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) { 15 try { 16 const cachedResult = await cache?.getBlento(identifier); 17 18 if (!cachedResult) return; 19 const result = JSON.parse(cachedResult); 20 21 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 22 console.log('skipping cache because of version mismatch'); 23 return; 24 } 25 26 result.page = 'blento.' + page; 27 28 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( 29 (v) => parseUri(v.uri)?.rkey === result.page 30 )?.value; 31 result.publication ??= { 32 name: result.profile?.displayName || result.profile?.handle, 33 description: result.profile?.description 34 }; 35 36 delete result['publications']; 37 38 return checkData(result); 39 } catch (error) { 40 console.log('getting cached result failed', error); 41 } 42} 43 44export async function loadData( 45 handle: ActorIdentifier, 46 cache: CacheService | undefined, 47 forceUpdate: boolean = false, 48 page: string = 'self', 49 env?: Record<string, string | undefined> 50): Promise<WebsiteData> { 51 if (!handle) throw error(404); 52 if (handle === 'favicon.ico') throw error(404); 53 54 if (!forceUpdate) { 55 const cachedResult = await getCache(handle, page, cache); 56 57 if (cachedResult) return cachedResult; 58 } 59 60 let did: Did | undefined = undefined; 61 if (isHandle(handle)) { 62 did = await resolveHandle({ handle }); 63 } else if (isDid(handle)) { 64 did = handle; 65 } else { 66 throw error(404); 67 } 68 69 const [cards, mainPublication, pages, profile] = await Promise.all([ 70 listRecords({ did, collection: 'app.blento.card', limit: 0 }).catch((e) => { 71 console.error('error getting records for collection app.blento.card', e); 72 return [] as Awaited<ReturnType<typeof listRecords>>; 73 }), 74 getRecord({ 75 did, 76 collection: 'site.standard.publication', 77 rkey: 'blento.self' 78 }).catch(() => { 79 console.error('error getting record for collection site.standard.publication'); 80 return undefined; 81 }), 82 listRecords({ did, collection: 'app.blento.page' }).catch(() => { 83 console.error('error getting records for collection app.blento.page'); 84 return [] as Awaited<ReturnType<typeof listRecords>>; 85 }), 86 getDetailedProfile({ did }) 87 ]); 88 89 const additionalData = await loadAdditionalData( 90 cards.map((v) => ({ ...v.value })) as Item[], 91 { did, handle, cache }, 92 env 93 ); 94 95 const result = { 96 page: 'blento.' + page, 97 handle, 98 did, 99 cards: (cards.map((v) => { 100 return { ...v.value }; 101 }) ?? []) as Item[], 102 publications: [mainPublication, ...pages].filter((v) => v), 103 additionalData, 104 profile, 105 updatedAt: Date.now(), 106 version: CURRENT_CACHE_VERSION 107 }; 108 109 // Only cache results that have cards to avoid caching PDS errors 110 if (result.cards.length > 0) { 111 const stringifiedResult = JSON.stringify(result); 112 await cache?.putBlento(did, handle as string, stringifiedResult); 113 } 114 115 const parsedResult = structuredClone(result) as any; 116 117 parsedResult.publication = ( 118 parsedResult.publications as Awaited<ReturnType<typeof listRecords>> 119 ).find((v) => parseUri(v.uri)?.rkey === parsedResult.page)?.value; 120 parsedResult.publication ??= { 121 name: profile?.displayName || profile?.handle, 122 description: profile?.description 123 }; 124 125 delete parsedResult['publications']; 126 127 return checkData(parsedResult); 128} 129 130export async function loadCardData( 131 handle: ActorIdentifier, 132 rkey: string, 133 cache: CacheService | undefined, 134 env?: Record<string, string | undefined> 135): Promise<WebsiteData> { 136 if (!handle) throw error(404); 137 if (handle === 'favicon.ico') throw error(404); 138 139 let did: Did | undefined = undefined; 140 if (isHandle(handle)) { 141 did = await resolveHandle({ handle }); 142 } else if (isDid(handle)) { 143 did = handle; 144 } else { 145 throw error(404); 146 } 147 148 const [cardRecord, profile] = await Promise.all([ 149 getRecord({ 150 did, 151 collection: 'app.blento.card', 152 rkey 153 }).catch(() => undefined), 154 getDetailedProfile({ did }) 155 ]); 156 157 if (!cardRecord?.value) { 158 throw error(404, 'Card not found'); 159 } 160 161 const card = migrateCard(structuredClone(cardRecord.value) as Item); 162 const page = card.page ?? 'blento.self'; 163 164 const publication = await getRecord({ 165 did, 166 collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', 167 rkey: page 168 }).catch(() => undefined); 169 170 const cards = [card]; 171 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did); 172 173 const additionalData = await loadAdditionalData( 174 cards, 175 { did, handle: resolvedHandle, cache }, 176 env 177 ); 178 179 const result = { 180 page, 181 handle: resolvedHandle, 182 did, 183 cards, 184 publication: 185 publication?.value ?? 186 ({ 187 name: profile?.displayName || profile?.handle, 188 description: profile?.description 189 } as WebsiteData['publication']), 190 additionalData, 191 profile, 192 updatedAt: Date.now(), 193 version: CURRENT_CACHE_VERSION 194 }; 195 196 return result; 197} 198 199export async function loadCardTypeData( 200 handle: ActorIdentifier, 201 type: string, 202 cardData: Record<string, unknown>, 203 cache: CacheService | undefined, 204 env?: Record<string, string | undefined> 205): Promise<WebsiteData> { 206 if (!handle) throw error(404); 207 if (handle === 'favicon.ico') throw error(404); 208 209 const cardDef = CardDefinitionsByType[type]; 210 if (!cardDef) { 211 throw error(404, 'Card type not found'); 212 } 213 214 let did: Did | undefined = undefined; 215 if (isHandle(handle)) { 216 did = await resolveHandle({ handle }); 217 } else if (isDid(handle)) { 218 did = handle; 219 } else { 220 throw error(404); 221 } 222 223 const [publication, profile] = await Promise.all([ 224 getRecord({ 225 did, 226 collection: 'site.standard.publication', 227 rkey: 'blento.self' 228 }).catch(() => undefined), 229 getDetailedProfile({ did }) 230 ]); 231 232 const card = createEmptyCard('blento.self'); 233 card.cardType = type; 234 235 cardDef.createNew?.(card); 236 card.cardData = { 237 ...card.cardData, 238 ...cardData 239 }; 240 241 const cards = [card]; 242 const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did); 243 244 const additionalData = await loadAdditionalData( 245 cards, 246 { did, handle: resolvedHandle, cache }, 247 env 248 ); 249 250 const result = { 251 page: 'blento.self', 252 handle: resolvedHandle, 253 did, 254 cards, 255 publication: 256 publication?.value ?? 257 ({ 258 name: profile?.displayName || profile?.handle, 259 description: profile?.description 260 } as WebsiteData['publication']), 261 additionalData, 262 profile, 263 updatedAt: Date.now(), 264 version: CURRENT_CACHE_VERSION 265 }; 266 267 return checkData(result); 268} 269 270function migrateCard(card: Item): Item { 271 if (!card.version) { 272 card.x *= 2; 273 card.y *= 2; 274 card.h *= 2; 275 card.w *= 2; 276 card.mobileX *= 2; 277 card.mobileY *= 2; 278 card.mobileH *= 2; 279 card.mobileW *= 2; 280 card.version = 1; 281 } 282 283 if (!card.version || card.version < 2) { 284 card.page = 'blento.self'; 285 card.version = 2; 286 } 287 288 const cardDef = CardDefinitionsByType[card.cardType]; 289 cardDef?.migrate?.(card); 290 291 return card; 292} 293 294async function loadAdditionalData( 295 cards: Item[], 296 { did, handle, cache }: { did: Did; handle: string; cache?: CacheService }, 297 env?: Record<string, string | undefined> 298) { 299 const cardTypes = new Set(cards.map((v) => v.cardType ?? '') as string[]); 300 const cardTypesArray = Array.from(cardTypes); 301 const additionDataPromises: Record<string, Promise<unknown>> = {}; 302 303 for (const cardType of cardTypesArray) { 304 const cardDef = CardDefinitionsByType[cardType]; 305 const items = cards.filter((v) => cardType === v.cardType); 306 307 try { 308 if (cardDef?.loadDataServer) { 309 additionDataPromises[cardType] = cardDef.loadDataServer(items, { 310 did, 311 handle, 312 cache, 313 env 314 }); 315 } else if (cardDef?.loadData) { 316 additionDataPromises[cardType] = cardDef.loadData(items, { did, handle, cache }); 317 } 318 } catch { 319 console.error('error getting additional data for', cardType); 320 } 321 } 322 323 await Promise.all(Object.values(additionDataPromises)); 324 325 const additionalData: Record<string, unknown> = {}; 326 for (const [key, value] of Object.entries(additionDataPromises)) { 327 try { 328 additionalData[key] = await value; 329 } catch (error) { 330 console.log('error loading', key, error); 331 } 332 } 333 334 return additionalData; 335} 336 337function checkData(data: WebsiteData): WebsiteData { 338 data = migrateData(data); 339 340 const cards = data.cards.filter((v) => v.page === data.page); 341 342 if (cards.length > 0) { 343 fixAllCollisions(cards, false); 344 fixAllCollisions(cards, true); 345 346 compactItems(cards, false); 347 compactItems(cards, true); 348 } 349 350 data.cards = cards; 351 352 return data; 353} 354 355function migrateData(data: WebsiteData): WebsiteData { 356 for (const card of data.cards) { 357 migrateCard(card); 358 } 359 return data; 360}