Action Hub
The ActionHub 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 ActionHub, you need an event store and event factory. Optionally, you can provide a publish method to automatically handle event publishing.
import { ActionHub } from "applesauce-actions";
import { NostrEvent } from "nostr-tools";
// Create a basic ActionHub without automatic publishing
const hub = new ActionHub(eventStore, eventFactory);
// Or create one with automatic publishing
const publish = async (event: NostrEvent) => {
console.log("Publishing event:", event.kind);
await relayPool.publish(event, defaultRelays);
};
const hub = new ActionHub(eventStore, eventFactory, publish);
With Custom Publishing Logic
You can provide sophisticated publishing logic when creating your ActionHub:
const publish = async (event: NostrEvent) => {
// Log the event
console.log("Publishing", event);
// Publish to relays
await app.relayPool.publish(event, app.defaultRelays);
// Save to local backup
await localBackup.save(event);
// Notify UI of new event
eventBus.emit("eventPublished", event);
};
const hub = new ActionHub(eventStore, eventFactory, publish);
INFO
For performance reasons, it's recommended to create only one ActionHub
instance for your entire application and reuse it across all action executions.
Configuration Options
Save to Store
By default, the ActionHub will automatically save all events created by actions to your event store. You can disable this behavior:
const hub = new ActionHub(eventStore, eventFactory, publish);
hub.saveToStore = false; // Disable automatic saving to event store
Running Actions
The ActionHub 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 ActionHub.run method executes an action and automatically publishes all generated events using the publish method provided during ActionHub creation.
WARNING
ActionHub.run()
will throw an error if no publish
method was provided when creating the ActionHub.
import { FollowUser, NewContacts } 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",
"wss://pyramid.fiatjaf.com/",
);
// Unfollow a user
await hub.run(UnfollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
Using .exec()
- Manual Event Handling
The ActionHub.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 } from "applesauce-actions/actions";
// Custom publishing logic for this specific action
const customPublish = async (event: NostrEvent) => {
// Publish to specific relays
await relayPool.publish(event, ["wss://relay.damus.io", "wss://nos.lol"]);
// 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", "wss://pyramid.fiatjaf.com/")
.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/operators";
import { EMPTY } from "rxjs";
const subscription = hub
.exec(FollowUser, userPubkey, relayUrl)
.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 publish(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 publish(event);
} catch (publishError) {
console.error("Failed to publish event:", publishError);
// Could retry, save for later, or notify user
await saveForRetry(event);
}
});
Best Practices
Single ActionHub Instance
Create one ActionHub instance per application and reuse it:
// app.ts
export const actionHub = new ActionHub(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:
const hub = new ActionHub(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 publish(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;