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 = 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}