your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #283 from flo-bit/fix/make-site-faster

make site faster

authored by

Florian and committed by
GitHub
457434c0 9b4c79be

+118 -4
+8 -1
src/app.d.ts
··· 1 - import type { KVNamespace, D1Database } from '@cloudflare/workers-types'; 1 + import type { 2 + KVNamespace, 3 + D1Database, 4 + ExecutionContext, 5 + CacheStorage 6 + } from '@cloudflare/workers-types'; 2 7 import type { OAuthSession } from '@atcute/oauth-node-client'; 3 8 import type { Client } from '@atcute/client'; 4 9 import type { Did } from '@atcute/lexicons'; ··· 26 31 COOKIE_SECRET: string; 27 32 CRON_SECRET: string; 28 33 }; 34 + context: ExecutionContext; 35 + caches: CacheStorage & { default: Cache }; 29 36 } 30 37 } 31 38 }
+2
src/app.html
··· 3 3 <head> 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <link rel="preconnect" href="https://cdn.bsky.app" crossorigin /> 7 + <link rel="preconnect" href="https://video.bsky.app" crossorigin /> 6 8 %sveltekit.head% 7 9 8 10 <script
+60
src/lib/cache.ts
··· 14 14 ical: 60 * 60 * 2, // 2 hours 15 15 events: 60 * 60, // 1 hour 16 16 rsvps: 60 * 60, // 1 hour 17 + 'card-data': 5 * 60, // 5 minutes (fresh window for loadData SWR) 17 18 meta: 0 // no auto-expiry 18 19 } as const; 19 20 ··· 81 82 await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); 82 83 } 83 84 85 + // === Stale-while-revalidate === 86 + 87 + /** 88 + * Read-through cache with stale-while-revalidate semantics. 89 + * 90 + * - Fresh hit (now < staleAt): returns cached value immediately. 91 + * - Stale hit: returns cached value immediately, kicks off a background refresh via `waitUntil`. 92 + * - Miss: awaits the loader and populates the cache. 93 + * 94 + * The stored entry lives in KV for `ttl + staleWindow` seconds so stale reads can keep 95 + * serving while a refresh runs. 96 + */ 97 + async swr<T>( 98 + namespace: CacheNamespace, 99 + key: string, 100 + loader: () => Promise<T>, 101 + opts?: { 102 + ttl?: number; 103 + staleWindow?: number; 104 + waitUntil?: (p: Promise<unknown>) => void; 105 + } 106 + ): Promise<T> { 107 + const ttl = opts?.ttl ?? NAMESPACE_TTL[namespace] ?? 300; 108 + const staleWindow = opts?.staleWindow ?? 24 * 60 * 60; 109 + const waitUntil = opts?.waitUntil ?? ((p: Promise<unknown>) => void p.catch(() => {})); 110 + 111 + const now = Date.now(); 112 + const entry = await this.getJSON<SwrEntry<T>>(namespace, key); 113 + 114 + const refresh = async (): Promise<T> => { 115 + const value = await loader(); 116 + const entry: SwrEntry<T> = { staleAt: Date.now() + ttl * 1000, value }; 117 + await this.putJSON(namespace, key, entry, ttl + staleWindow); 118 + return value; 119 + }; 120 + 121 + if (entry) { 122 + if (entry.staleAt > now) return entry.value; 123 + const staleFor = Math.round((now - entry.staleAt) / 1000); 124 + console.log(`[swr] background refresh ${namespace}:${key} (stale ${staleFor}s)`); 125 + const started = Date.now(); 126 + waitUntil( 127 + refresh().then( 128 + () => console.log(`[swr] refreshed ${namespace}:${key} in ${Date.now() - started}ms`), 129 + (err) => 130 + console.error( 131 + `[swr] refresh failed ${namespace}:${key} after ${Date.now() - started}ms`, 132 + err 133 + ) 134 + ) 135 + ); 136 + return entry.value; 137 + } 138 + 139 + return await refresh(); 140 + } 141 + 84 142 // === Profile cache (did → profile data) === 85 143 async getProfile(did: Did): Promise<CachedProfile> { 86 144 const cached = await this.getJSON<CachedProfile>('profile', did); ··· 100 158 return data; 101 159 } 102 160 } 161 + 162 + type SwrEntry<T> = { staleAt: number; value: T }; 103 163 104 164 export type CachedProfile = { 105 165 did: string;
+1
src/lib/cards/media/PopfeedReviews/index.ts
··· 16 16 17 17 return data; 18 18 }, 19 + cacheLoadData: true, 19 20 minH: 3, 20 21 canHaveLabel: true, 21 22
+1
src/lib/cards/media/StatusphereCard/index.ts
··· 25 25 const data = await listRecords({ did, collection: 'xyz.statusphere.status', limit: 1 }); 26 26 return data[0]; 27 27 }, 28 + cacheLoadData: true, 28 29 upload: async (item) => { 29 30 if (item.cardData.mode === 'statusphere' && item.cardData.hasUpdate) { 30 31 await putRecord({
+1
src/lib/cards/media/TealFMPlaysCard/index.ts
··· 20 20 21 21 return data; 22 22 }, 23 + cacheLoadData: true, 23 24 minW: 4, 24 25 canHaveLabel: true, 25 26 canAdd: ({ collections }) => collections.includes('fm.teal.alpha.feed.play'),
+1
src/lib/cards/social/BlueskyPostCard/index.ts
··· 61 61 62 62 return postsMap; 63 63 }, 64 + cacheLoadData: true, 64 65 minW: 4, 65 66 name: 'Bluesky Post', 66 67
+1
src/lib/cards/social/GuestbookCard/index.ts
··· 59 59 60 60 return results; 61 61 }, 62 + cacheLoadData: true, 62 63 name: 'Guestbook', 63 64 keywords: ['comments', 'visitors', 'message', 'sign'], 64 65 groups: ['Social'],
+1
src/lib/cards/social/LatestBlueskyPostCard/index.ts
··· 19 19 20 20 return JSON.parse(JSON.stringify(authorFeed)); 21 21 }, 22 + cacheLoadData: true, 22 23 minW: 4, 23 24 24 25 name: 'Latest Bluesky Post',
+5
src/lib/cards/types.ts
··· 40 40 { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 41 ) => Promise<unknown>; 42 42 43 + // Opt into server-side KV caching of `loadData` results, with stale-while-revalidate. 44 + // Key = `${type}:${did}:${hash(items.cardData)}`. On SSR, a stale hit is returned 45 + // immediately while a background refresh repopulates the cache. 46 + cacheLoadData?: boolean | { ttl?: number; staleWindow?: number }; 47 + 43 48 // server-side version of loadData that calls external APIs directly 44 49 // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers) 45 50 loadDataServer?: (
+22 -1
src/lib/website/load.ts
··· 468 468 return card; 469 469 } 470 470 471 + function hashCardData(items: Item[]): string { 472 + const str = JSON.stringify(items.map((i) => i.cardData ?? null)); 473 + let hash = 0x811c9dc5; 474 + for (let i = 0; i < str.length; i++) { 475 + hash ^= str.charCodeAt(i); 476 + hash = Math.imul(hash, 0x01000193); 477 + } 478 + return (hash >>> 0).toString(36); 479 + } 480 + 471 481 async function loadAdditionalData( 472 482 cards: Item[], 473 483 { ··· 496 506 platform 497 507 }); 498 508 } else if (cardDef?.loadData) { 499 - additionDataPromises[cardType] = cardDef.loadData(items, { did, handle, cache }); 509 + const loader = () => cardDef.loadData!(items, { did, handle, cache }); 510 + if (cache && cardDef.cacheLoadData) { 511 + const opts = typeof cardDef.cacheLoadData === 'object' ? cardDef.cacheLoadData : {}; 512 + const key = `${cardType}:${did}:${hashCardData(items)}`; 513 + additionDataPromises[cardType] = cache.swr('card-data', key, loader, { 514 + ttl: opts.ttl, 515 + staleWindow: opts.staleWindow, 516 + waitUntil: platform?.context?.waitUntil?.bind(platform.context) 517 + }); 518 + } else { 519 + additionDataPromises[cardType] = loader(); 520 + } 500 521 } 501 522 } catch { 502 523 console.error('error getting additional data for', cardType);
+15 -2
src/routes/[[actor=actor]]/(pages)/+layout.server.ts
··· 4 4 import { createCache } from '$lib/cache'; 5 5 import { getActor } from '$lib/actor.js'; 6 6 7 - export async function load({ params, platform, request }) { 7 + export async function load({ params, platform, request, locals, route, setHeaders }) { 8 8 if (env.PUBLIC_IS_SELFHOSTED) error(404); 9 9 10 10 const cache = createCache(platform); ··· 15 15 throw error(404, 'Page not found'); 16 16 } 17 17 18 - return await loadData(actor, cache, params.page, env, platform); 18 + const data = await loadData(actor, cache, params.page, env, platform); 19 + 20 + const isInteractiveRoute = route.id?.endsWith('/edit') || route.id?.endsWith('/copy') || false; 21 + const isAnonymous = !locals.did; 22 + 23 + if (isAnonymous && !isInteractiveRoute) { 24 + setHeaders({ 25 + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=600' 26 + }); 27 + } else { 28 + setHeaders({ 'Cache-Control': 'private, no-store' }); 29 + } 30 + 31 + return data; 19 32 }