Relay Pool
The RelayPool class in applesauce-relay manages multiple relay connections as one event source. Its high-level request and subscription methods emit deduplicated events and report total active-cohort failure as RelayGroupError.
Features
- Connect to multiple relays
- Create and manage groups of relays
- Send requests and events to multiple relays simultaneously
- Maintain a blacklist of relays to avoid
Relay Management
The RelayPool provides methods to create and manage relay connections:
// Get or create a relay connection
const relay = pool.relay("wss://relay.example.com");
// Create a group of relays
const group = pool.group(["wss://relay1.example.com", "wss://relay2.example.com"]);Making Requests
The RelayPool provides several methods to interact with relays. These methods mirror those found in the Relay class, allowing you to use familiar patterns while working with multiple relays simultaneously.
REQ Method
The req method sends a subscription request to multiple relays:
// Send a REQ to multiple relays
pool
.req(relays, {
kinds: [1],
limit: 10,
})
.subscribe({
next: (response) => {
if (response === "EOSE") {
console.log("End of stored events from all relays");
} else {
console.log("Event", response);
}
},
error: (error) => {
console.error("Subscription error:", error);
},
});Note: The req method does not deduplicate events by default. If you need deduplication, use the request or subscription methods instead, which automatically deduplicate events using an event store.
Event Method
The event method sends an EVENT message to multiple relays and returns an observable of the responses from each relay.
const event = {
kind: 1,
content: "Hello from RelayPool!",
created_at: Math.floor(Date.now() / 1000),
tags: [],
// ... other required fields
};
// Subscribe to a stream of responses
pool.event(relays, event).subscribe({
next: (response) => {
console.log(`Published to ${response.from}:`, response.ok);
if (!response.ok) console.log(`Error message: ${response.message}`);
},
complete: () => {
console.log("Publishing complete");
},
});Publish Method
The publish method is a wrapper around the event method that returns a Promise<PublishResponse[]> and automatically handles retrying:
// Publish with retries (defaults to 3 retries)
const responses = await pool.publish(relays, event);
for (const response of responses) {
console.log(`Published to ${response.from}:`, response.ok, response.message);
}Request Method
The request method allows you to make one-off requests with automatic retries for connection errors:
pool.request(relays, { kinds: [1], limit: 50 }, { timeout: 5000 }).subscribe({
next: (event) => console.log(event.id),
error(error) {
if (error instanceof RelayGroupError) {
for (const [url, outcome] of Object.entries(error.outcomes)) console.error(url, outcome.error);
}
},
complete: () => console.log("Request complete"),
});request() has one 30-second whole-Observable timeout by default. Events, EOSE, retries, and reconnections do not reset it. A request with no active relays completes empty; one EOSE plus any failures is also a successful request.
By default, request() completes once relays go quiet after the first EOSE (RelayGroup.completeAfterIdle()). It waits 3 seconds for relays that haven't responded, keeps waiting while events arrive, and stops after 15 seconds. Duplicate-only traffic can extend the wait for at most 5 seconds without a new event ID (progress); increase this option when useful events arrive between long duplicate bursts. A relay that sends nothing within firstResponseTimeout (default 10 seconds) is treated as failed. Pass complete to choose a different condition; the completion conditions example compares them side by side.
Subscription Method
The subscription method creates persistent subscriptions that retry connection errors and can resubscribe after clean relay CLOSED messages:
// Create persistent subscription
const subscription = pool
.subscription(
relays,
{
kinds: [1, 7],
"#t": ["nostr"],
},
{
id: "custom-sub-id", // optional custom subscription ID
reconnect: Infinity, // retry connection errors forever
resubscribe: true, // resubscribe after clean relay CLOSED messages
},
)
.subscribe({
next: (event) => console.log("Subscription update:", event),
});
// Later, you can unsubscribe
subscription.unsubscribe();Subscriptions have no built-in duration or inactivity timeout. Compose the lifetime at the call site when needed:
import { takeUntil, timer } from "rxjs";
pool.subscription(relays, filters).pipe(takeUntil(timer(30_000)));All of these methods accept the same parameters as their counterparts in the Relay class, making it easy to transition between working with individual relays and relay pools.
Subscription Map Method
The subscriptionMap method allows you to subscribe to different filters on different relays:
import { createFilterMap } from "applesauce-core/helpers";
// Create a map of relay URLs to filters
const filterMap = {
"wss://relay1.example.com": { kinds: [1], authors: ["pubkey1"] },
"wss://relay2.example.com": { kinds: [1], authors: ["pubkey2"] },
};
pool.subscriptionMap(filterMap).subscribe({
next: (event) => console.log("Event:", event),
});Outbox Subscription Method
The outboxSubscription method subscribes to each user's events on their own outbox relays; see Outbox.
Counting and Sync
pool.count() and pool.sync() send COUNT and negentropy requests to several relays and report each relay's result separately. See Counting and Negentropy Sync.
Relay Groups
The RelayGroup class is used internally by RelayPool to manage collections of relays. You can access relay groups directly through the pool:
// Create a group of relays
const group = pool.group(["wss://relay1.example.com", "wss://relay2.example.com"]);
// Make requests to the group
group.req({ kinds: [1] }).subscribe((response) => console.log(response));
// Send events to the group
group.event(event).subscribe((response) => console.log(response));
// Use other group methods
const responses = await group.publish(event);
group.request({ kinds: [1] }).subscribe((event) => console.log(event));
group.subscription({ kinds: [1] }).subscribe((response) => console.log(response));High-level Group and Pool methods preserve the same RelayGroupError instance. Outcome keys are normalized relay URLs and causes retain identity; raw req() keeps exposing lifecycle messages instead of this aggregate error contract.
Integration
Dynamic subscriptionMap() and outboxSubscription() replace the active relay cohort as their maps change. An empty dynamic subscription stays open for future relays, while an empty finite request completes immediately.
Pool and group methods forward auth options to every relay; see Authentication.
Best Practices
- Handle
RelayGroupErrorat the returned Observable'serrorcallback. - Use
outcomesfor normalized URL lookup and nativeerrorsfor aggregate tooling. - Let subscriptions run until the consumer unsubscribes.
- Compose
timeoutortakeUntil(timer(...))when a caller needs its own bound.
Observable Properties
The RelayPool provides observables for tracking relays:
status$ emits a record of the latest status for every relay, keyed by relay URL. Adding or removing a relay emits one complete snapshot while retaining subscriptions to unchanged relays; each relay's subsequent status changes still emit updated snapshots. An empty pool emits {}.
pool.status$.subscribe((statuses) => {
console.log("Connected relays:", Object.values(statuses).filter((status) => status.connected).length);
});Migration: membership changes no longer emit intermediate empty or partially rebuilt status records. Handle the complete snapshot instead of relying on those intermediate emissions.
// Subscribe to changes in the relays map
pool.relays$.subscribe((relaysMap) => {
console.log("Relays updated:", Array.from(relaysMap.keys()));
});
// Listen for relay additions
pool.add$.subscribe((relay) => {
console.log("Relay added:", relay.url);
});
// Listen for relay removals
pool.remove$.subscribe((relay) => {
console.log("Relay removed:", relay.url);
});