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
applesauce-common ships cast classes for most common event kinds (Note, Profile, Article, Zap, Reaction, Comment, lists, streams, and more). See Casting Events for the full reference, or the feature pages such as Notes & Threads, Articles, and Zaps for how each one is used.
User Class
The User class is not an event cast. It wraps a pubkey and exposes observables for the user's replaceable events:
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 nprofile
user.profile$; // Observable<Profile | undefined>See Casting Users for every observable, and Profiles, Follows, and Mailboxes & Outbox for how they are used.
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;