From 47f1357770f782db486d34a5375ea88740eec9c6 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sat, 25 Jul 2026 12:36:03 -0400 Subject: [PATCH] excise actor resolution + add cache --- src/components/profile/ProfileAvatar.tsx | 9 ++- src/components/vouch/VouchList.tsx | 15 ++-- src/components/vouch/VouchListItem.tsx | 13 +--- src/lib/actor.ts | 96 ++++++++++++++++++++++++ src/lib/profilePage.ts | 19 +++-- 5 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 src/lib/actor.ts diff --git a/src/components/profile/ProfileAvatar.tsx b/src/components/profile/ProfileAvatar.tsx index 27df945..c2e46d1 100644 --- a/src/components/profile/ProfileAvatar.tsx +++ b/src/components/profile/ProfileAvatar.tsx @@ -7,6 +7,7 @@ type ProfileAvatarProps = { miniDoc: MiniDoc profile: Profile bskyProfile?: AppBskyActorProfile.Main | null + avatarUrl?: string | null size?: 'small' | 'medium' } @@ -14,16 +15,18 @@ export function ProfileAvatar({ miniDoc, profile, bskyProfile, + avatarUrl, size = 'medium', }: ProfileAvatarProps) { const avatar = profile.value.avatar ?? bskyProfile?.avatar - const avatarUrl = avatar ? getAvatarUrl(miniDoc.did, avatar) : null + const resolvedAvatarUrl = + avatarUrl === undefined ? (avatar ? getAvatarUrl(miniDoc.did, avatar) : null) : avatarUrl const sizeClass = size === 'small' ? 'size-8 text-sm' : 'size-16 text-xl' - if (avatarUrl) { + if (resolvedAvatarUrl) { return ( {`${miniDoc.handle} diff --git a/src/components/vouch/VouchList.tsx b/src/components/vouch/VouchList.tsx index 8a3228d..bc206b7 100644 --- a/src/components/vouch/VouchList.tsx +++ b/src/components/vouch/VouchList.tsx @@ -1,18 +1,16 @@ import { parseResourceUri, type Did } from '@atcute/lexicons' import { isDid } from '@atcute/lexicons/syntax' import { useEffect, useState } from 'react' -import { getProfile as getBskyProfile } from '../../lib/bsky/actor' -import { getMiniDoc } from '../../lib/microcosm' -import { getProfile } from '../../lib/tangled' +import { resolveActor, type ResolvedActor } from '../../lib/actor' import type { VouchRecord } from '../../lib/tangled/graph' -import { VouchListItem, type VouchAuthor } from './VouchListItem' +import { VouchListItem } from './VouchListItem' type VouchListProps = { vouches: VouchRecord[] } export function VouchList({ vouches }: VouchListProps) { - const [authors, setAuthors] = useState>(new Map()) + const [authors, setAuthors] = useState>(new Map()) const [isLoading, setIsLoading] = useState(true) useEffect(() => { @@ -26,11 +24,8 @@ export function VouchList({ vouches }: VouchListProps) { ] const resolvedAuthors = await Promise.allSettled( - authorDids.map(async (did): Promise<[string, VouchAuthor]> => { - const [miniDoc, profile] = await Promise.all([getMiniDoc(did), getProfile(did)]) - const bskyProfile = await getBskyProfile(miniDoc).catch(() => null) - - return [did, { miniDoc, profile, bskyProfile }] + authorDids.map(async (did): Promise<[string, ResolvedActor]> => { + return [did, await resolveActor(did)] }), ) diff --git a/src/components/vouch/VouchListItem.tsx b/src/components/vouch/VouchListItem.tsx index 83b1423..4d845d8 100644 --- a/src/components/vouch/VouchListItem.tsx +++ b/src/components/vouch/VouchListItem.tsx @@ -1,21 +1,13 @@ -import type { AppBskyActorProfile } from '@atcute/bluesky' -import type { $output as MiniDoc } from '@atcute/microcosm/types/blue/microcosm/identity/resolveMiniDoc' import { IconThumbDown, IconThumbUp } from '@tabler/icons-react' import { Link } from 'react-router-dom' -import type { Profile } from '../../lib/tangled' +import type { ResolvedActor } from '../../lib/actor' import { ProfileAvatar } from '../profile/ProfileAvatar' import { SurfaceCard } from '../shared/SurfaceCard' import type { VouchRecord } from '../../lib/tangled/graph' type VouchListItemProps = { vouchRecord: VouchRecord - author: VouchAuthor -} - -export type VouchAuthor = { - miniDoc: MiniDoc - profile: Profile - bskyProfile: AppBskyActorProfile.Main | null + author: ResolvedActor } export function VouchListItem({ vouchRecord, author }: VouchListItemProps) { @@ -38,6 +30,7 @@ export function VouchListItem({ vouchRecord, author }: VouchListItemProps) { miniDoc={author.miniDoc} profile={author.profile} bskyProfile={author.bskyProfile} + avatarUrl={author.avatarUrl} size="small" /> diff --git a/src/lib/actor.ts b/src/lib/actor.ts new file mode 100644 index 0000000..94cde01 --- /dev/null +++ b/src/lib/actor.ts @@ -0,0 +1,96 @@ +import type { AppBskyActorProfile } from '@atcute/bluesky' +import type { Did } from '@atcute/lexicons' +import type { $output as MiniDoc } from '@atcute/microcosm/types/blue/microcosm/identity/resolveMiniDoc' +import { getAvatarUrl } from './atproto/media' +import { getProfile as getBskyProfile } from './bsky/actor' +import { getMiniDoc } from './microcosm' +import { getProfile, type Profile } from './tangled' + +export type ResolvedActor = { + miniDoc: MiniDoc + profile: Profile + bskyProfile: AppBskyActorProfile.Main | null + avatarUrl: string | null +} + +type CacheEntry = { + value: Promise + expiresAt: number +} + +const ACTOR_CACHE_TTL_MS = 5 * 60 * 1000 +const miniDocCache = new Map>() +const actorCache = new Map>() + +export function resolveMiniDoc(identifier: string): Promise { + const cached = getCachedValue(miniDocCache, identifier) + if (cached !== undefined) return cached + + const request = getMiniDoc(identifier).catch((error) => { + deleteIfCurrent(miniDocCache, identifier, request) + throw error + }) + + miniDocCache.set(identifier, createCacheEntry(request)) + return request +} + +export function resolveActor(did: Did): Promise { + const cached = getCachedValue(actorCache, did) + if (cached !== undefined) return cached + + const request = Promise.all([resolveMiniDoc(did), getProfile(did)]) + .then(async ([miniDoc, profile]) => { + const bskyProfile = await getBskyProfile(miniDoc).catch(() => null) + const avatar = profile.value.avatar ?? bskyProfile?.avatar + const avatarUrl = avatar ? getAvatarUrl(did, avatar) : null + + return { miniDoc, profile, bskyProfile, avatarUrl } + }) + .catch((error) => { + deleteIfCurrent(actorCache, did, request) + throw error + }) + + actorCache.set(did, createCacheEntry(request)) + return request +} + +export function invalidateActor(did: Did): void { + actorCache.delete(did) + miniDocCache.delete(did) +} + +export function clearActorCache(): void { + actorCache.clear() + miniDocCache.clear() +} + +function createCacheEntry(value: Promise): CacheEntry { + return { + value, + expiresAt: Date.now() + ACTOR_CACHE_TTL_MS, + } +} + +function getCachedValue(cache: Map>, key: string): Promise | undefined { + const entry = cache.get(key) + if (entry === undefined) return undefined + + if (entry.expiresAt <= Date.now()) { + cache.delete(key) + return undefined + } + + return entry.value +} + +function deleteIfCurrent( + cache: Map>, + key: string, + value: Promise, +): void { + if (cache.get(key)?.value === value) { + cache.delete(key) + } +} diff --git a/src/lib/profilePage.ts b/src/lib/profilePage.ts index a3f21ca..5ff575d 100644 --- a/src/lib/profilePage.ts +++ b/src/lib/profilePage.ts @@ -1,9 +1,8 @@ import type { AppBskyActorProfile } from '@atcute/bluesky' import { parseResourceUri, type Did, type Handle } from '@atcute/lexicons' import type { $output as MiniDoc } from '@atcute/microcosm/types/blue/microcosm/identity/resolveMiniDoc' -import { getProfile as getBskyProfile } from './bsky/actor' -import { getMiniDoc } from './microcosm' -import { getProfile, listStrings, type Profile, type StringList } from './tangled' +import { resolveActor, resolveMiniDoc } from './actor' +import { listStrings, type Profile, type StringList } from './tangled' import { getRepoByRepoDid, listRepos, type Repo, type RepoList } from './tangled/repo' import { getRepoDidsFromStars, listStarsBy } from './tangled/feed' import { listVouches, type VouchList } from './tangled/graph' @@ -25,24 +24,24 @@ export type ProfilePageData = { } export async function loadProfilePage(handle: Handle): Promise { - const identity = await getMiniDoc(handle) + const identity = await resolveMiniDoc(handle) const stars = await listStarsBy(identity.did) const repoDids = getRepoDidsFromStars(stars) - const [profile, repos, vouches, strings, bskyProfile] = await Promise.all([ - getProfile(identity.did), + const [actor, repos, vouches, strings] = await Promise.all([ + resolveActor(identity.did), listRepos(identity.did), listVouches(identity.did), listStrings(identity.did), - getBskyProfile(identity).catch(() => null), ]) + const profile = actor.profile const ownerDocs = new Map>() const getOwner = (identifier: string) => { const existing = ownerDocs.get(identifier) if (existing !== undefined) return existing - const request = getMiniDoc(identifier) + const request = resolveMiniDoc(identifier) ownerDocs.set(identifier, request) return request } @@ -68,12 +67,12 @@ export async function loadProfilePage(handle: Handle): Promise return { identity, - profile, + profile: actor.profile, repos, strings, pinnedRepos, starredRepos, vouches, - bskyProfile, + bskyProfile: actor.bskyProfile, } } -- 2.51.2