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
38 delete result['publications'];
39
40 console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago');
41 return checkData(result);
42 } catch (error) {
43 console.log('getting cached result failed', error);
44 }
45}
46
47export async function loadData(
48 handle: Handle,
49 cache: UserCache | undefined,
50 forceUpdate: boolean = false,
51 page: string = 'self'
52): Promise<WebsiteData> {
53 if (!handle) throw error(404);
54 if (handle === 'favicon.ico') throw error(404);
55
56 if (!forceUpdate) {
57 const cachedResult = await getCache(handle, page, cache);
58
59 if (cachedResult) return cachedResult;
60 }
61
62 const did = await resolveHandle({ handle });
63
64 const cards = await listRecords({ did, collection: 'app.blento.card' }).catch(() => {
65 console.error('error getting records for collection app.blento.card');
66 return [] as Awaited<ReturnType<typeof listRecords>>;
67 });
68
69 const mainPublication = await getRecord({
70 did,
71 collection: 'site.standard.publication',
72 rkey: 'blento.self'
73 }).catch(() => {
74 console.error('error getting record for collection site.standard.publication');
75 return undefined;
76 });
77
78 const pages = await listRecords({ did, collection: 'app.blento.page' }).catch(() => {
79 console.error('error getting records for collection app.blento.page');
80 return [] as Awaited<ReturnType<typeof listRecords>>;
81 });
82
83 const profile = await getDetailedProfile({ did });
84
85 const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]);
86 const cardTypesArray = Array.from(cardTypes);
87
88 const additionDataPromises: Record<string, Promise<unknown>> = {};
89
90 const loadOptions = { did, handle, cache };
91
92 for (const cardType of cardTypesArray) {
93 const cardDef = CardDefinitionsByType[cardType];
94
95 if (!cardDef?.loadData) continue;
96
97 try {
98 additionDataPromises[cardType] = cardDef.loadData(
99 cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[],
100 loadOptions
101 );
102 } catch {
103 console.error('error getting additional data for', cardType);
104 }
105 }
106
107 await Promise.all(Object.values(additionDataPromises));
108
109 const additionalData: Record<string, unknown> = {};
110 for (const [key, value] of Object.entries(additionDataPromises)) {
111 try {
112 additionalData[key] = await value;
113 } catch (error) {
114 console.log('error loading', key, error);
115 }
116 }
117
118 const result = {
119 page: 'blento.' + page,
120 handle,
121 did,
122 cards: (cards.map((v) => {
123 return { ...v.value };
124 }) ?? []) as Item[],
125 publications: [mainPublication, ...pages].filter((v) => v),
126 additionalData,
127 profile,
128 updatedAt: Date.now(),
129 version: CURRENT_CACHE_VERSION
130 };
131
132 const stringifiedResult = JSON.stringify(result);
133 await cache?.put?.(handle, stringifiedResult);
134
135 const parsedResult = JSON.parse(stringifiedResult);
136
137 parsedResult.publication = (
138 parsedResult.publications as Awaited<ReturnType<typeof listRecords>>
139 ).find((v) => parseUri(v.uri).rkey === parsedResult.page)?.value;
140 parsedResult.publication ??= {};
141
142 delete parsedResult['publications'];
143
144 return checkData(parsedResult);
145}
146
147function migrateFromV0ToV1(data: WebsiteData): WebsiteData {
148 for (const card of data.cards) {
149 if (card.version) continue;
150 card.x *= 2;
151 card.y *= 2;
152 card.h *= 2;
153 card.w *= 2;
154 card.mobileX *= 2;
155 card.mobileY *= 2;
156 card.mobileH *= 2;
157 card.mobileW *= 2;
158 card.version = 1;
159 }
160
161 return data;
162}
163
164function migrateFromV1ToV2(data: WebsiteData): WebsiteData {
165 for (const card of data.cards) {
166 if (!card.version || card.version < 2) {
167 card.page = 'blento.self';
168 card.version = 2;
169 }
170 }
171 return data;
172}
173
174function migrateCards(data: WebsiteData): WebsiteData {
175 for (const card of data.cards) {
176 const cardDef = CardDefinitionsByType[card.cardType];
177
178 if (!cardDef?.migrate) continue;
179
180 cardDef.migrate(card);
181 }
182 return data;
183}
184
185function checkData(data: WebsiteData): WebsiteData {
186 data = migrateData(data);
187
188 const cards = data.cards.filter((v) => v.page === data.page);
189
190 if (cards.length > 0) {
191 fixAllCollisions(cards);
192 fixAllCollisions(cards, true);
193
194 compactItems(cards);
195 compactItems(cards, true);
196 }
197
198 data.cards = cards;
199
200 return data;
201}
202
203function migrateData(data: WebsiteData): WebsiteData {
204 return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data)));
205}