Agent skill
applesauce-core
This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/applesauce-core-purrgrammer-grimoire
SKILL.md
applesauce-core Skill
This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients.
When to Use This Skill
Use this skill when:
- Building reactive Nostr applications
- Managing event stores and caches
- Working with observable patterns for Nostr
- Implementing real-time updates
- Building timeline and feed views
- Managing replaceable events
- Working with profiles and metadata
- Creating efficient Nostr queries
Core Concepts
applesauce-core Overview
applesauce-core provides:
- Event stores - Reactive event caching and management
- Queries - Declarative event querying patterns
- Observables - RxJS-based reactive patterns
- Profile helpers - Profile metadata management
- Timeline utilities - Feed and timeline building
- NIP helpers - NIP-specific utilities
Installation
npm install applesauce-core
Basic Architecture
applesauce-core is built on reactive principles:
- Events are stored in reactive stores
- Queries return observables that update when new events arrive
- Components subscribe to observables for real-time updates
Event Store
Creating an Event Store
import { EventStore } from 'applesauce-core';
// Create event store
const eventStore = new EventStore();
// Add events
eventStore.add(event1);
eventStore.add(event2);
// Add multiple events
eventStore.addMany([event1, event2, event3]);
// Check if event exists
const exists = eventStore.has(eventId);
// Get event by ID
const event = eventStore.get(eventId);
// Remove event
eventStore.remove(eventId);
// Clear all events
eventStore.clear();
Event Store Queries
// Get all events
const allEvents = eventStore.getAll();
// Get events by filter
const filtered = eventStore.filter({
kinds: [1],
authors: [pubkey]
});
// Get events by author
const authorEvents = eventStore.getByAuthor(pubkey);
// Get events by kind
const textNotes = eventStore.getByKind(1);
Replaceable Events
applesauce-core handles replaceable events automatically:
// For kind 0 (profile), only latest is kept
eventStore.add(profileEvent1); // stored
eventStore.add(profileEvent2); // replaces if newer
// For parameterized replaceable (30000-39999)
eventStore.add(articleEvent); // keyed by author + kind + d-tag
// Get replaceable event
const profile = eventStore.getReplaceable(0, pubkey);
const article = eventStore.getReplaceable(30023, pubkey, 'article-slug');
Queries
Query Patterns
import { createQuery } from 'applesauce-core';
// Create a query
const query = createQuery(eventStore, {
kinds: [1],
limit: 50
});
// Subscribe to query results
query.subscribe(events => {
console.log('Current events:', events);
});
// Query updates automatically when new events added
eventStore.add(newEvent); // Subscribers notified
Timeline Query
import { TimelineQuery } from 'applesauce-core';
// Create timeline for user's notes
const timeline = new TimelineQuery(eventStore, {
kinds: [1],
authors: [userPubkey]
});
// Get observable of timeline
const timeline$ = timeline.events$;
// Subscribe
timeline$.subscribe(events => {
// Events sorted by created_at, newest first
renderTimeline(events);
});
Profile Query
import { ProfileQuery } from 'applesauce-core';
// Query profile metadata
const profileQuery = new ProfileQuery(eventStore, pubkey);
// Get observable
const profile$ = profileQuery.profile$;
profile$.subscribe(profile => {
if (profile) {
console.log('Name:', profile.name);
console.log('Picture:', profile.picture);
}
});
Observables
Working with RxJS
applesauce-core uses RxJS observables:
import { map, filter, distinctUntilChanged } from 'rxjs/operators';
// Transform query results
const names$ = profileQuery.profile$.pipe(
filter(profile => profile !== null),
map(profile => profile.name),
distinctUntilChanged()
);
// Combine multiple observables
import { combineLatest } from 'rxjs';
const combined$ = combineLatest([
timeline$,
profile$
]).pipe(
map(([events, profile]) => ({
events,
authorName: profile?.name
}))
);
Creating Custom Observables
import { Observable } from 'rxjs';
function createEventObservable(store, filter) {
return new Observable(subscriber => {
// Initial emit
subscriber.next(store.filter(filter));
// Subscribe to store changes
const unsubscribe = store.onChange(() => {
subscriber.next(store.filter(filter));
});
// Cleanup
return () => unsubscribe();
});
}
Profile Helpers
Profile Metadata
import { parseProfile, ProfileContent } from 'applesauce-core';
// Parse kind 0 content
const profileEvent = await getProfileEvent(pubkey);
const profile = parseProfile(profileEvent);
// Profile fields
console.log(profile.name); // Display name
console.log(profile.about); // Bio
console.log(profile.picture); // Avatar URL
console.log(profile.banner); // Banner image URL
console.log(profile.nip05); // NIP-05 identifier
console.log(profile.lud16); // Lightning address
console.log(profile.website); // Website URL
Profile Store
import { ProfileStore } from 'applesauce-core';
const profileStore = new ProfileStore(eventStore);
// Get profile observable
const profile$ = profileStore.getProfile(pubkey);
// Get multiple profiles
const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]);
// Request profile load (triggers fetch if not cached)
profileStore.requestProfile(pubkey);
Timeline Utilities
Building Feeds
import { Timeline } from 'applesauce-core';
// Create timeline
const timeline = new Timeline(eventStore);
// Add filter
timeline.setFilter({
kinds: [1, 6],
authors: followedPubkeys
});
// Get events observable
const events$ = timeline.events$;
// Load more (pagination)
timeline.loadMore(50);
// Refresh (get latest)
timeline.refresh();
Thread Building
import { ThreadBuilder } from 'applesauce-core';
// Build thread from root event
const thread = new ThreadBuilder(eventStore, rootEventId);
// Get thread observable
const thread$ = thread.thread$;
thread$.subscribe(threadData => {
console.log('Root:', threadData.root);
console.log('Replies:', threadData.replies);
console.log('Reply count:', threadData.replyCount);
});
Reactions and Zaps
import { ReactionStore, ZapStore } from 'applesauce-core';
// Reactions
const reactionStore = new ReactionStore(eventStore);
const reactions$ = reactionStore.getReactions(eventId);
reactions$.subscribe(reactions => {
console.log('Likes:', reactions.likes);
console.log('Custom:', reactions.custom);
});
// Zaps
const zapStore = new ZapStore(eventStore);
const zaps$ = zapStore.getZaps(eventId);
zaps$.subscribe(zaps => {
console.log('Total sats:', zaps.totalAmount);
console.log('Zap count:', zaps.count);
});
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.
// ❌ 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
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
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:
function getTagValues(event, tagName) {
return event.tags
.filter(tag => tag[0] === tagName && tag[1])
.map(tag => tag[1]);
}
Article Helpers (NIP-23)
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)
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
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)
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
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)
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
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
// ❌ 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
import { verifyNip05 } from 'applesauce-core';
// Verify NIP-05
const result = await verifyNip05('alice@example.com', expectedPubkey);
if (result.valid) {
console.log('NIP-05 verified');
} else {
console.log('Verification failed:', result.error);
}
NIP-10 Reply Parsing
import { parseReplyTags } from 'applesauce-core';
// Parse reply structure
const parsed = parseReplyTags(event);
console.log('Root event:', parsed.root);
console.log('Reply to:', parsed.reply);
console.log('Mentions:', parsed.mentions);
NIP-65 Relay Lists
import { parseRelayList } from 'applesauce-core';
// Parse relay list event (kind 10002)
const relays = parseRelayList(relayListEvent);
console.log('Read relays:', relays.read);
console.log('Write relays:', relays.write);
Integration with nostr-tools
Using with SimplePool
import { SimplePool } from 'nostr-tools';
import { EventStore } from 'applesauce-core';
const pool = new SimplePool();
const eventStore = new EventStore();
// Load events into store
pool.subscribeMany(relays, [filter], {
onevent(event) {
eventStore.add(event);
}
});
// Query store reactively
const timeline$ = createTimelineQuery(eventStore, filter);
Publishing Events
import { finalizeEvent } from 'nostr-tools';
// Create event
const event = finalizeEvent({
kind: 1,
content: 'Hello!',
created_at: Math.floor(Date.now() / 1000),
tags: []
}, secretKey);
// Add to local store immediately (optimistic update)
eventStore.add(event);
// Publish to relays
await pool.publish(relays, event);
Svelte Integration
Using in Svelte Components
<script>
import { onMount, onDestroy } from 'svelte';
import { EventStore, TimelineQuery } from 'applesauce-core';
export let pubkey;
const eventStore = new EventStore();
let events = [];
let subscription;
onMount(() => {
const timeline = new TimelineQuery(eventStore, {
kinds: [1],
authors: [pubkey]
});
subscription = timeline.events$.subscribe(e => {
events = e;
});
});
onDestroy(() => {
subscription?.unsubscribe();
});
</script>
{#each events as event}
<div class="event">
{event.content}
</div>
{/each}
Svelte Store Adapter
import { readable } from 'svelte/store';
// Convert RxJS observable to Svelte store
function fromObservable(observable, initialValue) {
return readable(initialValue, set => {
const subscription = observable.subscribe(set);
return () => subscription.unsubscribe();
});
}
// Usage
const events$ = timeline.events$;
const eventsStore = fromObservable(events$, []);
<script>
import { eventsStore } from './stores.js';
</script>
{#each $eventsStore as event}
<div>{event.content}</div>
{/each}
Best Practices
Store Management
- Single store instance - Use one EventStore per app
- Clear stale data - Implement cache limits
- Handle replaceable events - Let store manage deduplication
- Unsubscribe - Clean up subscriptions on component destroy
Query Optimization
- Use specific filters - Narrow queries perform better
- Limit results - Use limit for initial loads
- Cache queries - Reuse query instances
- Debounce updates - Throttle rapid changes
Memory Management
- Limit store size - Implement LRU or time-based eviction
- Clean up observables - Unsubscribe when done
- Use weak references - For profile caches
- Paginate large feeds - Don't load everything at once
Reactive Patterns
- Prefer observables - Over imperative queries
- Use operators - Transform data with RxJS
- Combine streams - For complex views
- Handle loading states - Show placeholders
Common Patterns
Event Deduplication
// EventStore handles deduplication automatically
eventStore.add(event1);
eventStore.add(event1); // No duplicate
// For manual deduplication
const seen = new Set();
events.filter(e => {
if (seen.has(e.id)) return false;
seen.add(e.id);
return true;
});
Optimistic Updates
async function publishNote(content) {
// Create event
const event = await createEvent(content);
// Add to store immediately (optimistic)
eventStore.add(event);
try {
// Publish to relays
await pool.publish(relays, event);
} catch (error) {
// Remove on failure
eventStore.remove(event.id);
throw error;
}
}
Loading States
import { BehaviorSubject, combineLatest } from 'rxjs';
const loading$ = new BehaviorSubject(true);
const events$ = timeline.events$;
const state$ = combineLatest([loading$, events$]).pipe(
map(([loading, events]) => ({
loading,
events,
empty: !loading && events.length === 0
}))
);
// Start loading
loading$.next(true);
await loadEvents();
loading$.next(false);
Infinite Scroll
function createInfiniteScroll(timeline, pageSize = 50) {
let loading = false;
async function loadMore() {
if (loading) return;
loading = true;
await timeline.loadMore(pageSize);
loading = false;
}
function onScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target;
if (scrollHeight - scrollTop <= clientHeight * 1.5) {
loadMore();
}
}
return { loadMore, onScroll };
}
Troubleshooting
Common Issues
Events not updating:
- Check subscription is active
- Verify events are being added to store
- Ensure filter matches events
Memory growing:
- Implement store size limits
- Clean up subscriptions
- Use weak references where appropriate
Slow queries:
- Add indexes for common queries
- Use more specific filters
- Implement pagination
Stale data:
- Implement refresh mechanisms
- Set up real-time subscriptions
- Handle replaceable event updates
References
- applesauce GitHub: https://github.com/hzrd149/applesauce
- RxJS Documentation: https://rxjs.dev
- nostr-tools: https://github.com/nbd-wtf/nostr-tools
- Nostr Protocol: https://github.com/nostr-protocol/nostr
Related Skills
- nostr-tools - Lower-level Nostr operations
- applesauce-signers - Event signing abstractions
- svelte - Building reactive UIs
- nostr - Nostr protocol fundamentals
Didn't find tool you were looking for?