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