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