- 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
13 KiB
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:
// 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
// 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):
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
// 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:
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
// 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
// applesauce-core/helpers/filter
export function isFilterEqual(
a: FilterWithAnd | FilterWithAnd[],
b: FilterWithAnd | FilterWithAnd[]
): boolean;
Current usage in useStable.ts:55-58:
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:
-
getTagValues(plural) - src/lib/nostr-utils.ts:59-63export 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. -
resolveFilterAliases- src/lib/nostr-utils.ts:85-156- Resolves
$meand$contactsin filters - Grimoire-specific feature Keep: No applesauce equivalent.
- Resolves
-
NIP-34 helpers - src/lib/nip34-helpers.ts
- Git repository, issue, patch, PR helpers
Keep: Grimoire-specific, uses
getTagValueunderneath.
- Git repository, issue, patch, PR helpers
Keep: Grimoire-specific, uses
-
NIP-C0 helpers - src/lib/nip-c0-helpers.ts
- Code snippet helpers
Keep: Uses
getTagValueunderneath, grimoire-specific.
- Code snippet helpers
Keep: Uses
-
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)
// 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)
// 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)
// 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)
// 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)
// 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
// 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
// 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
// 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:
// 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():
// PREFER
const value = getTagValue(event, "tagName");
// AVOID
const value = event.tags.find(t => t[0] === "tagName")?.[1];
Testing Recommendations
- Test helper caching: Verify applesauce helpers actually cache (call twice, ensure same reference)
- Performance testing: Measure before/after removing useMemo (expect minimal change due to caching)
- Filter comparison: Test
isFilterEqualedge cases (undefined, empty arrays, NIP-ND AND operator) - Pointer parsing: Test
parseCoordinatewith various coordinate formats
Migration Strategy
Phase 1: Remove Unnecessary useMemo (Low Risk)
- Remove useMemo from applesauce helper calls in kind renderers
- Test rendering performance
- Verify no issues
- Commit
Phase 2: Replace Pointer Parsing (Medium Risk)
- Replace manual coordinate parsing with
parseCoordinate - Update types if needed
- Test reaction rendering
- Commit
Phase 3: Improve Filter Comparison (Medium Risk)
- Implement
useStableFilterswithisFilterEqual - Test filter subscription behavior
- Verify no unnecessary re-subscriptions
- Commit
Phase 4: Documentation (Low Risk)
- Update CLAUDE.md with applesauce helper guidance
- Add code comments documenting caching
- Update skills if needed
Questions to Investigate
- Do all our custom helpers cache? Check
parseLiveActivity,getLiveStatus,getEventDisplayTitle, etc. - Should we create a shared cache util? For custom helpers that don't use applesauce helpers underneath
- Is getTagValues used enough to add to applesauce? Consider contributing upstream
- Filter aliases: Could
resolveFilterAliasesbe 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