diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md index 7830711..8f6cfeb 100644 --- a/.claude/skills/applesauce-core/SKILL.md +++ b/.claude/skills/applesauce-core/SKILL.md @@ -329,6 +329,284 @@ zaps$.subscribe(zaps => { }); ``` +## Helper Functions & Caching + +### Applesauce Helper System + +applesauce-core provides **60+ helper functions** for extracting data from Nostr events. **Critical**: These helpers cache their results internally using symbols, so **you don't need `useMemo` when calling them**. + +```javascript +// ❌ WRONG - Unnecessary memoization +const title = useMemo(() => getArticleTitle(event), [event]); +const text = useMemo(() => getHighlightText(event), [event]); + +// ✅ CORRECT - Helpers cache internally +const title = getArticleTitle(event); +const text = getHighlightText(event); +``` + +### How Helper Caching Works + +```javascript +import { getOrComputeCachedValue } from 'applesauce-core/helpers'; + +// Helpers use symbol-based caching +const symbol = Symbol('ArticleTitle'); + +export function getArticleTitle(event) { + return getOrComputeCachedValue(event, symbol, () => { + // This expensive computation only runs once + return getTagValue(event, 'title') || 'Untitled'; + }); +} + +// First call - computes and caches +const title1 = getArticleTitle(event); // Computation happens + +// Second call - returns cached value +const title2 = getArticleTitle(event); // Instant, from cache + +// Same reference +console.log(title1 === title2); // true +``` + +### Tag Helpers + +```javascript +import { getTagValue, hasNameValueTag } from 'applesauce-core/helpers'; + +// Get single tag value (searches hidden tags first) +const dTag = getTagValue(event, 'd'); +const title = getTagValue(event, 'title'); +const url = getTagValue(event, 'r'); + +// Check if tag exists with value +const hasTag = hasNameValueTag(event, 'client', 'grimoire'); +``` + +**Note**: applesauce only provides `getTagValue` (singular). For multiple values, implement your own: + +```javascript +function getTagValues(event, tagName) { + return event.tags + .filter(tag => tag[0] === tagName && tag[1]) + .map(tag => tag[1]); +} +``` + +### Article Helpers (NIP-23) + +```javascript +import { + getArticleTitle, + getArticleSummary, + getArticleImage, + getArticlePublished, + isValidArticle +} from 'applesauce-core/helpers'; + +// All cached automatically +const title = getArticleTitle(event); +const summary = getArticleSummary(event); +const image = getArticleImage(event); +const publishedAt = getArticlePublished(event); + +// Validation +if (isValidArticle(event)) { + console.log('Valid article event'); +} +``` + +### Highlight Helpers (NIP-84) + +```javascript +import { + getHighlightText, + getHighlightSourceUrl, + getHighlightSourceEventPointer, + getHighlightSourceAddressPointer, + getHighlightContext, + getHighlightComment, + getHighlightAttributions +} from 'applesauce-core/helpers'; + +// All cached - no useMemo needed +const text = getHighlightText(event); +const url = getHighlightSourceUrl(event); +const eventPointer = getHighlightSourceEventPointer(event); +const addressPointer = getHighlightSourceAddressPointer(event); +const context = getHighlightContext(event); +const comment = getHighlightComment(event); +const attributions = getHighlightAttributions(event); +``` + +### Profile Helpers + +```javascript +import { + getProfileContent, + getDisplayName, + getProfilePicture, + isValidProfile +} from 'applesauce-core/helpers'; + +// Parse profile JSON (cached) +const profile = getProfileContent(profileEvent); + +// Get display name with fallback +const name = getDisplayName(profile, 'Anonymous'); + +// Get profile picture with fallback +const avatar = getProfilePicture(profile, '/default-avatar.png'); + +// Validation +if (isValidProfile(event)) { + const profile = getProfileContent(event); +} +``` + +### Pointer Helpers (NIP-19) + +```javascript +import { + parseCoordinate, + getEventPointerFromETag, + getEventPointerFromQTag, + getAddressPointerFromATag, + getProfilePointerFromPTag, + getEventPointerForEvent, + getAddressPointerForEvent +} from 'applesauce-core/helpers'; + +// Parse "a" tag coordinate (30023:pubkey:identifier) +const aTag = event.tags.find(t => t[0] === 'a')?.[1]; +const pointer = parseCoordinate(aTag); +console.log(pointer.kind, pointer.pubkey, pointer.identifier); + +// Extract pointers from tags +const eTag = event.tags.find(t => t[0] === 'e'); +const eventPointer = getEventPointerFromETag(eTag); + +const qTag = event.tags.find(t => t[0] === 'q'); +const quotePointer = getEventPointerFromQTag(qTag); + +// Create pointer from event +const pointer = getEventPointerForEvent(event, ['wss://relay.example.com']); +const address = getAddressPointerForEvent(replaceableEvent); +``` + +### Reaction Helpers + +```javascript +import { + getReactionEventPointer, + getReactionAddressPointer +} from 'applesauce-core/helpers'; + +// Get what the reaction is reacting to +const eventPointer = getReactionEventPointer(reactionEvent); +const addressPointer = getReactionAddressPointer(reactionEvent); + +if (eventPointer) { + console.log('Reacted to event:', eventPointer.id); +} +``` + +### Threading Helpers (NIP-10) + +```javascript +import { getNip10References } from 'applesauce-core/helpers'; + +// Parse NIP-10 thread structure (cached) +const refs = getNip10References(event); + +if (refs.root) { + console.log('Root event:', refs.root.e); + console.log('Root address:', refs.root.a); +} + +if (refs.reply) { + console.log('Reply to event:', refs.reply.e); + console.log('Reply to address:', refs.reply.a); +} +``` + +### Filter Helpers + +```javascript +import { + isFilterEqual, + matchFilter, + matchFilters, + mergeFilters +} from 'applesauce-core/helpers'; + +// Compare filters (better than JSON.stringify) +const areEqual = isFilterEqual(filter1, filter2); + +// Check if event matches filter +const matches = matchFilter({ kinds: [1], authors: [pubkey] }, event); + +// Check against multiple filters +const matchesAny = matchFilters([filter1, filter2], event); + +// Merge filters +const combined = mergeFilters(filter1, filter2); +``` + +### Available Helper Categories + +applesauce-core provides helpers for: + +- **Tags**: getTagValue, hasNameValueTag, tag type checks +- **Articles**: getArticleTitle, getArticleSummary, getArticleImage +- **Highlights**: 7+ helpers for highlight extraction +- **Profiles**: getProfileContent, getDisplayName, getProfilePicture +- **Pointers**: parseCoordinate, pointer extraction/creation +- **Reactions**: getReactionEventPointer, getReactionAddressPointer +- **Threading**: getNip10References for NIP-10 threads +- **Comments**: getCommentReplyPointer for NIP-22 +- **Filters**: isFilterEqual, matchFilter, mergeFilters +- **Bookmarks**: bookmark list parsing +- **Emoji**: custom emoji extraction +- **Zaps**: zap parsing and validation +- **Calendars**: calendar event helpers +- **Encryption**: NIP-04, NIP-44 helpers +- **And 40+ more** - explore `node_modules/applesauce-core/dist/helpers/` + +### When NOT to Use useMemo with Helpers + +```javascript +// ❌ DON'T memoize applesauce helper calls +const title = useMemo(() => getArticleTitle(event), [event]); +const summary = useMemo(() => getArticleSummary(event), [event]); + +// ✅ DO call helpers directly +const title = getArticleTitle(event); +const summary = getArticleSummary(event); + +// ❌ DON'T memoize helpers that wrap other helpers +function getRepoName(event) { + return getTagValue(event, 'name'); +} +const name = useMemo(() => getRepoName(event), [event]); + +// ✅ DO call directly (caching propagates) +const name = getRepoName(event); + +// ✅ DO use useMemo for expensive transformations +const sorted = useMemo( + () => events.sort((a, b) => b.created_at - a.created_at), + [events] +); + +// ✅ DO use useMemo for object creation +const options = useMemo( + () => ({ fallbackRelays, timeout: 1000 }), + [fallbackRelays] +); +``` + ## NIP Helpers ### NIP-05 Verification diff --git a/APPLESAUCE_REFACTORING_PLAN.md b/APPLESAUCE_REFACTORING_PLAN.md new file mode 100644 index 0000000..9cc4830 --- /dev/null +++ b/APPLESAUCE_REFACTORING_PLAN.md @@ -0,0 +1,379 @@ +# Applesauce Helpers Refactoring Plan + +## Executive Summary + +After investigating applesauce-core helpers and grimoire's codebase, I've identified several opportunities to leverage applesauce's built-in helpers and caching mechanisms. **Key insight**: Applesauce helpers use internal caching via symbols (`getOrComputeCachedValue`), so we don't need `useMemo` when calling them. + +## Key Findings + +### 1. Applesauce Caching System + +Applesauce helpers cache computed values on event objects using symbols: +```typescript +// From applesauce-core/helpers/cache.d.ts +export declare function getOrComputeCachedValue( + event: any, + symbol: symbol, + compute: () => T +): T; +``` + +**Implication**: When you call helpers like `getArticleTitle(event)`, `getHighlightText(event)`, etc., the result is cached on the event object. Subsequent calls return the cached value instantly. **We don't need useMemo for applesauce helper calls.** + +### 2. Already Using Applesauce Helpers + +Grimoire already imports and uses many applesauce helpers correctly: +- ✅ `getTagValue` - used extensively in nip34-helpers.ts, nip-c0-helpers.ts +- ✅ `getNip10References` - used in nostr-utils.ts +- ✅ `getCommentReplyPointer` - used in nostr-utils.ts +- ✅ Article helpers - `getArticleTitle`, `getArticleSummary` (ArticleRenderer.tsx) +- ✅ Highlight helpers - all 6+ helpers used (HighlightRenderer.tsx, HighlightDetailRenderer.tsx) +- ✅ Code snippet helpers - imported from nip-c0-helpers.ts + +### 3. Applesauce Helpers Available but Not Used + +#### Profile Helpers +```typescript +// applesauce-core/helpers/profile +export function getDisplayName( + metadata: ProfileContent | NostrEvent | undefined, + fallback?: string +): string | undefined; +``` + +**Current grimoire code** (src/lib/nostr-utils.ts:65-76): +```typescript +export function getDisplayName( + pubkey: string, + metadata?: ProfileContent, +): string { + if (metadata?.display_name) return metadata.display_name; + if (metadata?.name) return metadata.name; + return derivePlaceholderName(pubkey); +} +``` + +**Issue**: Grimoire's version requires both `pubkey` and `metadata`, while applesauce only takes `metadata`. Our version adds fallback logic with `derivePlaceholderName`. + +**Recommendation**: Keep grimoire's version - it provides better UX with pubkey-based fallback. + +#### Pointer Helpers +```typescript +// applesauce-core/helpers/pointers +export function getEventPointerFromETag(tag: string[]): EventPointer; +export function getEventPointerFromQTag(tag: string[]): EventPointer; +export function getAddressPointerFromATag(tag: string[]): AddressPointer; +export function getProfilePointerFromPTag(tag: string[]): ProfilePointer; +export function parseCoordinate(a: string): AddressPointerWithoutD | null; +``` + +**Current usage** in ReactionRenderer.tsx:58-66: +```typescript +const addressParts = useMemo(() => { + if (!reactedAddress) return null; + const parts = reactedAddress.split(":"); + return { + kind: parseInt(parts[0], 10), + pubkey: parts[1], + dTag: parts[2], + }; +}, [reactedAddress]); +``` + +**Recommendation**: Replace manual parsing with `parseCoordinate` helper. + +#### Reaction Pointer Helpers +```typescript +// applesauce-core/helpers/reactions +export function getReactionEventPointer(event: NostrEvent): EventPointer | undefined; +export function getReactionAddressPointer(event: NostrEvent): AddressPointer | undefined; +``` + +**Current usage** in ReactionRenderer.tsx: Manual tag extraction and parsing. + +**Recommendation**: Use built-in reaction pointer helpers. + +#### Filter Comparison Helper +```typescript +// applesauce-core/helpers/filter +export function isFilterEqual( + a: FilterWithAnd | FilterWithAnd[], + b: FilterWithAnd | FilterWithAnd[] +): boolean; +``` + +**Current usage** in useStable.ts:55-58: +```typescript +export function useStableFilters(filters: T): T { + return useMemo(() => filters, [JSON.stringify(filters)]); +} +``` + +**Recommendation**: Replace JSON.stringify comparison with `isFilterEqual` for more robust filter comparison. + +### 4. Custom Helpers We Need to Keep + +These are **not** available in applesauce and provide grimoire-specific functionality: + +1. **`getTagValues` (plural)** - src/lib/nostr-utils.ts:59-63 + ```typescript + export function getTagValues(event: NostrEvent, tagName: string): string[] { + return event.tags + .filter((tag) => tag[0] === tagName && tag[1]) + .map((tag) => tag[1]); + } + ``` + **Keep**: Applesauce only has `getTagValue` (singular). We need the plural version. + +2. **`resolveFilterAliases`** - src/lib/nostr-utils.ts:85-156 + - Resolves `$me` and `$contacts` in filters + - Grimoire-specific feature + **Keep**: No applesauce equivalent. + +3. **NIP-34 helpers** - src/lib/nip34-helpers.ts + - Git repository, issue, patch, PR helpers + **Keep**: Grimoire-specific, uses `getTagValue` underneath. + +4. **NIP-C0 helpers** - src/lib/nip-c0-helpers.ts + - Code snippet helpers + **Keep**: Uses `getTagValue` underneath, grimoire-specific. + +5. **Custom event processing** - src/lib/spell-conversion.ts, spellbook-manager.ts + **Keep**: Grimoire-specific functionality. + +## Refactoring Opportunities + +### HIGH PRIORITY: Remove Unnecessary useMemo + +Since applesauce helpers cache internally, **remove useMemo from all applesauce helper calls**: + +#### 1. ArticleRenderer.tsx (lines 17-18) +```typescript +// BEFORE +const title = useMemo(() => getArticleTitle(event), [event]); +const summary = useMemo(() => getArticleSummary(event), [event]); + +// AFTER - helpers cache internally +const title = getArticleTitle(event); +const summary = getArticleSummary(event); +``` + +#### 2. HighlightRenderer.tsx (lines 24-36) + HighlightDetailRenderer.tsx (lines 22-35) +```typescript +// BEFORE +const highlightText = useMemo(() => getHighlightText(event), [event]); +const sourceUrl = useMemo(() => getHighlightSourceUrl(event), [event]); +const comment = useMemo(() => getHighlightComment(event), [event]); +const eventPointer = useMemo(() => getHighlightSourceEventPointer(event), [event]); +const addressPointer = useMemo(() => getHighlightSourceAddressPointer(event), [event]); +const context = useMemo(() => getHighlightContext(event), [event]); + +// AFTER - helpers cache internally +const highlightText = getHighlightText(event); +const sourceUrl = getHighlightSourceUrl(event); +const comment = getHighlightComment(event); +const eventPointer = getHighlightSourceEventPointer(event); +const addressPointer = getHighlightSourceAddressPointer(event); +const context = getHighlightContext(event); +``` + +#### 3. CodeSnippetDetailRenderer.tsx (lines 37-44) +```typescript +// BEFORE +const name = useMemo(() => getCodeName(event), [event]); +const language = useMemo(() => getCodeLanguage(event), [event]); +const extension = useMemo(() => getCodeExtension(event), [event]); +const description = useMemo(() => getCodeDescription(event), [event]); +const runtime = useMemo(() => getCodeRuntime(event), [event]); +const licenses = useMemo(() => getCodeLicenses(event), [event]); +const dependencies = useMemo(() => getCodeDependencies(event), [event]); +const repo = useMemo(() => getCodeRepo(event), [event]); + +// AFTER - our custom helpers use getTagValue which caches +const name = getCodeName(event); +const language = getCodeLanguage(event); +const extension = getCodeExtension(event); +const description = getCodeDescription(event); +const runtime = getCodeRuntime(event); +const licenses = getCodeLicenses(event); +const dependencies = getCodeDependencies(event); +const repo = getCodeRepo(event); +``` + +#### 4. ChatView.tsx (lines 94, 96) +```typescript +// BEFORE +const threadRefs = useMemo(() => getNip10References(event), [event]); +const qTagValue = useMemo(() => getTagValue(event, "q"), [event]); + +// AFTER - helpers cache internally +const threadRefs = getNip10References(event); +const qTagValue = getTagValue(event, "q"); +``` + +#### 5. LiveActivityRenderer.tsx (lines 20-22) +```typescript +// BEFORE - if using applesauce helpers +const activity = useMemo(() => parseLiveActivity(event), [event]); +const status = useMemo(() => getLiveStatus(event), [event]); +const hostPubkey = useMemo(() => getLiveHost(event), [event]); + +// AFTER - check if these use applesauce helpers internally +// If yes, remove useMemo. If no, keep as is. +``` + +**Note**: Check if `parseLiveActivity`, `getLiveStatus`, `getLiveHost` use applesauce helpers or implement caching themselves. + +### MEDIUM PRIORITY: Use Applesauce Pointer Helpers + +#### 1. ReactionRenderer.tsx - Replace manual coordinate parsing +```typescript +// BEFORE (lines 58-66) +const addressParts = useMemo(() => { + if (!reactedAddress) return null; + const parts = reactedAddress.split(":"); + return { + kind: parseInt(parts[0], 10), + pubkey: parts[1], + dTag: parts[2], + }; +}, [reactedAddress]); + +// AFTER - use parseCoordinate helper +import { parseCoordinate } from "applesauce-core/helpers"; + +const addressPointer = reactedAddress ? parseCoordinate(reactedAddress) : null; +// No useMemo needed - parseCoordinate is a simple function +``` + +#### 2. ReactionRenderer.tsx - Use reaction pointer helpers +```typescript +// CURRENT: Manual tag extraction +const reactedEventId = event.tags.find((t) => t[0] === "e")?.[1]; +const reactedAddress = event.tags.find((t) => t[0] === "a")?.[1]; + +// POTENTIAL ALTERNATIVE: Use built-in helpers +import { getReactionEventPointer, getReactionAddressPointer } from "applesauce-core/helpers"; + +const eventPointer = getReactionEventPointer(event); +const addressPointer = getReactionAddressPointer(event); +``` + +**Trade-off**: Current code gets raw values, helpers return typed pointers. May require component changes. **Evaluate if worth it.** + +### MEDIUM PRIORITY: Improve Filter Comparison + +#### useStable.ts - Use isFilterEqual instead of JSON.stringify +```typescript +// BEFORE (lines 55-58) +export function useStableFilters(filters: T): T { + return useMemo(() => filters, [JSON.stringify(filters)]); +} + +// AFTER - use isFilterEqual for comparison +import { isFilterEqual } from "applesauce-core/helpers"; + +export function useStableFilters(filters: T): T { + const prevFiltersRef = useRef(); + + if (!prevFiltersRef.current || !isFilterEqual(prevFiltersRef.current as any, filters as any)) { + prevFiltersRef.current = filters; + } + + return prevFiltersRef.current; +} +``` + +**Benefits**: +- More robust comparison (handles undefined values correctly) +- Avoids JSON serialization overhead +- Supports NIP-ND AND operator (`&` prefix) + +**Note**: May need to handle non-filter types (arrays, objects). + +### LOW PRIORITY: Code Organization + +#### 1. Document applesauce caching in code comments +Add comments to custom helpers that use applesauce helpers: + +```typescript +// nip34-helpers.ts +/** + * Get the repository name from a repository event + * Note: Uses applesauce getTagValue which caches internally + * @param event Repository event (kind 30617) + * @returns Repository name or undefined + */ +export function getRepositoryName(event: NostrEvent): string | undefined { + return getTagValue(event, "name"); +} +``` + +#### 2. Consider consolidating tag extraction +Since we use `getTagValue` extensively, ensure all single-tag extractions use it instead of manual `find()`: + +```typescript +// PREFER +const value = getTagValue(event, "tagName"); + +// AVOID +const value = event.tags.find(t => t[0] === "tagName")?.[1]; +``` + +## Testing Recommendations + +1. **Test helper caching**: Verify applesauce helpers actually cache (call twice, ensure same reference) +2. **Performance testing**: Measure before/after removing useMemo (expect minimal change due to caching) +3. **Filter comparison**: Test `isFilterEqual` edge cases (undefined, empty arrays, NIP-ND AND operator) +4. **Pointer parsing**: Test `parseCoordinate` with various coordinate formats + +## Migration Strategy + +### Phase 1: Remove Unnecessary useMemo (Low Risk) +1. Remove useMemo from applesauce helper calls in kind renderers +2. Test rendering performance +3. Verify no issues +4. Commit + +### Phase 2: Replace Pointer Parsing (Medium Risk) +1. Replace manual coordinate parsing with `parseCoordinate` +2. Update types if needed +3. Test reaction rendering +4. Commit + +### Phase 3: Improve Filter Comparison (Medium Risk) +1. Implement `useStableFilters` with `isFilterEqual` +2. Test filter subscription behavior +3. Verify no unnecessary re-subscriptions +4. Commit + +### Phase 4: Documentation (Low Risk) +1. Update CLAUDE.md with applesauce helper guidance +2. Add code comments documenting caching +3. Update skills if needed + +## Questions to Investigate + +1. **Do all our custom helpers cache?** Check `parseLiveActivity`, `getLiveStatus`, `getEventDisplayTitle`, etc. +2. **Should we create a shared cache util?** For custom helpers that don't use applesauce helpers underneath +3. **Is getTagValues used enough to add to applesauce?** Consider contributing upstream +4. **Filter aliases**: Could `resolveFilterAliases` be contributed to applesauce? + +## Summary of Changes + +| Category | Impact | Files Affected | Effort | +|----------|--------|----------------|--------| +| Remove useMemo from applesauce helpers | Performance (minor), Code clarity (major) | 8+ renderer files | Low | +| Use pointer helpers | Type safety, Code clarity | ReactionRenderer.tsx | Medium | +| Improve filter comparison | Correctness, Performance | useStable.ts | Medium | +| Documentation | Developer experience | CLAUDE.md, skills | Low | + +**Total estimated effort**: 4-6 hours +**Risk level**: Low-Medium +**Expected benefits**: Cleaner code, better alignment with applesauce patterns, easier maintenance + +## References + +- [Applesauce Documentation](https://hzrd149.github.io/applesauce/) +- [Applesauce GitHub](https://github.com/hzrd149/applesauce) +- [Applesauce Core TypeDoc](https://hzrd149.github.io/applesauce/typedoc/modules/applesauce-core.html) diff --git a/CLAUDE.md b/CLAUDE.md index 3088b21..b1af184 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,47 @@ Applesauce uses RxJS observables for reactive data flow: Use hooks like `useProfile()`, `useNostrEvent()`, `useTimeline()` - they handle subscriptions. +### Applesauce Helpers & Caching + +**Critical Performance Insight**: Applesauce helpers cache computed values internally using symbols. **You don't need `useMemo` when calling applesauce helpers.** + +```typescript +// ❌ WRONG - Unnecessary memoization +const title = useMemo(() => getArticleTitle(event), [event]); +const text = useMemo(() => getHighlightText(event), [event]); + +// ✅ CORRECT - Helpers cache internally +const title = getArticleTitle(event); +const text = getHighlightText(event); +``` + +**How it works**: Helpers use `getOrComputeCachedValue(event, symbol, compute)` to cache results on the event object. The first call computes and caches, subsequent calls return the cached value instantly. + +**Available Helpers** (from `applesauce-core/helpers`): +- **Tags**: `getTagValue(event, name)` - get single tag value (searches hidden tags first) +- **Article**: `getArticleTitle`, `getArticleSummary`, `getArticleImage`, `getArticlePublished` +- **Highlight**: `getHighlightText`, `getHighlightSourceUrl`, `getHighlightSourceEventPointer`, `getHighlightSourceAddressPointer`, `getHighlightContext`, `getHighlightComment` +- **Profile**: `getProfileContent(event)`, `getDisplayName(metadata, fallback)` +- **Pointers**: `parseCoordinate(aTag)`, `getEventPointerFromETag`, `getAddressPointerFromATag`, `getProfilePointerFromPTag` +- **Reactions**: `getReactionEventPointer(event)`, `getReactionAddressPointer(event)` +- **Threading**: `getNip10References(event)` - parses NIP-10 thread tags +- **Filters**: `isFilterEqual(a, b)`, `matchFilter(filter, event)`, `mergeFilters(...filters)` +- **And 50+ more** - see `node_modules/applesauce-core/dist/helpers/index.d.ts` + +**Custom Grimoire Helpers** (not in applesauce): +- `getTagValues(event, name)` - plural version to get array of tag values (src/lib/nostr-utils.ts) +- `resolveFilterAliases(filter, pubkey, contacts)` - resolves `$me`/`$contacts` aliases (src/lib/nostr-utils.ts) +- `getDisplayName(pubkey, metadata)` - enhanced version with pubkey fallback (src/lib/nostr-utils.ts) +- NIP-34 git helpers (src/lib/nip34-helpers.ts) - wraps `getTagValue` for repository, issue, patch metadata +- NIP-C0 code snippet helpers (src/lib/nip-c0-helpers.ts) - wraps `getTagValue` for code metadata + +**When to use `useMemo`**: +- ✅ Complex transformations not using applesauce helpers (sorting, filtering, mapping) +- ✅ Creating objects/arrays for dependency tracking (options, configurations) +- ✅ Expensive computations that don't call applesauce helpers +- ❌ Direct calls to applesauce helpers (they cache internally) +- ❌ Grimoire helpers that wrap `getTagValue` (caching propagates) + ## Key Conventions - **Path Alias**: `@/` = `./src/`