Action Hub
The ActionRunner class is the central orchestrator for running actions in your Nostr application. It combines an event store, event factory, and optional publish method into a unified interface, making it simple to execute actions that read from your local event store and publish new events to the Nostr network.
Creating an Action Hub
Basic Setup
To create an ActionRunner, you need an event store and event factory. Optionally, you can provide a publish method to automatically handle event publishing.
import { ActionRunner } from "applesauce-actions";
import { NostrEvent } from "applesauce-core/helpers/event";
// Create a basic ActionRunner without automatic publishing
const hub = new ActionRunner(eventStore, eventFactory);
// Or create one with automatic publishing
const publish = async (event: NostrEvent, relays?: string[]) => {
console.log("Publishing event:", event.kind);
await relayPool.publish(relays || defaultRelays, event);
};
const hub = new ActionRunner(eventStore, eventFactory, publish);With Custom Publishing Logic
You can provide sophisticated publishing logic when creating your ActionRunner:
const publish = async (event: NostrEvent, relays?: string[]) => {
// Log the event
console.log("Publishing", event);
// Publish to relays (use provided relays or fallback to defaults)
await app.relayPool.publish(relays || app.defaultRelays, event);
// Save to local backup
await localBackup.save(event);
// Notify UI of new event
eventBus.emit("eventPublished", event);
};
const hub = new ActionRunner(eventStore, eventFactory, publish);INFO
For performance reasons, it's recommended to create only one ActionRunner instance for your entire application and reuse it across all action executions.
Configuration Options
Save to Store
By default, the ActionRunner will automatically save all events created by actions to your event store. You can disable this behavior:
const hub = new ActionRunner(eventStore, eventFactory, publish);
hub.saveToStore = false; // Disable automatic saving to event storeRunning Actions
The ActionRunner provides two primary methods for executing actions: .run() for fire-and-forget execution with automatic publishing, and .exec() for fine-grained control over event handling.
Using .run() - Automatic Publishing
The ActionRunner.run method executes an action and automatically publishes all generated events using the publish method provided during ActionRunner creation.
Actions can specify which relays to publish to by passing a relays array as the second argument to the publish function in their context. If no relays are specified, the publish method will use its default behavior (often determined by the user's outboxes or default relays).
WARNING
ActionRunner.run() will throw an error if no publish method was provided when creating the ActionRunner.
import { FollowUser, NewContacts, UnfollowUser } from "applesauce-actions/actions";
// Create a new contact list (throws if one already exists)
try {
await hub.run(NewContacts);
console.log("Contact list created successfully");
} catch (err) {
console.error("Failed to create contact list:", err.message);
}
// Follow a user - events are automatically published
await hub.run(FollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
// Unfollow a user
await hub.run(UnfollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");Using .exec() - Manual Event Handling
The ActionRunner.exec method executes an action and returns an RxJS Observable of events, giving you complete control over how events are handled and published.
Using RxJS forEach for Simple Cases
The RxJS Observable.forEach method provides a clean way to handle all events with a single function:
import { FollowUser, NewContacts } from "applesauce-actions/actions";
// Custom publishing logic for this specific action
const customPublish = async (event: NostrEvent) => {
// Publish to specific relays
await relayPool.publish(["wss://relay.damus.io", "wss://nos.lol"], event);
// Save to local database with custom metadata
await localDatabase.saveContactListUpdate(event, { source: "user_action" });
};
// Execute action and handle each event
await hub.exec(NewContacts).forEach(customPublish);
// Follow user with custom handling
await hub.exec(FollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d").forEach(customPublish);Using RxJS Subscriptions for Advanced Control
For more complex scenarios, you can manually subscribe to the observable:
import { tap, catchError, finalize } from "rxjs";
import { EMPTY } from "rxjs";
const subscription = hub
.exec(FollowUser, userPubkey)
.pipe(
tap((event) => console.log("Generated event:", event.kind)),
catchError((err) => {
console.error("Action failed:", err);
return EMPTY; // Handle errors gracefully
}),
finalize(() => console.log("Action completed")),
)
.subscribe({
next: async (event) => {
try {
await customPublish(event);
console.log("Event published successfully");
} catch (err) {
console.error("Failed to publish event:", err);
}
},
complete: () => {
console.log("All events processed");
subscription.unsubscribe();
},
error: (err) => {
console.error("Observable error:", err);
subscription.unsubscribe();
},
});Collecting Events Before Publishing
You can collect all events from an action before publishing them:
import { toArray, lastValueFrom } from "rxjs";
// Collect all events into an array
const events = await lastValueFrom(hub.exec(NewContacts).pipe(toArray()));
console.log(`Action generated ${events.length} events`);
// Publish them in a specific order or with delays
for (const event of events) {
await relayPool.publish(defaultRelays, event);
await delay(100); // Small delay between publishes
}Error Handling
Action Validation Errors
Actions will throw errors for various validation failures:
try {
await hub.run(NewContacts);
} catch (err) {
if (err.message.includes("contact list already exists")) {
console.log("User already has a contact list");
} else {
console.error("Unexpected error:", err);
}
}Publishing Errors
When using .exec(), you can handle publishing errors independently:
await hub.exec(FollowUser, userPubkey).forEach(async (event) => {
try {
await relayPool.publish(defaultRelays, event);
} catch (publishError) {
console.error("Failed to publish event:", publishError);
// Could retry, save for later, or notify user
await saveForRetry(event);
}
});Best Practices
Single ActionRunner Instance
Create one ActionRunner instance per application and reuse it:
// app.ts
export const actionHub = new ActionRunner(eventStore, eventFactory, publish);
// other-file.ts
import { actionHub } from "./app.js";
await actionHub.run(FollowUser, pubkey);Conditional Event Store Saving
For actions that generate many events, you might want to control when events are saved to the store:
import { toArray, lastValueFrom } from "rxjs";
const hub = new ActionRunner(eventStore, eventFactory, publish);
// Disable automatic saving for bulk operations
hub.saveToStore = false;
// Run bulk action
const events = await lastValueFrom(hub.exec(BulkFollowUsers, userList).pipe(toArray()));
// Manually save only successful events
for (const event of events) {
try {
await relayPool.publish(defaultRelays, event);
await eventStore.add(event); // Save only after successful publish
} catch (err) {
console.error("Skipping failed event:", err);
}
}
// Re-enable automatic saving
hub.saveToStore = true;