feat: Integrate NIP-17 inbox relays (kind 10050) into relay list cache

NIP-17 inbox relays are now properly cached and displayed just like regular relay lists:

**Relay List Cache Integration**:
- Extended CachedRelayList schema with `inbox?: string[]` field for kind 10050
- relay-list-cache now subscribes to both kind 10002 and kind 10050 events
- Parses and caches inbox relays from "relay" tags in kind 10050 events
- Merges inbox relays with existing cached entries (preserves read/write)

**NIP-17 Adapter Improvements**:
- Checks cache first before fetching inbox relays from network
- Fetched kind 10050 events auto-added to EventStore → triggers cache
- Logs show "Using cached" vs "Fetched and cached" for visibility
- Inbox relays now persist across sessions via Dexie

**Benefits**:
- Inbox relays display immediately from cache (no network delay)
- Reduced network requests - fetch once, use everywhere
- RelaysDropdown shows per-participant inbox relays automatically
- Inbox relays sync whenever chatting with participants

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-01-16 13:11:15 +01:00
parent a7e1746cf9
commit 0293d2dde6
3 changed files with 108 additions and 42 deletions

View File

@@ -78,7 +78,22 @@ async function fetchInboxRelays(pubkey: string): Promise<string[]> {
// Not in store, continue to network fetch
}
// 2. Build relay list to query: participant's outbox + ALL aggregators
// 2. Check if we already have inbox relays cached
try {
const cached = await relayListCache.get(pubkey);
if (cached?.inbox && cached.inbox.length > 0) {
console.log(
`[NIP-17] ✅ Using cached inbox relays for ${pubkey.slice(0, 8)}:`,
cached.inbox.length,
"relays",
);
return cached.inbox;
}
} catch {
// Cache miss, continue to fetch
}
// 3. Build relay list to query: participant's outbox + ALL aggregators
const relaysToQuery: string[] = [];
try {
@@ -111,14 +126,14 @@ async function fetchInboxRelays(pubkey: string): Promise<string[]> {
`[NIP-17] Fetching inbox relays for ${pubkey.slice(0, 8)} from ${uniqueRelays.length} relays (trying harder)`,
);
// 3. Fetch from relays with aggressive timeout and retry
// 4. Fetch from relays with aggressive timeout and retry
try {
const events = await firstValueFrom(
pool
.request(
uniqueRelays,
[{ kinds: [DM_RELAY_LIST_KIND], authors: [pubkey], limit: 1 }],
{ eventStore },
{ eventStore }, // Events auto-added to EventStore → triggers relay-list-cache
)
.pipe(
toArray(),
@@ -131,9 +146,11 @@ async function fetchInboxRelays(pubkey: string): Promise<string[]> {
const latest = events.reduce((a, b) =>
a.created_at > b.created_at ? a : b,
);
// Event is already in EventStore and will be cached by relay-list-cache subscription
const relays = parseRelayTags(latest);
console.log(
`[NIP-17] ✅ Fetched inbox relays for ${pubkey.slice(0, 8)}:`,
`[NIP-17] ✅ Fetched and cached inbox relays for ${pubkey.slice(0, 8)}:`,
relays.length,
"relays",
);

View File

@@ -42,6 +42,7 @@ export interface CachedRelayList {
event: NostrEvent;
read: string[];
write: string[];
inbox?: string[]; // NIP-17 private inbox relays (kind 10050)
updatedAt: number;
}

View File

@@ -1,10 +1,13 @@
/**
* Relay List Cache Service
*
* Caches NIP-65 relay lists (kind:10002) in Dexie for fast access.
* Caches relay lists in Dexie for fast access:
* - NIP-65 relay lists (kind 10002) - outbox/inbox relays
* - NIP-17 inbox relays (kind 10050) - private DM inbox relays
*
* Reduces network requests and improves cold start performance.
*
* Auto-caches kind:10002 events from EventStore when subscribed.
* Auto-caches kind 10002 and 10050 events from EventStore when subscribed.
*/
import type { NostrEvent } from "@/types/nostr";
@@ -13,9 +16,21 @@ import { normalizeRelayURL } from "@/lib/relay-url";
import db, { CachedRelayList } from "./db";
import type { IEventStore } from "applesauce-core/event-store";
import type { Subscription } from "rxjs";
import { merge } from "rxjs";
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
const MAX_MEMORY_CACHE = 100; // LRU cache size
const DM_RELAY_LIST_KIND = 10050; // NIP-17 DM inbox relays
/**
* Parse inbox relay URLs from kind 10050 event
* Tags are in format: ["relay", "wss://relay.example.com"]
*/
function parseInboxRelays(event: NostrEvent): string[] {
return event.tags
.filter((tag) => tag[0] === "relay" && tag[1])
.map((tag) => tag[1]);
}
class RelayListCache {
private eventStoreSubscription: Subscription | null = null;
@@ -23,7 +38,7 @@ class RelayListCache {
private cacheOrder: string[] = [];
/**
* Subscribe to EventStore to auto-cache kind:10002 events
* Subscribe to EventStore to auto-cache kind 10002 and 10050 events
*/
subscribeToEventStore(eventStore: IEventStore): void {
if (this.eventStoreSubscription) {
@@ -31,16 +46,17 @@ class RelayListCache {
return;
}
// Subscribe to kind:10002 events
this.eventStoreSubscription = eventStore
.filters({ kinds: [10002] })
.subscribe((event: NostrEvent) => {
// Cache each kind:10002 event as it arrives
this.set(event);
});
// Subscribe to both kind 10002 (NIP-65) and kind 10050 (NIP-17 inbox)
this.eventStoreSubscription = merge(
eventStore.filters({ kinds: [10002] }),
eventStore.filters({ kinds: [DM_RELAY_LIST_KIND] }),
).subscribe((event: NostrEvent) => {
// Cache each relay list event as it arrives
this.set(event);
});
console.log(
"[RelayListCache] Subscribed to EventStore for kind:10002 events",
"[RelayListCache] Subscribed to EventStore for kind 10002 and 10050 events",
);
}
@@ -87,42 +103,65 @@ class RelayListCache {
/**
* Store relay list event in cache
* Handles both kind 10002 (NIP-65) and kind 10050 (NIP-17 inbox)
*/
async set(event: NostrEvent): Promise<void> {
try {
if (event.kind !== 10002) {
if (event.kind !== 10002 && event.kind !== DM_RELAY_LIST_KIND) {
console.warn(
`[RelayListCache] Attempted to cache non-10002 event (kind ${event.kind})`,
`[RelayListCache] Attempted to cache invalid event kind ${event.kind}`,
);
return;
}
// Parse relays from event
const readRelays = getInboxes(event);
const writeRelays = getOutboxes(event);
// Get existing cache entry (if any) to merge with
const existing = await db.relayLists.get(event.pubkey);
// Normalize URLs and filter invalid ones
const normalizedRead = readRelays
.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
console.warn(`[RelayListCache] Invalid read relay URL: ${url}`);
return null;
}
})
.filter((url): url is string => url !== null);
let normalizedRead: string[] = existing?.read || [];
let normalizedWrite: string[] = existing?.write || [];
let normalizedInbox: string[] | undefined = existing?.inbox;
const normalizedWrite = writeRelays
.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
console.warn(`[RelayListCache] Invalid write relay URL: ${url}`);
return null;
}
})
.filter((url): url is string => url !== null);
if (event.kind === 10002) {
// Parse NIP-65 relay list (outbox/inbox)
const readRelays = getInboxes(event);
const writeRelays = getOutboxes(event);
normalizedRead = readRelays
.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
console.warn(`[RelayListCache] Invalid read relay URL: ${url}`);
return null;
}
})
.filter((url): url is string => url !== null);
normalizedWrite = writeRelays
.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
console.warn(`[RelayListCache] Invalid write relay URL: ${url}`);
return null;
}
})
.filter((url): url is string => url !== null);
} else if (event.kind === DM_RELAY_LIST_KIND) {
// Parse NIP-17 inbox relays (kind 10050)
const inboxRelays = parseInboxRelays(event);
normalizedInbox = inboxRelays
.map((url) => {
try {
return normalizeRelayURL(url);
} catch {
console.warn(`[RelayListCache] Invalid inbox relay URL: ${url}`);
return null;
}
})
.filter((url): url is string => url !== null);
}
// Store in Dexie and memory cache
const cachedEntry: CachedRelayList = {
@@ -130,6 +169,7 @@ class RelayListCache {
event,
read: normalizedRead,
write: normalizedWrite,
inbox: normalizedInbox,
updatedAt: Date.now(),
};
@@ -140,8 +180,16 @@ class RelayListCache {
this.cacheOrder.push(event.pubkey);
this.evictOldest();
const logParts = [`${event.pubkey.slice(0, 8)}`];
if (normalizedWrite.length > 0)
logParts.push(`${normalizedWrite.length} write`);
if (normalizedRead.length > 0)
logParts.push(`${normalizedRead.length} read`);
if (normalizedInbox && normalizedInbox.length > 0)
logParts.push(`${normalizedInbox.length} inbox`);
console.debug(
`[RelayListCache] Cached relay list for ${event.pubkey.slice(0, 8)} (${normalizedWrite.length} write, ${normalizedRead.length} read)`,
`[RelayListCache] Cached relay list for ${logParts.join(", ")}`,
);
} catch (error) {
console.error(