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