Casting System
The casting system transforms raw Nostr events into easy-to-use classes with both synchronous properties and reactive observable interfaces. This makes it simple to work with Nostr data in web UIs, where you need to read event data and subscribe to updates.
Overview
The casting system provides two main functions:
castEvent- Converts a Nostr event into a typed class instancecastUser- Converts a pubkey or profile pointer into aUserinstance
All cast classes extend EventCast and provide:
- Synchronous properties - Direct access to event data (e.g.,
note.id,note.createdAt) - Observable properties - Reactive streams that update when data changes (e.g.,
note.author.profile$,user.outboxes$)
Basic Usage
Casting Events
Use castEvent to convert a Nostr event into a typed class:
import { castEvent, Note } from "applesauce-common/casts";
import { EventStore } from "applesauce-core/event-store";
const eventStore = new EventStore();
const event: NostrEvent = /* ... */;
// Cast the event to a Note
const note = castEvent(event, Note, eventStore);
// Access synchronous properties
console.log(note.id); // Event ID
console.log(note.createdAt); // Date object
console.log(note.isReply); // BooleanCasting Users
Use castUser to create a User instance from a pubkey or event:
import { castUser } from "applesauce-common/casts";
// From a pubkey string
const user = castUser("abc123...", eventStore);
// From a Nostr event (extracts the pubkey)
const user = castUser(event, eventStore);
// From a profile pointer
const user = castUser({ pubkey: "abc123...", relays: [...] }, eventStore);Cast Classes
All cast classes extend EventCast and provide a consistent interface:
Base Properties
All cast classes inherit these base properties from EventCast:
const note = castEvent(event, Note, eventStore);
// Base properties available on all cast classes
note.id; // string - The event ID (32-byte hex string)
note.uid; // string - Unique identifier for the event
note.createdAt; // Date - Creation timestamp as a Date object
note.author; // User - The User instance that authored this event
note.seen; // Set<string> - Set of relay URLs where this event was seen
note.event; // NostrEvent - The raw event object
note.store; // CastRefEventStore - The event store instanceProperty Details:
id- The event's unique identifier (32-byte hex string)uid- A unique identifier that may differ fromidfor parameterized replaceable eventscreatedAt- The event's creation timestamp converted to a JavaScriptDateobjectauthor- AUserinstance created from the event'spubkey. This is cached, so multiple accesses return the same instanceseen- ASetof relay URLs where this event was observed. Useful for knowing which relays have this eventevent- The rawNostrEventobject. Access this when you need the original event datastore- The event store instance used for loading related events and data
Synchronous Properties
These are regular getters that return values directly:
const note = castEvent(event, Note, eventStore);
// Direct property access
note.id; // string - Event ID
note.uid; // string - Unique identifier
note.createdAt; // Date - Creation timestamp
note.author; // User - Author instance
note.isReply; // boolean - Whether this is a reply
note.references; // object - NIP-10 referencesObservable Properties
Properties ending with $ return chainable observables that emit values and update reactively:
// Observable properties return ChainableObservable instances
const profile$ = note.author.profile$; // Observable<Profile | undefined>
const outboxes$ = note.author.outboxes$; // Observable<string[] | undefined>
const replies$ = note.replies$; // Observable<Note[]>Chainable Observables
The casting system uses chainable observables to enable deep property access through observable chains. This allows you to subscribe to nested properties without manually chaining RxJS operators.
Deep Property Access
Chainable observables let you access nested properties using dot notation:
// Access nested observable properties
// Note: author is a synchronous property, so we access it directly
const displayName$ = note.author.profile$.displayName;
// Observable<string | undefined>
const outboxes$ = note.author.outboxes$;
// Observable<string[] | undefined>The $ suffix indicates an observable property. When you access a property on a chainable observable:
- Properties ending with
$- Returns the inner observable value (extracts fromObservable<T>) - Regular properties - Returns an observable of that property's value
Using in React
The use$ hook from applesauce-react makes it easy to subscribe to chainable observables:
import { use$ } from "applesauce-react/hooks";
function NoteItem({ note }: { note: Note }) {
// Subscribe to nested observable properties
const profile = use$(note.author.profile$);
const replies = use$(note.replies$);
const zaps = use$(note.zaps$);
return (
<div>
<h3>{profile?.displayName || note.author.npub}</h3>
<p>{note.event.content}</p>
{replies && replies.length > 0 && (
<div>Replies: {replies.length}</div>
)}
</div>
);
}Async Code with $first
For async code (outside React), use the $first method to wait for the first value from a deep subscription:
// Wait for the first value with a timeout
const displayName = await note.author.profile$.displayName.$first(5000);
// Returns: string | undefined (or throws if timeout)
// With a fallback value
const displayName = await note.author.profile$.displayName.$first(5000, "Anonymous");
// Returns: string (never undefined)The $first method signature:
$first(timeout?: number): Promise<NonNullable<T>>;
$first<V>(timeout?: number, fallback?: V): Promise<NonNullable<T> | V>;Parameters:
timeout- Maximum time to wait in milliseconds (default: 10,000ms)fallback- Optional value to return if timeout occurs
Example:
// Get user's outboxes with timeout
const outboxes = await user.outboxes$.$first(5000, []);
if (outboxes.length === 0) {
console.log("No outboxes available");
}
// Get profile display name
const displayName = await note.author.profile$.displayName.$first(3000, "Unknown");$last Method
Similar to $first, but waits for the last value emitted before the observable completes:
INFO
Most observables will not complete automatically, which means in almost all cases this will wait for the full timeout.
const lastValue = await observable.$last(5000);
const lastValue = await observable.$last(5000, fallback);Casting in Observable Streams
Use castEventStream and castTimelineStream to cast events within RxJS pipelines:
Single Event
import { castEventStream } from "applesauce-common/observable";
const note$ = eventStore.event(eventPointer).pipe(castEventStream(Note, eventStore));Multiple Events
import { castTimelineStream } from "applesauce-common/observable";
const notes$ = eventStore.timeline([{ kinds: [1] }]).pipe(castTimelineStream(Note, eventStore));Available Cast Classes
The casting system provides classes for common Nostr event types:
Note- Kind 1 (Short text notes)Profile- Kind 0 (User metadata)User- User instance (not an event, but provides user-related observables)Reaction- Kind 7 (Reactions)Zap- Kind 9735 (Lightning zaps)Comment- NIP-22 commentsShare- Kind 6 (Reposts)Article- Kind 30023 (Long-form articles)Mutes- Kind 10000 (Mute lists)BookmarksList- Kind 10003 (Bookmarks)Stream- Kind 30311 (Live streams)- And more...
User Class
The User class is special - it's not an event cast, but provides a rich interface for working with users:
Synchronous Properties
const user = castUser(pubkey, eventStore);
user.pubkey; // string - User's public key
user.npub; // string - NIP-19 encoded npub
user.pointer; // ProfilePointer - Profile pointer with relay hints
user.nprofile; // string - NIP-19 encoded nprofileObservable Properties
// Profile metadata
user.profile$; // Observable<Profile | undefined>
// Contact list
user.contacts$; // Observable<User[]>
// Mute list
user.mutes$; // Observable<Mutes | undefined>
// Relay lists
user.outboxes$; // Observable<string[] | undefined>
user.inboxes$; // Observable<string[] | undefined>
user.favoriteRelays$; // Observable<FavoriteRelays | undefined>
user.searchRelays$; // Observable<SearchRelays | undefined>
user.blockedRelays$; // Observable<BlockedRelays | undefined>
// Bookmarks
user.bookmarks$; // Observable<BookmarksList | undefined>
// Groups (NIP-29)
user.groups$; // Observable<GroupsList | undefined>
// Live stream
user.live$; // Observable<Stream | undefined>Example: Contact Manager
Using castUser to manage contacts:
import { castUser, User } from "applesauce-common/casts";
import { use$ } from "applesauce-react/hooks";
function ContactManager({ user }: { user: User }) {
// Subscribe to user's contacts
const contacts = use$(user.contacts$);
const outboxes = use$(user.outboxes$);
return (
<div>
<h1>Contacts ({contacts?.length || 0})</h1>
{contacts?.map(contact => (
<ContactCard key={contact.pubkey} user={contact} />
))}
</div>
);
}
// Create a user instance
const user = castUser(pubkey, eventStore);Example: Async Code
Using $first in async functions:
async function publishNote(content: string, user: User) {
// Wait for outboxes with 5 second timeout
const outboxes = await user.outboxes$.$first(5000);
if (!outboxes || outboxes.length === 0) {
throw new Error("No outbox relays available");
}
// Create and publish event
const event = await createNote(content);
await pool.publish(outboxes, event);
}
async function getAuthorDisplayName(note: Note): Promise<string> {
// Get display name with fallback
return await note.author.profile$.displayName.$first(3000, "Anonymous");
}Caching
Cast instances are cached to avoid creating duplicate instances:
- Events - Each event can only be cast once per class type. Subsequent calls return the cached instance.
- Users - User instances are cached globally by pubkey. Creating a user with the same pubkey returns the existing instance.
This ensures referential equality and prevents unnecessary object creation.
Best Practices
Use observables for reactive data - Properties that may change over time should use observables (ending with
$)Use synchronous properties for static data - Properties that don't change (like
id,createdAt) are available directlyChain observables for nested data - Use chainable observables to access nested properties:
note.author.profile$.displayNameHandle undefined values - Observable properties may emit
undefinedwhile loading. Always check for undefined valuesUse
$firstin async code - When you need to wait for a value in async functions, use$firstwith appropriate timeoutsCache user instances - The
castUserfunction automatically caches instances, so you can safely call it multiple times with the same pubkey
Type Safety
All cast classes are fully typed. TypeScript will infer types for:
- Synchronous properties
- Observable properties (as
ChainableObservable<T>) - Chained observable properties
For better type inference in complex chains, you may need to explicitly type the result:
const displayName$: Observable<string | undefined> = note.author.profile$.displayName;