your personal website on atproto - mirror
blento.app
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}