xiaohongshu
xiaohongshu
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.
Full skill instructions
This skill provides comprehensive knowledge and patterns for working with applesauce-core v5, a library that provides reactive utilities and patterns for building Nostr clients.
Note: applesauce v5 introduced package reorganization:
applesauce-coreapplesauce-commonEventFactory moved from applesauce-factory to applesauce-core/event-factoryActionHub renamed to ActionRunner in applesauce-actionsUse this skill when:
applesauce-core provides:
npm install applesauce-core
applesauce-core is built on reactive principles:
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();
// 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);
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');
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
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);
});
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);
}
});
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
}))
);
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();
});
}
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
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);
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();
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);
});
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);
});
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);
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
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]);
}
Note: Article helpers moved to applesauce-common in v5.
import {
getArticleTitle,
getArticleSummary,
getArticleImage,
getArticlePublished,
isValidArticle
} from 'applesauce-common/helpers/article';
// 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');
}
Note: Highlight helpers moved to applesauce-common in v5.
import {
getHighlightText,
getHighlightSourceUrl,
getHighlightSourceEventPointer,
getHighlightSourceAddressPointer,
getHighlightContext,
getHighlightComment,
getHighlightAttributions
} from 'applesauce-common/helpers/highlight';
// 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);
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);
}
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);
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);
}
Note: Threading helpers moved to applesauce-common in v5.
import { getNip10References } from 'applesauce-common/helpers/threading';
// 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);
}
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);
applesauce-core provides helpers for:
node_modules/applesauce-core/dist/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]
);
The EventFactory class (moved to applesauce-core/event-factory in v5) provides a unified API for creating and signing Nostr events using blueprints.
import { EventFactory } from 'applesauce-core/event-factory';
// Create factory
const factory = new EventFactory();
// Set signer (required for signing)
factory.setSigner(signer);
// Optional context
factory.setClient({ name: 'grimoire', address: { pubkey, identifier } });
Blueprints are templates for event creation. See the applesauce-common skill for detailed blueprint documentation.
import { NoteBlueprint, NoteReplyBlueprint } from 'applesauce-common/blueprints';
// Create a note
const draft = await factory.create(
NoteBlueprint,
'Hello #nostr!',
{ emojis: [{ shortcode: 'wave', url: 'https://...' }] }
);
// Sign the draft
const event = await factory.sign(draft);
// Or combine: create and sign
const event = await factory.sign(
await factory.create(NoteBlueprint, content, options)
);
// Create from blueprint
const draft = await factory.create(Blueprint, ...args);
// Sign event template
const event = await factory.sign(draft);
// Stamp (add pubkey without signing)
const unsigned = await factory.stamp(draft);
// Build with operations
const draft = await factory.build(template, ...operations);
// Modify existing event
const modified = await factory.modify(event, ...operations);
EventFactory integrates seamlessly with the action system:
import accountManager from '@/services/accounts';
import { EventFactory } from 'applesauce-core/event-factory';
import { NoteBlueprint } from 'applesauce-common/blueprints';
const account = accountManager.active;
const signer = account.signer;
const factory = new EventFactory();
factory.setSigner(signer);
// Create and sign
const draft = await factory.create(NoteBlueprint, content, options);
const event = await factory.sign(draft);
// Publish
await pool.publish(relays, event);
Creating with custom tags:
// Let blueprint handle automatic extraction
const draft = await factory.create(NoteBlueprint, content, { emojis });
// Add custom tags afterward
draft.tags.push(['client', 'grimoire', '31990:...']);
draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`]);
// Sign
const event = await factory.sign(draft);
Error handling:
try {
const draft = await factory.create(NoteBlueprint, content, options);
const event = await factory.sign(draft);
await pool.publish(relays, event);
} catch (error) {
if (error.message.includes('User rejected')) {
// Handle rejection
} else {
// Handle other errors
}
}
Optimistic updates:
// Create and sign
const event = await factory.sign(
await factory.create(NoteBlueprint, content, options)
);
// Add to local store immediately
eventStore.add(event);
// Publish in background
pool.publish(relays, event).catch(err => {
// Remove from store on failure
eventStore.remove(event.id);
});
import { verifyNip05 } from 'applesauce-core';
// Verify NIP-05
const result = await verifyNip05('[email protected]', expectedPubkey);
if (result.valid) {
console.log('NIP-05 verified');
} else {
console.log('Verification failed:', result.error);
}
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);
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);
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);
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);
<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}
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}
// 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;
});
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;
}
}
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);
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 };
}
Events not updating:
Memory growing:
Slow queries:
Stale data:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience