From 0293d2dde63e1a5585e85bc844a1522891a4ce87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Fri, 16 Jan 2026 13:11:15 +0100 Subject: [PATCH] feat: Integrate NIP-17 inbox relays (kind 10050) into relay list cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/chat/adapters/nip-17-adapter.ts | 25 ++++- src/services/db.ts | 1 + src/services/relay-list-cache.ts | 124 ++++++++++++++++-------- 3 files changed, 108 insertions(+), 42 deletions(-) diff --git a/src/lib/chat/adapters/nip-17-adapter.ts b/src/lib/chat/adapters/nip-17-adapter.ts index d29ef16..3bfb4c8 100644 --- a/src/lib/chat/adapters/nip-17-adapter.ts +++ b/src/lib/chat/adapters/nip-17-adapter.ts @@ -78,7 +78,22 @@ async function fetchInboxRelays(pubkey: string): Promise { // 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 { `[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 { 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", ); diff --git a/src/services/db.ts b/src/services/db.ts index c441b51..5e7c7ea 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -42,6 +42,7 @@ export interface CachedRelayList { event: NostrEvent; read: string[]; write: string[]; + inbox?: string[]; // NIP-17 private inbox relays (kind 10050) updatedAt: number; } diff --git a/src/services/relay-list-cache.ts b/src/services/relay-list-cache.ts index 13a91aa..fef4cdd 100644 --- a/src/services/relay-list-cache.ts +++ b/src/services/relay-list-cache.ts @@ -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 { 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(