A collection of functional loading methods to make common event loading patterns easier.
Use createCountService to share and refresh NIP-45 event totals across views without downloading matching events. It balances requests across trusted relays and adapts per-relay concurrency after rate limits and transport failures.
import { createCountService } from "applesauce-loaders/loaders";
const counts = createCountService(pool, { relays: trustedRelays, concurrency: 40 });
const subscription = counts.event(event).reactions.subscribe((state) => {
// value can be zero or undefined; origin.relay identifies the answering relay.
renderCount(state.value, state.refreshing);
});
subscription.unsubscribe(); // Release this view's demand.
counts.stop(); // Stop the application-owned service.
The pool must provide relay(url).count(...), as RelayPool does. Counts come from one relay and are not network-wide totals. In-memory caching is built in; pass a Keyv-compatible cache to persist successful answers across sessions.
See the event count service guide for custom metrics, React integration, persistence, invalidation, and shared schedulers.
The Address Loader is a specialized loader for fetching Nostr replaceable events by their address (kind, pubkey, and optional identifier). It provides an efficient way to batch and deduplicate requests, cache results, and handle relay hints.
import { createAddressLoader } from "applesauce-loaders/loaders";
import { EventStore } from "applesauce-core";
import { RelayPool } from "applesauce-relay";
const eventStore = new EventStore();
const pool = new RelayPool();
// Create an address loader (do this once at the app level)
const addressLoader = createAddressLoader(pool, {
// Pass all events to the event store to deduplicate them
eventStore,
// Optional configuration options
bufferTime: 1000,
followRelayHints: true,
extraRelays: ["wss://relay.example.com"],
});
// Load a profile (kind 0)
addressLoader({
kind: 0,
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
relays: ["wss://relay.example.com"],
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
// Load a contact list (kind 3)
addressLoader({
kind: 3,
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
relays: ["wss://relay.example.com"],
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
// Load a parameterized replaceable event
addressLoader({
kind: 30000,
pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
identifier: "list of bad people",
relays: ["wss://relay.example.com"],
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
The Event Loader is a specialized loader for fetching Nostr events by their IDs. It provides an efficient way to batch and deduplicate requests, cache results, and handle relay hints.
import { createEventLoader } from "applesauce-loaders/loaders";
// Create an event loader (do this once at the app level)
const eventLoader = createEventLoader(pool, {
// Pass all events to the event store to deduplicate them
eventStore,
// Optional configuration options
bufferTime: 1000,
followRelayHints: true,
extraRelays: ["wss://relay.example.com"],
});
// Load an event by ID
eventLoader({
id: "2650f6292166624f45795248edb9ca136c276a3d10a0d8f4efd2b8b23eb2d5fc",
relays: ["wss://relay.example.com"],
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
// Load from extra relays
eventLoader({
id: "2650f6292166624f45795248edb9ca136c276a3d10a0d8f4efd2b8b23eb2d5fc",
relays: ["wss://relay.example.com"],
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
The Unified Event Loader is a single loader that can handle both EventPointer and AddressPointer types. It automatically routes to the appropriate loader (createEventLoader for events by ID, createAddressLoader for addressable/replaceable events) based on the pointer type.
This is the recommended approach when setting up loaders for an EventStore, as it provides a single loader that works with the unified eventLoader property.
import { createUnifiedEventLoader, createEventLoaderForStore } from "applesauce-loaders/loaders";
import { EventStore } from "applesauce-core";
import { RelayPool } from "applesauce-relay";
const eventStore = new EventStore();
const pool = new RelayPool();
// Option 1: Create and assign manually
const unifiedLoader = createUnifiedEventLoader(pool, {
eventStore,
bufferTime: 1000,
followRelayHints: true,
extraRelays: ["wss://relay.example.com"],
lookupRelays: ["wss://purplepag.es", "wss://index.hzrd149.com"],
});
eventStore.eventLoader = unifiedLoader;
// Option 2: Use the convenience function (recommended)
createEventLoaderForStore(eventStore, pool, {
bufferTime: 1000,
followRelayHints: true,
extraRelays: ["wss://relay.example.com"],
lookupRelays: ["wss://purplepag.es", "wss://index.hzrd149.com"],
});
// Now the event store can load both events by ID and addressable events
eventStore.event({ id: "event_id" }).subscribe((event) => {
console.log("Loaded event:", event);
});
eventStore.replaceable({ kind: 0, pubkey: "pubkey" }).subscribe((profile) => {
console.log("Loaded profile:", profile);
});
The unified loader accepts all options from both EventPointerLoaderOptions and AddressLoaderOptions, making it easy to configure both types of loading in one place.
The Timeline Loader follows finite time windows, fills missing ranges independently across relays, and prefetches surrounding events for scrolling. A network batch is not an application page.
import { BehaviorSubject } from "rxjs";
import { createTimelineLoader } from "applesauce-loaders/loaders";
const until = Math.floor(Date.now() / 1000);
const windows$ = new BehaviorSubject({ since: until - 3600, until });
const loader = createTimelineLoader(pool, relays, { kinds: [1] }, { eventStore });
const sub = loader(windows$).subscribe();
windows$.next({ since: until - 7200, until: until - 3600 });
loader.status$.subscribe((statuses) => console.log(statuses));
sub.unsubscribe();
Defaults are limit: 100 and prefetch: 100 events buffered past each side of the window. Set prefetch: 0 to disable buffering. Older events are buffered with exact newest-first pages; newer events use a range estimated from the density of loaded events, since relays cannot page forward. Failed ranges retry with exponential backoff, and a liveness tracker such as RelayLiveness can receive relay failures so dead relays leave the relay set.
Keep the window stream subscribed to reuse coverage. A single finite { since, until } is also accepted and completes when visible and buffer requests settle. Inspect status$ to distinguish coverage from request failures or unpageable boundary seconds; completion alone does not guarantee coverage.
createOutboxTimelineLoader follows the same contract for changing outbox maps. Cache and relay coverage stay independent. Events are stored before output deduplication, preserving relay metadata.
Numeric arguments, omitted windows, infinite bounds, directional block operators, and deprecated timeline wrappers are no longer supported. See the timeline documentation for status details, operators, and migration guidance.
All loaders support a cacheRequest option to load events from a local cache.
import { NostrEvent, Filter } from "nostr-tools";
import { createEventLoader } from "applesauce-loaders/loaders";
// Custom method for loading events from a database
async function cacheRequest(filters: Filter[]): Promise<NostrEvent[]> {
return await cacheDatabase.getEvents(filters);
}
const eventLoader = createEventLoader(pool, {
// Pass all events to the event store to deduplicate them
eventStore,
// Pass a custom cache method
cacheRequest,
// Optional configuration options
bufferTime: 1000,
});
// Because no relays are specified, the event will be loaded from the cache
eventLoader({
id: "2650f6292166624f45795248edb9ca136c276a3d10a0d8f4efd2b8b23eb2d5fc",
}).subscribe((event) => {
// Handle the loaded event
console.log(event);
});
All loaders accept these common configuration options:
bufferTime: Time interval to buffer requests in ms (default 1000)bufferSize: Max buffer size (default 200)eventStore: An event store used to deduplicate eventscacheRequest: A method used to load events from a local cachefollowRelayHints: Whether to follow relay hints (default true)lookupRelays: Fallback lookup relays to check when event can't be foundextraRelays: An array of relays to always fetch frombufferTime: Time interval to buffer requests in ms (default 1000)bufferSize: Max buffer size (default 200)eventStore: An event store used to deduplicate eventscacheRequest: A method used to load events from a local cachefollowRelayHints: Whether to follow relay hints (default true)extraRelays: An array of relays to always fetch fromThe unified event loader accepts all options from both Event Loader and Address Loader options, plus:
lookupRelays: Fallback lookup relays to check when event can't be found (from Address Loader)limit: Maximum number of events to request per filtercache: A method used to load events from a local cacheeventStore: An event store to pass all events toAll loaders require a request method for loading Nostr events from relays. You can provide this in multiple ways:
The simplest approach is to pass a RelayPool instance directly:
import { createAddressLoader, createEventLoader } from "applesauce-loaders/loaders";
import { RelayPool } from "applesauce-relay";
const pool = new RelayPool();
const addressLoader = createAddressLoader(pool, { eventStore });
const eventLoader = createEventLoader(pool, { eventStore });
You can also provide a custom request method, such as one from nostr-tools:
import { createEventLoader } from "applesauce-loaders/loaders";
import { SimplePool } from "nostr-tools";
import { Observable } from "rxjs";
const pool = SimplePool();
// Create a custom request function using nostr-tools
function customRequest(relays, filters) {
return new Observable((observer) => {
const sub = pool.subscribeMany(relays, filters, {
onevent: (event) => observer.next(event),
eose: () => observer.complete(),
});
return () => sub.close();
});
}
// Create event loader with custom request
const eventLoader = createEventLoader(customRequest, options);