Negentropy Sync
Negentropy sync is an efficient way to synchronize events between your local event store and Nostr relays using NIP-77. Instead of downloading all events and checking for duplicates, negentropy compares event IDs and timestamps to identify which events need to be transferred, dramatically reducing bandwidth usage.
Overview
The negentropy protocol works by:
- Comparing inventories: Both client and relay share summaries of what events they have
- Identifying differences: The protocol determines which events each side is missing
- Reconciling efficiently: Only the missing events are transferred
This is particularly useful for:
- Initial sync: Getting up to date with a relay without downloading everything
- Periodic updates: Staying synchronized with minimal data transfer
- Relay comparison: Finding differences between relays without transferring events
Requirements
- Relay must support NIP-77 (Negentropy)
- Use
relay.getSupported()
to check if a relay supports NIP-77 before syncing
Basic Usage
Single Relay Sync
The negentropy()
method provides low-level control over the sync process:
// Define what to do with the sync results
const reconcile = async (have: string[], need: string[]) => {
// 'have' = event IDs we have but relay doesn't
// 'need' = event IDs relay has but we don't
// Send our events to the relay
for (const id of have) {
const event = await eventStore.getEvent(id);
if (event) await relay.publish(event);
}
// Fetch missing events from relay
if (need.length > 0) {
const events = await relay.request({ ids: need });
// Process received events...
}
};
// Sync reactions for a specific note
await relay.negentropy(eventStore, { kinds: [7], "#e": ["note_id_here"] }, reconcile);
High-Level Sync with Observable
The sync()
method provides a simpler interface that returns events as they're received:
// Sync and receive events as an observable
relay
.sync(eventStore, {
kinds: [1], // Text notes
authors: ["pubkey_here"],
since: unixNow() - 60 * 60 * 24, // Last 24 hours
})
.subscribe((event) => {
console.log("Received event:", event);
});
Pool and Multi-Relay Sync
RelayPool Usage
Use RelayPool
to sync with multiple relays simultaneously:
const pool = new RelayPool();
// Sync mentions across multiple relays
const relays = ["wss://relay.damus.io", "wss://nos.lol"];
pool
.sync(relays, eventStore, {
kinds: [1],
"#p": ["user_pubkey"],
since: unixNow() - 60 * 60 * 24,
})
.subscribe((event) => {
// Events from any of the relays
eventStore.addEvent(event);
});
Parallel Sync
For maximum efficiency, sync multiple relays in parallel:
// Sync reactions from user's outbox relays
const filter = { kinds: [7], "#e": ["note_id"] };
const syncPromises = outboxRelays.map((relay) => pool.relay(relay).sync(eventStore, filter));
// Wait for all syncs to complete
await Promise.allSettled(syncPromises);
Real-World Examples
1. Sync User Mentions
Load mentions for a user from their inbox relays:
// Get user's NIP-65 relay list
eventStore
.mailboxes(pubkey)
.pipe(
switchMap((mailboxes) => {
// If no mailboxes, return nothing
if (!mailboxes) return NEVER;
// Sync mentions from last 24 hours
return pool.sync(mailboxes.inboxes, eventStore, {
kinds: [1],
"#p": [pubkey],
since: unixNow() - 86400,
});
}),
)
.subscribe((mention) => {
console.log("New mention:", mention.content);
});
2. Sync Note Reactions
Load all reactions for a specific note:
const noteId = "event_id_here";
// Sync from multiple relays to get complete reaction set
pool
.sync(relays, eventStore, {
kinds: [7], // Reactions
"#e": [noteId],
})
.subscribe((reaction) => {
console.log(`${reaction.content} from ${reaction.pubkey}`);
});
API Reference
For full API reference, see the API Reference.
Performance Tips
- Check NIP-77 support before attempting sync
- Use time filters (
since
,until
) to limit sync scope - Sync in parallel when working with multiple relays
- Use appropriate sync direction to avoid unnecessary transfers
- Handle abort signals for long-running syncs
// Efficient multi-relay sync with timeout
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000); // 30s timeout
const syncPromises = relays.map((relay) =>
pool.relay(relay).negentropy(eventStore, filter, reconcile, { signal: controller.signal }),
);
await Promise.allSettled(syncPromises);
The negentropy sync feature makes it practical to keep your local event store synchronized with multiple relays efficiently, enabling rich offline-first Nostr applications.