your personal website on atproto - mirror blento.app

refactor, cache stuff, rename handle to actor

+210 -292
+12 -1
CLAUDE.md
··· 66 - Data is stored in user's PDS under collection `app.blento.card` 67 - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. 68 69 **Data Loading (`src/lib/website/`):** 70 71 - - `load.ts` - Fetches user data from their PDS, with optional KV caching via `UserCache` 72 - `data.ts` - Defines which collections/records to fetch 73 - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree 74
··· 66 - Data is stored in user's PDS under collection `app.blento.card` 67 - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. 68 69 + **Caching (`src/lib/cache.ts`):** 70 + 71 + - `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys 72 + - Keys are stored as `namespace:key` (e.g. `user:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) 73 + - Per-namespace default TTLs via KV `expirationTtl`: `user` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) 74 + - User data is keyed by DID with bidirectional handle↔DID identity mappings (`identity:h:{handle}` → DID, `identity:d:{did}` → handle) 75 + - `getUser(identifier)` accepts either a handle or DID and resolves automatically 76 + - `putUser(did, handle, data)` writes data + both identity mappings 77 + - `createCache(platform)` factory function creates a `CacheService` from `platform.env` 78 + - `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain → DID resolution 79 + 80 **Data Loading (`src/lib/website/`):** 81 82 + - `load.ts` - Fetches user data from their PDS, with optional caching via `CacheService` 83 - `data.ts` - Defines which collections/records to fetch 84 - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree 85
+93
src/lib/cache.ts
···
··· 1 + import type { KVNamespace } from '@cloudflare/workers-types'; 2 + 3 + /** TTL in seconds for each cache namespace */ 4 + const NAMESPACE_TTL = { 5 + user: 60 * 60 * 24, // 24 hours 6 + identity: 60 * 60 * 24 * 7, // 7 days 7 + github: 60 * 60 * 12, // 12 hours 8 + 'gh-contrib': 60 * 60 * 12, // 12 hours 9 + lastfm: 60 * 60, // 1 hour (default, overridable per-put) 10 + npmx: 60 * 60 * 12, // 12 hours 11 + meta: 0 // no auto-expiry 12 + } as const; 13 + 14 + export type CacheNamespace = keyof typeof NAMESPACE_TTL; 15 + 16 + export class CacheService { 17 + constructor(private kv: KVNamespace) {} 18 + 19 + // === Generic namespaced operations === 20 + 21 + async get(namespace: CacheNamespace, key: string): Promise<string | null> { 22 + return this.kv.get(`${namespace}:${key}`); 23 + } 24 + 25 + async put(namespace: CacheNamespace, key: string, value: string, ttlSeconds?: number): Promise<void> { 26 + const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; 27 + await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); 28 + } 29 + 30 + async delete(namespace: CacheNamespace, key: string): Promise<void> { 31 + await this.kv.delete(`${namespace}:${key}`); 32 + } 33 + 34 + async list(namespace: CacheNamespace): Promise<string[]> { 35 + const prefix = `${namespace}:`; 36 + const result = await this.kv.list({ prefix }); 37 + return result.keys.map((k) => k.name.slice(prefix.length)); 38 + } 39 + 40 + // === JSON convenience === 41 + 42 + async getJSON<T = unknown>(namespace: CacheNamespace, key: string): Promise<T | null> { 43 + const raw = await this.get(namespace, key); 44 + if (!raw) return null; 45 + return JSON.parse(raw) as T; 46 + } 47 + 48 + async putJSON( 49 + namespace: CacheNamespace, 50 + key: string, 51 + value: unknown, 52 + ttlSeconds?: number 53 + ): Promise<void> { 54 + await this.put(namespace, key, JSON.stringify(value), ttlSeconds); 55 + } 56 + 57 + // === User data (keyed by DID, with handle↔did resolution) === 58 + 59 + async getUser(identifier: string): Promise<string | null> { 60 + const did = await this.resolveDid(identifier); 61 + if (!did) return null; 62 + return this.get('user', did); 63 + } 64 + 65 + async putUser(did: string, handle: string, data: string): Promise<void> { 66 + await Promise.all([ 67 + this.put('user', did, data), 68 + this.put('identity', `h:${handle}`, did), 69 + this.put('identity', `d:${did}`, handle) 70 + ]); 71 + } 72 + 73 + async listUsers(): Promise<string[]> { 74 + return this.list('user'); 75 + } 76 + 77 + // === Identity resolution === 78 + 79 + async resolveDid(identifier: string): Promise<string | null> { 80 + if (identifier.startsWith('did:')) return identifier; 81 + return this.get('identity', `h:${identifier}`); 82 + } 83 + 84 + async resolveHandle(did: string): Promise<string | null> { 85 + return this.get('identity', `d:${did}`); 86 + } 87 + } 88 + 89 + export function createCache(platform?: App.Platform): CacheService | undefined { 90 + const kv = platform?.env?.USER_DATA_CACHE; 91 + if (!kv) return undefined; 92 + return new CacheService(kv); 93 + }
+3 -4
src/lib/cards/special/UpdatedBlentos/index.ts
··· 15 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 ); 17 const recentRecords = await response.json(); 18 - const existingUsers = await cache?.get('updatedBlentos'); 19 const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 ? JSON.parse(existingUsers) 21 : []; ··· 50 (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 ); 52 53 - if (cache) { 54 - await cache?.put('updatedBlentos', JSON.stringify(result)); 55 - } 56 return JSON.parse(JSON.stringify(result.slice(0, 20))); 57 } catch (error) { 58 console.error('error fetching updated blentos', error);
··· 15 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 ); 17 const recentRecords = await response.json(); 18 + const existingUsers = await cache?.get('meta', 'updatedBlentos'); 19 const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 ? JSON.parse(existingUsers) 21 : []; ··· 50 (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 ); 52 53 + await cache?.put('meta', 'updatedBlentos', JSON.stringify(result)); 54 + 55 return JSON.parse(JSON.stringify(result.slice(0, 20))); 56 } catch (error) { 57 console.error('error fetching updated blentos', error);
+3 -2
src/lib/cards/types.ts
··· 1 import type { Component } from 'svelte'; 2 - import type { Item, UserCache } from '$lib/types'; 3 import type { Did } from '@atcute/lexicons'; 4 5 export type CreationModalComponentProps = { ··· 36 loadData?: ( 37 // all cards of that type 38 items: Item[], 39 - { did, handle, cache }: { did: Did; handle: string; cache?: UserCache } 40 ) => Promise<unknown>; 41 42 // show color selection popup
··· 1 import type { Component } from 'svelte'; 2 + import type { Item } from '$lib/types'; 3 + import type { CacheService } from '$lib/cache'; 4 import type { Did } from '@atcute/lexicons'; 5 6 export type CreationModalComponentProps = { ··· 37 loadData?: ( 38 // all cards of that type 39 items: Item[], 40 + { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 ) => Promise<unknown>; 42 43 // show color selection popup
-5
src/lib/types.ts
··· 66 updatedAt: number; 67 version?: number; 68 }; 69 - 70 - export type UserCache = { 71 - get: (key: string) => string; 72 - put: (key: string, value: string) => void; 73 - };
··· 66 updatedAt: number; 67 version?: number; 68 };
+12 -18
src/lib/website/load.ts
··· 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 import { CardDefinitionsByType } from '$lib/cards'; 3 - import type { Item, UserCache, WebsiteData } from '$lib/types'; 4 import { error } from '@sveltejs/kit'; 5 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 6 ··· 9 10 const CURRENT_CACHE_VERSION = 1; 11 12 - export 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( ··· 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); ··· 51 52 export async function loadData( 53 handle: ActorIdentifier, 54 - cache: UserCache | undefined, 55 forceUpdate: boolean = false, 56 page: string = 'self' 57 ): Promise<WebsiteData> { ··· 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({ ··· 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
··· 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 import { CardDefinitionsByType } from '$lib/cards'; 3 + import type { CacheService } from '$lib/cache'; 4 + import type { Item, WebsiteData } from '$lib/types'; 5 import { error } from '@sveltejs/kit'; 6 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 7 ··· 10 11 const CURRENT_CACHE_VERSION = 1; 12 13 + export async function getCache(identifier: string, page: string, cache?: CacheService) { 14 try { 15 + const cachedResult = await cache?.getUser(identifier); 16 17 if (!cachedResult) return; 18 const result = JSON.parse(cachedResult); 19 20 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 21 console.log('skipping cache because of version mismatch'); 22 return; 23 } 24 25 result.page = 'blento.' + page; 26 27 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( ··· 34 35 delete result['publications']; 36 37 return checkData(result); 38 } catch (error) { 39 console.log('getting cached result failed', error); ··· 42 43 export async function loadData( 44 handle: ActorIdentifier, 45 + cache: CacheService | undefined, 46 forceUpdate: boolean = false, 47 page: string = 'self' 48 ): Promise<WebsiteData> { ··· 65 } 66 67 const [cards, mainPublication, pages, profile] = await Promise.all([ 68 + listRecords({ did, collection: 'app.blento.card' }).catch((e) => { 69 + console.error('error getting records for collection app.blento.card', e); 70 return [] as Awaited<ReturnType<typeof listRecords>>; 71 }), 72 getRecord({ ··· 131 version: CURRENT_CACHE_VERSION 132 }; 133 134 + // Only cache results that have cards to avoid caching PDS errors 135 + if (result.cards.length > 0) { 136 + const stringifiedResult = JSON.stringify(result); 137 + await cache?.putUser(did, handle as string, stringifiedResult); 138 + } 139 140 const parsedResult = structuredClone(result) as any; 141
src/params/handle.ts src/params/actor.ts
+4 -4
src/routes/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { createCache } from '$lib/cache'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 + const cache = createCache(platform); 12 const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 + if (did) return await loadData(did as ActorIdentifier, cache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 + return await loadData(handle as ActorIdentifier, cache); 25 }
+13
src/routes/[actor=actor]/(pages)/+layout.server.ts
···
··· 1 + import { loadData } from '$lib/website/load'; 2 + import { env } from '$env/dynamic/private'; 3 + import { error } from '@sveltejs/kit'; 4 + import { createCache } from '$lib/cache'; 5 + import type { Handle } from '@atcute/lexicons'; 6 + 7 + export async function load({ params, platform }) { 8 + if (env.PUBLIC_IS_SELFHOSTED) error(404); 9 + 10 + const cache = createCache(platform); 11 + 12 + return await loadData(params.actor, cache, false, params.page); 13 + }
+13
src/routes/[actor=actor]/api/refresh/+server.ts
···
··· 1 + import { createCache } from '$lib/cache'; 2 + import { loadData } from '$lib/website/load.js'; 3 + import type { Handle } from '@atcute/lexicons'; 4 + import { json } from '@sveltejs/kit'; 5 + 6 + export async function GET({ params, platform }) { 7 + const cache = createCache(platform); 8 + if (!cache) return json('no cache'); 9 + 10 + await loadData(params.actor, cache, true); 11 + 12 + return json('ok'); 13 + }
-13
src/routes/[handle=handle]/(pages)/+layout.server.ts
··· 1 - import { loadData } from '$lib/website/load'; 2 - import { env } from '$env/dynamic/private'; 3 - import { error } from '@sveltejs/kit'; 4 - import type { UserCache } from '$lib/types'; 5 - import type { Handle } from '@atcute/lexicons'; 6 - 7 - export async function load({ params, platform }) { 8 - if (env.PUBLIC_IS_SELFHOSTED) error(404); 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - 12 - return await loadData(params.handle as Handle, cache as UserCache, false, params.page); 13 - }
···
src/routes/[handle=handle]/(pages)/+page.svelte src/routes/[actor=actor]/(pages)/+page.svelte
src/routes/[handle=handle]/(pages)/edit/+page.svelte src/routes/[actor=actor]/(pages)/edit/+page.svelte
src/routes/[handle=handle]/(pages)/p/[[page]]/+page.svelte src/routes/[actor=actor]/(pages)/p/[[page]]/+page.svelte
src/routes/[handle=handle]/(pages)/p/[[page]]/copy/+page.svelte src/routes/[actor=actor]/(pages)/p/[[page]]/copy/+page.svelte
src/routes/[handle=handle]/(pages)/p/[[page]]/edit/+page.svelte src/routes/[actor=actor]/(pages)/p/[[page]]/edit/+page.svelte
+4 -3
src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { error } from '@sveltejs/kit'; 3 - import type { UserCache } from '$lib/types'; 4 import { text } from '@sveltejs/kit'; 5 6 export async function GET({ params, platform }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 9 - const data = await loadData(params.handle, cache as UserCache, false, params.page); 10 11 if (!data.publication) throw error(300); 12
··· 1 import { loadData } from '$lib/website/load'; 2 + import { createCache } from '$lib/cache'; 3 + 4 import { error } from '@sveltejs/kit'; 5 import { text } from '@sveltejs/kit'; 6 7 export async function GET({ params, platform }) { 8 + const cache = createCache(platform); 9 10 + const data = await loadData(params.actor, cache, false, params.page); 11 12 if (!data.publication) throw error(300); 13
-14
src/routes/[handle=handle]/api/refresh/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 2 - import { loadData } from '$lib/website/load.js'; 3 - import type { Handle } from '@atcute/lexicons'; 4 - import { json } from '@sveltejs/kit'; 5 - 6 - export async function GET({ params, platform }) { 7 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 - const handle = params.handle; 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - await loadData(handle as Handle, cache as UserCache, true); 12 - 13 - return json('ok'); 14 - }
···
+3 -4
src/routes/[handle=handle]/og.png/+server.ts src/routes/[actor=actor]/og.png/+server.ts
··· 1 import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 - import type { UserCache } from '$lib/types'; 3 import { loadData } from '$lib/website/load'; 4 import type { Handle } from '@atcute/lexicons'; 5 - import { isDid } from '@atcute/lexicons/syntax'; 6 import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 8 function escapeHtml(str: string): string { ··· 15 } 16 17 export async function GET({ params, platform }) { 18 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 19 20 - const data = await loadData(params.handle as Handle, cache as UserCache); 21 22 let image: string | undefined = data.profile.avatar; 23
··· 1 import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 + import { createCache } from '$lib/cache'; 3 import { loadData } from '$lib/website/load'; 4 import type { Handle } from '@atcute/lexicons'; 5 import { ImageResponse } from '@ethercorps/sveltekit-og'; 6 7 function escapeHtml(str: string): string { ··· 14 } 15 16 export async function GET({ params, platform }) { 17 + const cache = createCache(platform); 18 19 + const data = await loadData(params.actor, cache); 20 21 let image: string | undefined = data.profile.avatar; 22
-35
src/routes/all/+page.server.ts
··· 1 - import { env } from '$env/dynamic/public'; 2 - import type { UserCache, WebsiteData } from '$lib/types.js'; 3 - import { loadData } from '$lib/website/load'; 4 - import type { Handle } from '@atcute/lexicons'; 5 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 - 7 - export async function load({ platform }) { 8 - const cache = platform?.env?.USER_DATA_CACHE; 9 - 10 - const list = await cache?.list(); 11 - 12 - const profiles: AppBskyActorDefs.ProfileViewDetailed[] = []; 13 - for (const value of list?.keys ?? []) { 14 - // check if at least one card 15 - const result = await cache?.get(value.name); 16 - if (!result) continue; 17 - const parsed = JSON.parse(result) as WebsiteData; 18 - 19 - if (parsed.version !== 1 || !parsed.cards?.length) continue; 20 - 21 - profiles.push(parsed.profile); 22 - } 23 - 24 - profiles.sort((a, b) => a.handle.localeCompare(b.handle)); 25 - 26 - const handle = env.PUBLIC_HANDLE; 27 - 28 - const data = await loadData(handle as Handle, cache as unknown as UserCache); 29 - 30 - data.publication ??= {}; 31 - data.publication.preferences ??= {}; 32 - data.publication.preferences.hideProfileSection = true; 33 - 34 - return { ...data, profiles }; 35 - }
···
-29
src/routes/all/+page.svelte
··· 1 - <script lang="ts"> 2 - import { createEmptyCard } from '$lib/helper.js'; 3 - import Website from '$lib/website/Website.svelte'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <Website 9 - data={{ 10 - ...data, 11 - cards: data.profiles.map((v, i) => { 12 - const card = createEmptyCard(''); 13 - card.cardType = 'blueskyProfile'; 14 - card.cardData = { 15 - avatar: v.avatar, 16 - handle: v.handle, 17 - displayName: v.displayName 18 - }; 19 - 20 - card.x = (i % 4) * 2; 21 - card.y = Math.floor(i / 4) * 2; 22 - 23 - card.mobileX = (i % 2) * 4; 24 - card.mobileY = Math.floor(i / 2) * 4; 25 - 26 - return card; 27 - }) 28 - }} 29 - />
···
+5 -11
src/routes/api/github/+server.ts
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; 4 5 const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 6 ··· 11 return json({ error: 'No user provided' }, { status: 400 }); 12 } 13 14 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get('#github:' + user); 15 16 if (cachedData) { 17 - const parsedCache = JSON.parse(cachedData); 18 - 19 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 20 - const now = Date.now(); 21 - 22 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 23 - return json(parsedCache); 24 - } 25 } 26 27 try { ··· 42 } 43 44 const result = data.user as GitHubContributionsData; 45 - result.updatedAt = Date.now(); 46 47 - await platform?.env?.USER_DATA_CACHE?.put('#github:' + user, JSON.stringify(result)); 48 49 return json(result); 50 } catch (error) {
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; 4 + import { createCache } from '$lib/cache'; 5 6 const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 7 ··· 12 return json({ error: 'No user provided' }, { status: 400 }); 13 } 14 15 + const cache = createCache(platform); 16 + const cachedData = await cache?.get('github', user); 17 18 if (cachedData) { 19 + return json(JSON.parse(cachedData)); 20 } 21 22 try { ··· 37 } 38 39 const result = data.user as GitHubContributionsData; 40 41 + await cache?.put('github', user, JSON.stringify(result)); 42 43 return json(result); 44 } catch (error) {
+6 -14
src/routes/api/github/contributors/+server.ts
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 4 const GithubContributorsAPIURL = 5 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; ··· 12 return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); 13 } 14 15 - const cacheKey = `#github-contributors:${owner}/${repo}`; 16 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 17 18 if (cachedData) { 19 - const parsedCache = JSON.parse(cachedData); 20 - 21 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 22 - const now = Date.now(); 23 - 24 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 25 - return json(parsedCache.data); 26 - } 27 } 28 29 try { ··· 40 41 const data = await response.json(); 42 43 - await platform?.env?.USER_DATA_CACHE?.put( 44 - cacheKey, 45 - JSON.stringify({ data, updatedAt: Date.now() }) 46 - ); 47 48 return json(data); 49 } catch (error) {
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 + import { createCache } from '$lib/cache'; 4 5 const GithubContributorsAPIURL = 6 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; ··· 13 return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); 14 } 15 16 + const cache = createCache(platform); 17 + const cacheKey = `${owner}/${repo}`; 18 + const cachedData = await cache?.get('gh-contrib', cacheKey); 19 20 if (cachedData) { 21 + return json(JSON.parse(cachedData)); 22 } 23 24 try { ··· 35 36 const data = await response.json(); 37 38 + await cache?.put('gh-contrib', cacheKey, JSON.stringify(data)); 39 40 return json(data); 41 } catch (error) {
+11 -15
src/routes/api/lastfm/+server.ts
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 import { env } from '$env/dynamic/private'; 4 5 const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 6 ··· 12 ]; 13 14 const CACHE_TTL: Record<string, number> = { 15 - 'user.getRecentTracks': 15 * 60 * 1000, 16 - 'user.getTopTracks': 60 * 60 * 1000, 17 - 'user.getTopAlbums': 60 * 60 * 1000, 18 - 'user.getInfo': 12 * 60 * 60 * 1000 19 }; 20 21 export const GET: RequestHandler = async ({ url, platform }) => { ··· 32 return json({ error: 'Method not allowed' }, { status: 400 }); 33 } 34 35 - const cacheKey = `#lastfm:${method}:${user}:${period}:${limit}`; 36 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 37 38 if (cachedData) { 39 - const parsed = JSON.parse(cachedData); 40 - const ttl = CACHE_TTL[method] || 60 * 60 * 1000; 41 - 42 - if (Date.now() - (parsed._cachedAt || 0) < ttl) { 43 - return json(parsed); 44 - } 45 } 46 47 const apiKey = env?.LASTFM_API_KEY; ··· 77 return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); 78 } 79 80 - data._cachedAt = Date.now(); 81 - 82 - await platform?.env?.USER_DATA_CACHE?.put(cacheKey, JSON.stringify(data)); 83 84 return json(data); 85 } catch (error) {
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 import { env } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 5 6 const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 7 ··· 13 ]; 14 15 const CACHE_TTL: Record<string, number> = { 16 + 'user.getRecentTracks': 15 * 60, 17 + 'user.getTopTracks': 60 * 60, 18 + 'user.getTopAlbums': 60 * 60, 19 + 'user.getInfo': 12 * 60 * 60 20 }; 21 22 export const GET: RequestHandler = async ({ url, platform }) => { ··· 33 return json({ error: 'Method not allowed' }, { status: 400 }); 34 } 35 36 + const cache = createCache(platform); 37 + const cacheKey = `${method}:${user}:${period}:${limit}`; 38 + const cachedData = await cache?.get('lastfm', cacheKey); 39 40 if (cachedData) { 41 + return json(JSON.parse(cachedData)); 42 } 43 44 const apiKey = env?.LASTFM_API_KEY; ··· 74 return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); 75 } 76 77 + const ttl = CACHE_TTL[method] || 60 * 60; 78 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); 79 80 return json(data); 81 } catch (error) {
+5 -14
src/routes/api/npmx-leaderboard/+server.ts
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 4 const LEADERBOARD_API_URL = 5 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 7 export const GET: RequestHandler = async ({ platform }) => { 8 - const cacheKey = '#npmx-leaderboard:likes'; 9 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 10 11 if (cachedData) { 12 - const parsedCache = JSON.parse(cachedData); 13 - 14 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 15 - const now = Date.now(); 16 - 17 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 18 - return json(parsedCache.data); 19 - } 20 } 21 22 try { ··· 31 32 const data = await response.json(); 33 34 - await platform?.env?.USER_DATA_CACHE?.put( 35 - cacheKey, 36 - JSON.stringify({ data, updatedAt: Date.now() }) 37 - ); 38 39 return json(data); 40 } catch (error) {
··· 1 import { json } from '@sveltejs/kit'; 2 import type { RequestHandler } from './$types'; 3 + import { createCache } from '$lib/cache'; 4 5 const LEADERBOARD_API_URL = 6 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 7 8 export const GET: RequestHandler = async ({ platform }) => { 9 + const cache = createCache(platform); 10 + const cachedData = await cache?.get('npmx', 'likes'); 11 12 if (cachedData) { 13 + return json(JSON.parse(cachedData)); 14 } 15 16 try { ··· 25 26 const data = await response.json(); 27 28 + await cache?.put('npmx', 'likes', JSON.stringify(data)); 29 30 return json(data); 31 } catch (error) {
+6 -3
src/routes/api/reloadRecent/+server.ts
··· 1 import { getDetailedProfile } from '$lib/atproto'; 2 import { json } from '@sveltejs/kit'; 3 import type { AppBskyActorDefs } from '@atcute/bluesky'; 4 5 export async function GET({ platform }) { 6 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 7 - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 8 9 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 10 ? JSON.parse(existingUsers) ··· 21 22 const newProfiles = await Promise.all(newProfilesPromises); 23 24 - await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(newProfiles)); 25 26 return json('ok'); 27 }
··· 1 import { getDetailedProfile } from '$lib/atproto'; 2 + import { createCache } from '$lib/cache'; 3 import { json } from '@sveltejs/kit'; 4 import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 6 export async function GET({ platform }) { 7 + const cache = createCache(platform); 8 + if (!cache) return json('no cache'); 9 + 10 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 11 12 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 13 ? JSON.parse(existingUsers) ··· 24 25 const newProfiles = await Promise.all(newProfilesPromises); 26 27 + await cache.put('meta', 'updatedBlentos', JSON.stringify(newProfiles)); 28 29 return json('ok'); 30 }
+8 -8
src/routes/api/update/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 2 import { getCache, loadData } from '$lib/website/load'; 3 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 4 import { json } from '@sveltejs/kit'; 5 6 export async function GET({ platform }) { 7 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 9 10 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 11 ? JSON.parse(existingUsers) 12 : []; 13 14 const existingUsersHandle = existingUsersArray.map((v) => v.handle); 15 - 16 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 17 18 for (const handle of existingUsersHandle) { 19 if (!handle) continue; 20 21 try { 22 - const cached = await getCache(handle, 'self', cache as UserCache); 23 - if (!cached) await loadData(handle, cache as UserCache, true); 24 } catch (error) { 25 console.error(error); 26 return json('error');
··· 1 + import { createCache } from '$lib/cache'; 2 import { getCache, loadData } from '$lib/website/load'; 3 import { json } from '@sveltejs/kit'; 4 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 6 export async function GET({ platform }) { 7 + const cache = createCache(platform); 8 + if (!cache) return json('no cache'); 9 + 10 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 11 12 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 13 ? JSON.parse(existingUsers) 14 : []; 15 16 const existingUsersHandle = existingUsersArray.map((v) => v.handle); 17 18 for (const handle of existingUsersHandle) { 19 if (!handle) continue; 20 21 try { 22 + const cached = await getCache(handle, 'self', cache); 23 + if (!cached) await loadData(handle, cache, true); 24 } catch (error) { 25 console.error(error); 26 return json('error');
+5 -5
src/routes/edit/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 - const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { createCache } from '$lib/cache'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 + const cache = createCache(platform); 12 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 + if (did) return await loadData(did as ActorIdentifier, cache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 + return await loadData(handle as ActorIdentifier, cache); 25 }
+4 -4
src/routes/p/[[page]]/+layout.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { Did, Handle } from '@atcute/lexicons'; 5 6 export async function load({ params, platform, request }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 9 const handle = env.PUBLIC_HANDLE; 10 ··· 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 - return await loadData(did as Did, cache as UserCache, false, params.page); 19 } catch { 20 console.error('failed'); 21 } 22 } 23 24 - return await loadData(handle as Handle, cache as UserCache, false, params.page); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { createCache } from '$lib/cache'; 4 import type { Did, Handle } from '@atcute/lexicons'; 5 6 export async function load({ params, platform, request }) { 7 + const cache = createCache(platform); 8 9 const handle = env.PUBLIC_HANDLE; 10 ··· 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 + return await loadData(did as Did, cache, false, params.page); 19 } catch { 20 console.error('failed'); 21 } 22 } 23 24 + return await loadData(handle as Handle, cache, false, params.page); 25 }
-30
src/routes/random/+page.server.ts
··· 1 - import type { UserCache, WebsiteData } from '$lib/types.js'; 2 - import { getCache } from '$lib/website/load.js'; 3 - import { error } from '@sveltejs/kit'; 4 - 5 - export async function load({ platform }) { 6 - const cache = platform?.env?.USER_DATA_CACHE; 7 - 8 - const list = await cache?.list(); 9 - 10 - if (!list) { 11 - throw error(404); 12 - } 13 - 14 - let foundData: WebsiteData | undefined = undefined; 15 - let i = 0; 16 - 17 - while (!foundData && i < 20) { 18 - const rando = Math.floor(Math.random() * list.keys.length); 19 - console.log(list.keys[rando].name); 20 - 21 - foundData = await getCache(list.keys[rando].name, 'self', cache as unknown as UserCache); 22 - 23 - if (!foundData?.cards.length) foundData = undefined; 24 - i++; 25 - } 26 - 27 - if (!foundData) throw error(404); 28 - 29 - return foundData; 30 - }
···
-40
src/routes/random/+page.svelte
··· 1 - <script lang="ts"> 2 - import Website from '$lib/website/Website.svelte'; 3 - import { Button } from '@foxui/core'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <svelte:body 9 - onkeydown={(e) => { 10 - if (e.key === 'ArrowRight' || e.key === 'r') { 11 - window.location.reload(); 12 - } 13 - }} 14 - /> 15 - 16 - <Website {data} /> 17 - 18 - <Button 19 - onclick={() => { 20 - window.location.reload(); 21 - }} 22 - size="lg" 23 - class="bg-accent-100 hover:bg-accent-200 dark:bg-accent-950/50 dark:hover:bg-accent-900/50 fixed right-4 bottom-4" 24 - ><svg 25 - xmlns="http://www.w3.org/2000/svg" 26 - width="24" 27 - height="24" 28 - viewBox="0 0 24 24" 29 - fill="none" 30 - stroke="currentColor" 31 - stroke-width="2" 32 - stroke-linecap="round" 33 - stroke-linejoin="round" 34 - class="lucide lucide-dices-icon lucide-dices" 35 - ><rect width="12" height="12" x="2" y="10" rx="2" ry="2" /><path 36 - d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" 37 - /><path d="M6 18h.01" /><path d="M10 14h.01" /><path d="M15 6h.01" /><path d="M18 9h.01" /></svg 38 - >Next 39 - <span class="sr-only">Next random profile</span></Button 40 - >
···
-16
src/routes/test/domains/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - 3 - export async function GET({ platform }) { 4 - const kv = platform?.env?.CUSTOM_DOMAINS; 5 - if (!kv) return json({ error: 'KV not available' }, { status: 500 }); 6 - 7 - const list = await kv.list(); 8 - const entries: Record<string, string> = {}; 9 - 10 - for (const key of list.keys) { 11 - const value = await kv.get(key.name); 12 - entries[key.name] = value ?? ''; 13 - } 14 - 15 - return json(entries); 16 - }
···