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