@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.
172 lines (147 loc) • 7.22 kB
text/typescript
import type { NDKEvent } from "../events/index.js";
import { NDKKind } from "../events/kinds/index.js";
import { NDKRelayList, relayListFromKind3 } from "../events/kinds/relay-list.js";
import type { NDK } from "../ndk/index.js";
import type { NDKRelay } from "../relay/index.js";
import { NDKRelaySet } from "../relay/sets/index.js";
import type { NDKSubscriptionOptions } from "../subscription/index.js";
import { NDKSubscriptionCacheUsage } from "../subscription/index.js";
import type { Hexpubkey } from "../user/index.js";
export async function getRelayListForUser(pubkey: Hexpubkey, ndk: NDK): Promise<NDKRelayList> {
const list = await getRelayListForUsers([pubkey], ndk);
return list.get(pubkey)!;
}
/**
* Fetches a map of relay lists for a number of users
* @param pubkeys
* @param ndk
* @param skipCache
* @param timeout
* @param relayHints - Optional map of pubkey to relay URLs to augment the relay set for fetching that user's relay list
* @returns
*/
export async function getRelayListForUsers(
pubkeys: Hexpubkey[],
ndk: NDK,
skipCache = false,
timeout = 1000,
relayHints?: Map<Hexpubkey, string[]>,
): Promise<Map<Hexpubkey, NDKRelayList>> {
const pool = ndk.outboxPool || ndk.pool;
const set = new Set<NDKRelay>();
for (const relay of pool.relays.values()) set.add(relay);
// Add relays from hints to augment the relay set
if (relayHints) {
for (const hints of relayHints.values()) {
for (const url of hints) {
const relay = pool.getRelay(url, true, true);
if (relay) set.add(relay);
}
}
}
const relayLists = new Map<Hexpubkey, NDKRelayList>();
const fromContactList = new Map<Hexpubkey, NDKEvent>();
const relaySet = new NDKRelaySet(set, ndk);
// get all kind 10002 events from cache if we have an adapter and is locking
if (ndk.cacheAdapter?.locking && !skipCache) {
const cachedList = await ndk.fetchEvents(
{ kinds: [3, 10002], authors: Array.from(new Set(pubkeys)) },
{ cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE, subId: "ndk-relay-list-fetch" },
);
// get list of relay lists from cache
for (const relayList of cachedList) {
if (relayList.kind === 10002) relayLists.set(relayList.pubkey, NDKRelayList.from(relayList));
}
for (const relayList of cachedList) {
if (relayList.kind === 3) {
// skip if we already have a relay list for this pubkey
if (relayLists.has(relayList.pubkey)) continue;
const list = relayListFromKind3(ndk, relayList);
if (list) fromContactList.set(relayList.pubkey, list);
}
}
// remove the pubkeys we found from the list
pubkeys = pubkeys.filter((pubkey) => !relayLists.has(pubkey) && !fromContactList.has(pubkey));
}
// if we have no pubkeys left, return the results
if (pubkeys.length === 0) return relayLists;
const relayListEvents = new Map<Hexpubkey, NDKEvent>();
const contactListEvents = new Map<Hexpubkey, NDKEvent>();
return new Promise<Map<Hexpubkey, NDKRelayList>>((resolve) => {
let resolved = false;
const handleSubscription = async () => {
// Get from relays the missing pubkeys
// Prepare options, including the relaySet if available
const subscribeOpts: NDKSubscriptionOptions = {
closeOnEose: true,
pool,
groupable: true,
subId: "ndk-relay-list-fetch",
addSinceFromCache: true,
relaySet,
};
if (relaySet) subscribeOpts.relaySet = relaySet;
const sub = ndk.subscribe({ kinds: [3, 10002], authors: pubkeys }, subscribeOpts, {
onEvent: (event) => {
if (event.kind === NDKKind.RelayList) {
const existingEvent = relayListEvents.get(event.pubkey);
if (existingEvent && existingEvent.created_at! > event.created_at!) return;
relayListEvents.set(event.pubkey, event);
} else if (event.kind === NDKKind.Contacts) {
const existingEvent = contactListEvents.get(event.pubkey);
if (existingEvent && existingEvent.created_at! > event.created_at!) return;
contactListEvents.set(event.pubkey, event);
}
},
onEose: () => {
if (resolved) return;
resolved = true;
ndk.debug(
`[getRelayListForUsers] EOSE - relayListEvents: ${relayListEvents.size}, contactListEvents: ${contactListEvents.size}`,
);
// Get all kind 10002 events
for (const event of relayListEvents.values()) {
relayLists.set(event.pubkey, NDKRelayList.from(event));
}
// Go through the pubkeys we don't have results for and get the from kind 3 events
for (const pubkey of pubkeys) {
if (relayLists.has(pubkey)) continue;
const contactList = contactListEvents.get(pubkey);
if (!contactList) continue;
const list = relayListFromKind3(ndk, contactList);
if (list) relayLists.set(pubkey, list);
}
ndk.debug(
`[getRelayListForUsers] Returning ${relayLists.size} relay lists for ${pubkeys.length} pubkeys`,
);
resolve(relayLists);
},
});
// Check if any relays are still connecting or disconnected
const hasDisconnectedRelays = Array.from(set).some(
(relay) => relay.status <= 2, // DISCONNECTING, DISCONNECTED, or RECONNECTING
);
const hasConnectingRelays = Array.from(set).some(
(relay) => relay.status === 4, // CONNECTING
);
// Use a longer timeout if relays are still connecting or disconnected
// Otherwise use the provided timeout (EOSE should resolve it)
let effectiveTimeout = timeout;
if (hasDisconnectedRelays || hasConnectingRelays) {
effectiveTimeout = timeout + 3000; // Give 3 extra seconds for connection + response
}
ndk.debug(
`[getRelayListForUsers] Setting fallback timeout to ${effectiveTimeout}ms (disconnected: ${hasDisconnectedRelays}, connecting: ${hasConnectingRelays})`,
{ pubkeys },
);
setTimeout(() => {
if (!resolved) {
resolved = true;
ndk.debug(`[getRelayListForUsers] Timeout reached, returning ${relayLists.size} relay lists`);
resolve(relayLists);
}
}, effectiveTimeout);
};
handleSubscription();
});
}