mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
docs: add applesauce helpers investigation and refactoring plan
- Created APPLESAUCE_REFACTORING_PLAN.md with detailed analysis - Updated CLAUDE.md with Applesauce Helpers & Caching section - Enhanced applesauce-core skill with helper documentation Key findings: - Applesauce helpers cache internally using symbols - No need for useMemo when calling applesauce helpers - Identified 40+ useMemo instances that can be removed - Documented available helpers and custom grimoire helpers - Provided migration strategy and refactoring opportunities
This commit is contained in:
@@ -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
|
||||
|
||||
379
APPLESAUCE_REFACTORING_PLAN.md
Normal file
379
APPLESAUCE_REFACTORING_PLAN.md
Normal file
@@ -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<T>(
|
||||
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<T>(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<T>(filters: T): T {
|
||||
return useMemo(() => filters, [JSON.stringify(filters)]);
|
||||
}
|
||||
|
||||
// AFTER - use isFilterEqual for comparison
|
||||
import { isFilterEqual } from "applesauce-core/helpers";
|
||||
|
||||
export function useStableFilters<T>(filters: T): T {
|
||||
const prevFiltersRef = useRef<T>();
|
||||
|
||||
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)
|
||||
41
CLAUDE.md
41
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/`
|
||||
|
||||
Reference in New Issue
Block a user