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