20 changed files with 826 additions and 499 deletions
@ -1,11 +0,0 @@ |
|||||||
import { derived, writable } from "svelte/store"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Stores the user's public key if logged in, or null otherwise. |
|
||||||
*/ |
|
||||||
export const userPubkey = writable<string | null>(null); |
|
||||||
|
|
||||||
/** |
|
||||||
* Derived store indicating if the user is logged in. |
|
||||||
*/ |
|
||||||
export const isLoggedIn = derived(userPubkey, ($userPubkey) => !!$userPubkey); |
|
||||||
@ -0,0 +1,85 @@ |
|||||||
|
import { unifiedProfileCache } from './npubCache'; |
||||||
|
import { searchCache } from './searchCache'; |
||||||
|
import { indexEventCache } from './indexEventCache'; |
||||||
|
import { clearRelaySetCache } from '../ndk'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears all application caches |
||||||
|
*
|
||||||
|
* Clears: |
||||||
|
* - unifiedProfileCache (profile metadata) |
||||||
|
* - searchCache (search results) |
||||||
|
* - indexEventCache (index events) |
||||||
|
* - relaySetCache (relay configuration) |
||||||
|
*/ |
||||||
|
export function clearAllCaches(): void { |
||||||
|
console.log('[CacheManager] Clearing all application caches...'); |
||||||
|
|
||||||
|
// Clear in-memory caches
|
||||||
|
unifiedProfileCache.clear(); |
||||||
|
searchCache.clear(); |
||||||
|
indexEventCache.clear(); |
||||||
|
clearRelaySetCache(); |
||||||
|
|
||||||
|
// Clear localStorage caches
|
||||||
|
clearLocalStorageCaches(); |
||||||
|
|
||||||
|
console.log('[CacheManager] All caches cleared successfully'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears profile-specific caches to force fresh profile data |
||||||
|
* This is useful when profile pictures or metadata are stale |
||||||
|
*/ |
||||||
|
export function clearProfileCaches(): void { |
||||||
|
console.log('[CacheManager] Clearing profile-specific caches...'); |
||||||
|
|
||||||
|
// Clear unified profile cache
|
||||||
|
unifiedProfileCache.clear(); |
||||||
|
|
||||||
|
// Clear profile-related search results
|
||||||
|
// Note: searchCache doesn't have a way to clear specific types, so we clear all
|
||||||
|
// This is acceptable since profile searches are the most common
|
||||||
|
searchCache.clear(); |
||||||
|
|
||||||
|
console.log('[CacheManager] Profile caches cleared successfully'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears localStorage caches |
||||||
|
*/ |
||||||
|
function clearLocalStorageCaches(): void { |
||||||
|
if (typeof window === 'undefined') return; |
||||||
|
|
||||||
|
const keysToRemove: string[] = []; |
||||||
|
|
||||||
|
// Find all localStorage keys that start with 'alexandria'
|
||||||
|
for (let i = 0; i < localStorage.length; i++) { |
||||||
|
const key = localStorage.key(i); |
||||||
|
if (key && key.startsWith('alexandria')) { |
||||||
|
keysToRemove.push(key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove the keys
|
||||||
|
keysToRemove.forEach(key => { |
||||||
|
localStorage.removeItem(key); |
||||||
|
}); |
||||||
|
|
||||||
|
console.log(`[CacheManager] Cleared ${keysToRemove.length} localStorage items`); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets statistics about all caches |
||||||
|
*/ |
||||||
|
export function getCacheStats(): { |
||||||
|
profileCacheSize: number; |
||||||
|
searchCacheSize: number; |
||||||
|
indexEventCacheSize: number; |
||||||
|
} { |
||||||
|
return { |
||||||
|
profileCacheSize: unifiedProfileCache.size(), |
||||||
|
searchCacheSize: searchCache.size(), |
||||||
|
indexEventCacheSize: indexEventCache.size(), |
||||||
|
}; |
||||||
|
} |
||||||
@ -1,257 +0,0 @@ |
|||||||
import NDK, { type NDKEvent } from "@nostr-dev-kit/ndk"; |
|
||||||
import { nip19 } from "nostr-tools"; |
|
||||||
import { toNpub } from "./nostrUtils"; |
|
||||||
|
|
||||||
interface ProfileData { |
|
||||||
display_name?: string; |
|
||||||
name?: string; |
|
||||||
picture?: string; |
|
||||||
about?: string; |
|
||||||
} |
|
||||||
|
|
||||||
// Cache for user profiles
|
|
||||||
const profileCache = new Map<string, ProfileData>(); |
|
||||||
|
|
||||||
/** |
|
||||||
* Fetches profile data for a pubkey |
|
||||||
* @param pubkey - The public key to fetch profile for |
|
||||||
* @returns Profile data or null if not found |
|
||||||
*/ |
|
||||||
async function fetchProfile(pubkey: string, ndk: NDK): Promise<ProfileData | null> { |
|
||||||
try { |
|
||||||
const profileEvents = await ndk.fetchEvents({ |
|
||||||
kinds: [0], |
|
||||||
authors: [pubkey], |
|
||||||
limit: 1, |
|
||||||
}); |
|
||||||
|
|
||||||
if (profileEvents.size === 0) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
// Get the most recent profile event
|
|
||||||
const profileEvent = Array.from(profileEvents)[0]; |
|
||||||
|
|
||||||
try { |
|
||||||
const content = JSON.parse(profileEvent.content); |
|
||||||
return content as ProfileData; |
|
||||||
} catch (e) { |
|
||||||
console.error("Failed to parse profile content:", e); |
|
||||||
return null; |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
console.error("Failed to fetch profile:", e); |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets the display name for a pubkey, using cache |
|
||||||
* @param pubkey - The public key to get display name for |
|
||||||
* @returns Display name, name, or shortened npub (never hex ID) |
|
||||||
*/ |
|
||||||
export async function getDisplayName(pubkey: string, ndk: NDK): Promise<string> { |
|
||||||
// Check cache first
|
|
||||||
if (profileCache.has(pubkey)) { |
|
||||||
const profile = profileCache.get(pubkey)!; |
|
||||||
const npub = toNpub(pubkey); |
|
||||||
return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); |
|
||||||
} |
|
||||||
|
|
||||||
// Fetch profile
|
|
||||||
const profile = await fetchProfile(pubkey, ndk); |
|
||||||
if (profile) { |
|
||||||
profileCache.set(pubkey, profile); |
|
||||||
const npub = toNpub(pubkey); |
|
||||||
return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); |
|
||||||
} |
|
||||||
|
|
||||||
// Fallback to shortened npub or pubkey
|
|
||||||
const npub = toNpub(pubkey); |
|
||||||
return npub ? shortenNpub(npub) : pubkey; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Batch fetches profiles for multiple pubkeys |
|
||||||
* @param pubkeys - Array of public keys to fetch profiles for |
|
||||||
* @param onProgress - Optional callback for progress updates |
|
||||||
* @returns Array of profile events |
|
||||||
*/ |
|
||||||
export async function batchFetchProfiles( |
|
||||||
pubkeys: string[], |
|
||||||
ndk: NDK, |
|
||||||
onProgress?: (fetched: number, total: number) => void, |
|
||||||
): Promise<NDKEvent[]> { |
|
||||||
const allProfileEvents: NDKEvent[] = []; |
|
||||||
|
|
||||||
// Filter out already cached pubkeys
|
|
||||||
const uncachedPubkeys = pubkeys.filter((pk) => !profileCache.has(pk)); |
|
||||||
|
|
||||||
if (uncachedPubkeys.length === 0) { |
|
||||||
if (onProgress) onProgress(pubkeys.length, pubkeys.length); |
|
||||||
return allProfileEvents; |
|
||||||
} |
|
||||||
|
|
||||||
try { |
|
||||||
// Report initial progress
|
|
||||||
const cachedCount = pubkeys.length - uncachedPubkeys.length; |
|
||||||
if (onProgress) onProgress(cachedCount, pubkeys.length); |
|
||||||
|
|
||||||
// Batch fetch in chunks to avoid overwhelming relays
|
|
||||||
const CHUNK_SIZE = 50; |
|
||||||
let fetchedCount = cachedCount; |
|
||||||
|
|
||||||
for (let i = 0; i < uncachedPubkeys.length; i += CHUNK_SIZE) { |
|
||||||
const chunk = uncachedPubkeys.slice( |
|
||||||
i, |
|
||||||
Math.min(i + CHUNK_SIZE, uncachedPubkeys.length), |
|
||||||
); |
|
||||||
|
|
||||||
const profileEvents = await ndk.fetchEvents({ |
|
||||||
kinds: [0], |
|
||||||
authors: chunk, |
|
||||||
}); |
|
||||||
|
|
||||||
// Process each profile event
|
|
||||||
profileEvents.forEach((event: NDKEvent) => { |
|
||||||
try { |
|
||||||
const content = JSON.parse(event.content); |
|
||||||
profileCache.set(event.pubkey, content as ProfileData); |
|
||||||
allProfileEvents.push(event); |
|
||||||
fetchedCount++; |
|
||||||
} catch (e) { |
|
||||||
console.error("Failed to parse profile content:", e); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
// Update progress
|
|
||||||
if (onProgress) { |
|
||||||
onProgress(fetchedCount, pubkeys.length); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Final progress update
|
|
||||||
if (onProgress) onProgress(pubkeys.length, pubkeys.length); |
|
||||||
} catch (e) { |
|
||||||
console.error("Failed to batch fetch profiles:", e); |
|
||||||
} |
|
||||||
|
|
||||||
return allProfileEvents; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets display name synchronously from cache |
|
||||||
* @param pubkey - The public key to get display name for |
|
||||||
* @returns Display name, name, or shortened npub (never hex ID) |
|
||||||
*/ |
|
||||||
export function getDisplayNameSync(pubkey: string): string { |
|
||||||
if (profileCache.has(pubkey)) { |
|
||||||
const profile = profileCache.get(pubkey)!; |
|
||||||
const npub = toNpub(pubkey); |
|
||||||
return profile.display_name || profile.name || (npub ? shortenNpub(npub) : pubkey); |
|
||||||
} |
|
||||||
const npub = toNpub(pubkey); |
|
||||||
return npub ? shortenNpub(npub) : pubkey; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Shortens an npub for display |
|
||||||
* @param npub - The npub to shorten |
|
||||||
* @returns Shortened npub (first 8 chars...last 4 chars) |
|
||||||
*/ |
|
||||||
function shortenNpub(npub: string): string { |
|
||||||
if (npub.length <= 12) return npub; |
|
||||||
return `${npub.slice(0, 8)}...${npub.slice(-4)}`; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Clears the profile cache |
|
||||||
*/ |
|
||||||
export function clearProfileCache(): void { |
|
||||||
profileCache.clear(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Extracts all pubkeys from events (authors and p tags) |
|
||||||
* @param events - Array of events to extract pubkeys from |
|
||||||
* @returns Set of unique pubkeys |
|
||||||
*/ |
|
||||||
export function extractPubkeysFromEvents(events: NDKEvent[]): Set<string> { |
|
||||||
const pubkeys = new Set<string>(); |
|
||||||
|
|
||||||
events.forEach((event) => { |
|
||||||
// Add author pubkey
|
|
||||||
if (event.pubkey) { |
|
||||||
pubkeys.add(event.pubkey); |
|
||||||
} |
|
||||||
|
|
||||||
// Add pubkeys from p tags
|
|
||||||
const pTags = event.getMatchingTags("p"); |
|
||||||
pTags.forEach((tag) => { |
|
||||||
if (tag[1]) { |
|
||||||
pubkeys.add(tag[1]); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
// Extract pubkeys from content (nostr:npub1... format)
|
|
||||||
const npubPattern = /nostr:npub1[a-z0-9]{58}/g; |
|
||||||
const matches = event.content?.match(npubPattern) || []; |
|
||||||
matches.forEach((match) => { |
|
||||||
try { |
|
||||||
const npub = match.replace("nostr:", ""); |
|
||||||
const decoded = nip19.decode(npub); |
|
||||||
if (decoded.type === "npub") { |
|
||||||
pubkeys.add(decoded.data as string); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
// Invalid npub, ignore
|
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
return pubkeys; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Replaces pubkeys in content with display names |
|
||||||
* @param content - The content to process |
|
||||||
* @returns Content with pubkeys replaced by display names |
|
||||||
*/ |
|
||||||
export function replaceContentPubkeys(content: string): string { |
|
||||||
if (!content) return content; |
|
||||||
|
|
||||||
// Replace nostr:npub1... references
|
|
||||||
const npubPattern = /nostr:npub[a-z0-9]{58}/g; |
|
||||||
let result = content; |
|
||||||
|
|
||||||
const matches = content.match(npubPattern) || []; |
|
||||||
matches.forEach((match) => { |
|
||||||
try { |
|
||||||
const npub = match.replace("nostr:", ""); |
|
||||||
const decoded = nip19.decode(npub); |
|
||||||
if (decoded.type === "npub") { |
|
||||||
const pubkey = decoded.data as string; |
|
||||||
const displayName = getDisplayNameSync(pubkey); |
|
||||||
result = result.replace(match, `@${displayName}`); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
// Invalid npub, leave as is
|
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Replaces pubkey references in text with display names |
|
||||||
* @param text - Text that may contain pubkey references |
|
||||||
* @returns Text with pubkeys replaced by display names |
|
||||||
*/ |
|
||||||
export function replacePubkeysWithDisplayNames(text: string): string { |
|
||||||
// Match hex pubkeys (64 characters)
|
|
||||||
const pubkeyRegex = /\b[0-9a-fA-F]{64}\b/g; |
|
||||||
|
|
||||||
return text.replace(pubkeyRegex, (match) => { |
|
||||||
return getDisplayNameSync(match); |
|
||||||
}); |
|
||||||
} |
|
||||||
Loading…
Reference in new issue