FavoriteRelaysRepositoryImpl.ts raw
1 import { FavoriteRelaysRepository, FavoriteRelays, Pubkey, tryToFavoriteRelays } from '@/domain'
2 import { ExtendedKind } from '@/constants'
3 import client from '@/services/client.service'
4 import indexedDb from '@/services/indexed-db.service'
5 import { kinds, Event } from 'nostr-tools'
6 import { RepositoryDependencies } from './types'
7
8 /**
9 * IndexedDB + Relay implementation of FavoriteRelaysRepository
10 *
11 * Uses IndexedDB for local caching and the client service for relay fetching.
12 * Save operations publish to relays and update the local cache.
13 */
14 export class FavoriteRelaysRepositoryImpl implements FavoriteRelaysRepository {
15 constructor(private readonly deps: RepositoryDependencies) {}
16
17 async findByOwner(pubkey: Pubkey): Promise<FavoriteRelays | null> {
18 // Try cache first for the favorite relays event
19 let favoriteRelaysEvent = await indexedDb.getReplaceableEvent(
20 pubkey.hex,
21 ExtendedKind.FAVORITE_RELAYS
22 )
23
24 // Fetch from relays if not cached
25 if (!favoriteRelaysEvent) {
26 favoriteRelaysEvent = await client.fetchFavoriteRelaysEvent(pubkey.hex)
27 }
28
29 if (!favoriteRelaysEvent) return null
30
31 // Extract relay set IDs from the event
32 const relaySetIds: string[] = []
33 favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
34 if (tagName === 'a' && tagValue) {
35 const [kind, author, relaySetId] = tagValue.split(':')
36 if (kind !== kinds.Relaysets.toString()) return
37 if (author !== pubkey.hex) return // Only own relay sets for now
38 if (!relaySetId || relaySetIds.includes(relaySetId)) return
39 relaySetIds.push(relaySetId)
40 }
41 })
42
43 // Load relay set events
44 const relaySetEvents: Event[] = []
45 if (relaySetIds.length > 0) {
46 // Try cache first
47 const cachedEvents = await Promise.all(
48 relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey.hex, kinds.Relaysets, id))
49 )
50
51 // Collect cached events
52 const cachedEventMap = new Map<string, Event>()
53 const missingIds: string[] = []
54 relaySetIds.forEach((id, index) => {
55 const cached = cachedEvents[index]
56 if (cached) {
57 cachedEventMap.set(id, cached)
58 } else {
59 missingIds.push(id)
60 }
61 })
62
63 // Fetch missing from relays
64 if (missingIds.length > 0) {
65 const fetchedEvents = await client.fetchEvents([], {
66 kinds: [kinds.Relaysets],
67 authors: [pubkey.hex],
68 '#d': missingIds
69 })
70
71 // Deduplicate and cache
72 for (const event of fetchedEvents) {
73 const d = event.tags.find((t) => t[0] === 'd')?.[1]
74 if (!d) continue
75 const existing = cachedEventMap.get(d)
76 if (!existing || existing.created_at < event.created_at) {
77 cachedEventMap.set(d, event)
78 await indexedDb.putReplaceableEvent(event)
79 }
80 }
81 }
82
83 // Collect in original order
84 for (const id of relaySetIds) {
85 const event = cachedEventMap.get(id)
86 if (event) {
87 relaySetEvents.push(event)
88 }
89 }
90 }
91
92 // Update favorite relays cache
93 await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
94
95 return tryToFavoriteRelays(favoriteRelaysEvent, relaySetEvents)
96 }
97
98 async save(favoriteRelays: FavoriteRelays): Promise<void> {
99 // First, publish all relay sets
100 for (const relaySet of favoriteRelays.getSets()) {
101 const relaySetDraftEvent = relaySet.toDraftEvent()
102 const publishedRelaySetEvent = await this.deps.publish(relaySetDraftEvent)
103 await indexedDb.putReplaceableEvent(publishedRelaySetEvent)
104 }
105
106 // Then publish the favorite relays event
107 const draftEvent = favoriteRelays.toDraftEvent(favoriteRelays.owner.hex)
108 const publishedEvent = await this.deps.publish(draftEvent)
109
110 // Update cache
111 await indexedDb.putReplaceableEvent(publishedEvent)
112 }
113 }
114