Event Caching
Event caching is designed to work alongside the synchronous EventStore. It's generally implemented using the persistEventsToCache helper method and a cacheRequest function that plugs into event loaders.
Overview
Event caching is designed to work alongside the synchronous EventStore so that web applications can take advantage of the in-memory nature of the sync event store. The event store provides fast, synchronous access to events in memory, while the cache provides persistent storage for frequently accessed events like profiles, contacts, and other replaceable events. Unlike event databases, which persist all events, event caching allows you to selectively cache only the events you need.
Key Characteristics
In-Memory Event Store
Event caching works with the synchronous EventStore, which keeps all events in memory for fast, synchronous access. This makes it ideal for web applications where you want instant access to events without async operations:
import { EventStore } from "applesauce-core";
import { persistEventsToCache } from "applesauce-core/helpers";
const eventStore = new EventStore();
// All operations are synchronous and in-memory
eventStore.add(event); // Fast, synchronous, in-memory
const event = eventStore.getEvent(eventId); // Instant access
const events = eventStore.getByFilters({ kinds: [1] }); // No async overheadSelective Caching
Event caching allows you to cache only the events you need, typically:
- Profiles (kind 0) - User metadata
- Contacts (kind 3) - User contact lists
- Replaceable events - Frequently accessed replaceable events
- Specific event IDs - Events you want to cache for offline access
Two-Part System
Event caching consists of two main components that work together without blocking the event store:
persistEventsToCache- Non-blocking: automatically saves new events to the cache after they are added to the storecacheRequest- Loads events from the cache when requested by event loaders, so cache operations don't block the event store
Basic Usage
Setting Up Event Caching
import { EventStore } from "applesauce-core";
import { persistEventsToCache } from "applesauce-core/helpers";
import { createEventLoader } from "applesauce-loaders/loaders";
import { RelayPool } from "applesauce-relay";
import { NostrIDB } from "nostr-idb";
// Create your event store and relay pool
const eventStore = new EventStore();
const pool = new RelayPool();
// Create a cache (e.g., using nostr-idb)
const nostrIDB = new NostrIDB();
await nostrIDB.start();
// Create a cache request function
const cacheRequest = (filters) => nostrIDB.filters(filters);
// Automatically persist new events to the cache
persistEventsToCache(eventStore, async (events) => {
await Promise.allSettled(
events.map(async (event) => {
await nostrIDB.add(event);
}),
);
});
// Use the cache with event loaders
const eventLoader = createEventLoader(pool, {
cacheRequest,
eventStore,
});How It Works
Event caching operates in a non-blocking manner to preserve the fast, synchronous nature of the in-memory event store:
- Event Loader Checks Cache First: When an event loader needs to load an event, it first checks the cache using
cacheRequest. This happens outside the event store, so it doesn't block synchronous operations. - Cache Miss Falls Back to Relays: If the event isn't in the cache, the loader fetches it from relays and adds it to the event store.
- New Events Are Cached Asynchronously: When new events are added to the event store, they're immediately available in memory.
persistEventsToCachethen saves them to the cache after they're added (non-blocking), so the event store remains fast and synchronous.
// Event loader automatically uses cache
const eventLoader = createEventLoader(pool, {
cacheRequest,
eventStore,
});
// Load an event - checks cache first (non-blocking), then relays
eventLoader({ id: "event-id" }).subscribe((event) => {
// Event is immediately available in the event store
const cached = eventStore.getEvent("event-id"); // Fast, synchronous access
if (isFromCache(event)) {
console.log("Loaded from cache!");
} else {
console.log("Loaded from relay");
// Event is now in the event store and will be cached asynchronously
}
});Integration with Event Loaders
Event caching integrates seamlessly with AppleSauce's event loaders. The key is that cache operations happen through event loaders, not directly through the event store, so they don't block the synchronous event store:
import { EventStore } from "applesauce-core";
import { persistEventsToCache } from "applesauce-core/helpers";
import { createEventLoader } from "applesauce-loaders/loaders";
import { RelayPool } from "applesauce-relay";
import { NostrIDB } from "nostr-idb";
const eventStore = new EventStore();
const pool = new RelayPool();
const nostrIDB = new NostrIDB();
await nostrIDB.start();
// Setup caching - cacheRequest is used by event loaders, not the event store
const cacheRequest = (filters) => nostrIDB.filters(filters);
// Persist events to cache after they're added to the store (non-blocking)
persistEventsToCache(eventStore, async (events) => {
// This runs asynchronously after events are added to the store
await Promise.allSettled(
events.map(async (event) => {
await nostrIDB.add(event);
}),
);
});
// Create event loader with cache - cache loading happens here, not in event store
const eventLoader = createEventLoader(pool, {
cacheRequest, // Event loaders use this to check cache first
eventStore,
lookupRelays: ["wss://relay.example.com"],
});
// Use the loader - cache is checked by the loader, not the event store
eventLoader({ id: "profile-id" }).subscribe((event) => {
// Event is immediately available in the event store (synchronous, in-memory)
eventStore.add(event);
// Cache persistence happens asynchronously in the background
});Available Cache Implementations
nostr-idb (IndexedDB)
The most common cache implementation for web applications:
import { NostrIDB } from "nostr-idb";
const nostrIDB = new NostrIDB({
cacheIndexes: 1000, // Cache 1000 indexes in memory
maxEvents: 10000, // Maximum events to store
});
await nostrIDB.start();
const cacheRequest = (filters) => nostrIDB.filters(filters);See the nostr-idb documentation for more details.
Custom Cache Implementation
You can create a custom cache by implementing a cacheRequest function:
// Custom cache using localStorage (simple example)
function cacheRequest(filters: Filter[]): Promise<NostrEvent[]> {
// Load events from your custom storage
const cached = localStorage.getItem("nostr-cache");
const events: NostrEvent[] = cached ? JSON.parse(cached) : [];
// Filter events based on the provided filters
return Promise.resolve(
events.filter((event) => {
// Implement filter matching logic
return matchesFilters(event, filters);
}),
);
}
// Use with event loader
const eventLoader = createEventLoader(pool, {
cacheRequest,
eventStore,
});Advanced Usage
Configuring Batch Persistence
Control how events are batched when persisting to cache:
const stopPersisting = persistEventsToCache(
eventStore,
async (events) => {
await nostrIDB.addEvents(events);
},
{
batchTime: 5000, // Wait 5 seconds before writing
maxBatchSize: 100, // Maximum events per batch
},
);
// Stop persisting when done
stopPersisting();Filtering Cached Events
Only cache specific event types:
persistEventsToCache(eventStore, async (events) => {
// Only cache profiles and contacts
const toCache = events.filter((e) => e.kind === 0 || e.kind === 3);
if (toCache.length > 0) {
await nostrIDB.addEvents(toCache);
}
});Best Practices
Cache Frequently Accessed Events
Focus on caching events that are accessed frequently:
// Cache profiles, contacts, and other replaceable events
persistEventsToCache(eventStore, async (events) => {
const important = events.filter((e) => {
return (
e.kind === 0 || // Profiles
e.kind === 3 || // Contacts
isReplaceable(e.kind) // Other replaceable events
);
});
if (important.length > 0) {
await nostrIDB.addEvents(important);
}
});