RelaySelector.ts raw

   1  import { RelayUrl } from '@/domain/shared'
   2  import { RelayList } from '@/domain/relay'
   3  
   4  /**
   5   * Options for relay selection
   6   */
   7  export type RelaySelectorOptions = {
   8    maxRelays?: number
   9    preferSecure?: boolean
  10    excludeOnion?: boolean
  11    includeDefaultRelays?: boolean
  12  }
  13  
  14  /**
  15   * RelaySelector Domain Service
  16   *
  17   * Handles intelligent selection of relays for various operations.
  18   * Implements relay selection strategies based on context.
  19   */
  20  export class RelaySelector {
  21    constructor(
  22      private readonly defaultRelays: RelayUrl[] = []
  23    ) {}
  24  
  25    /**
  26     * Select relays for publishing an event
  27     * Prioritizes write relays from the user's relay list
  28     */
  29    selectForPublishing(
  30      relayList: RelayList | null,
  31      options: RelaySelectorOptions = {}
  32    ): RelayUrl[] {
  33      const { maxRelays = 5, preferSecure = true, excludeOnion = false } = options
  34  
  35      const candidates: RelayUrl[] = []
  36  
  37      // Add user's write relays
  38      if (relayList) {
  39        const writeRelays = relayList.getWriteRelays()
  40        candidates.push(...writeRelays)
  41      }
  42  
  43      // Add default relays if needed
  44      if (options.includeDefaultRelays || candidates.length === 0) {
  45        for (const relay of this.defaultRelays) {
  46          if (!candidates.some((c) => c.equals(relay))) {
  47            candidates.push(relay)
  48          }
  49        }
  50      }
  51  
  52      // Filter and sort
  53      let filtered = candidates
  54      if (excludeOnion) {
  55        filtered = filtered.filter((r) => !r.isOnion)
  56      }
  57  
  58      if (preferSecure) {
  59        filtered.sort((a, b) => {
  60          if (a.isSecure && !b.isSecure) return -1
  61          if (!a.isSecure && b.isSecure) return 1
  62          return 0
  63        })
  64      }
  65  
  66      return filtered.slice(0, maxRelays)
  67    }
  68  
  69    /**
  70     * Select relays for fetching events
  71     * Prioritizes read relays from the user's relay list
  72     */
  73    selectForFetching(
  74      relayList: RelayList | null,
  75      options: RelaySelectorOptions = {}
  76    ): RelayUrl[] {
  77      const { maxRelays = 5, preferSecure = true, excludeOnion = false } = options
  78  
  79      const candidates: RelayUrl[] = []
  80  
  81      // Add user's read relays
  82      if (relayList) {
  83        const readRelays = relayList.getReadRelays()
  84        candidates.push(...readRelays)
  85      }
  86  
  87      // Add default relays if needed
  88      if (options.includeDefaultRelays || candidates.length === 0) {
  89        for (const relay of this.defaultRelays) {
  90          if (!candidates.some((c) => c.equals(relay))) {
  91            candidates.push(relay)
  92          }
  93        }
  94      }
  95  
  96      // Filter and sort
  97      let filtered = candidates
  98      if (excludeOnion) {
  99        filtered = filtered.filter((r) => !r.isOnion)
 100      }
 101  
 102      if (preferSecure) {
 103        filtered.sort((a, b) => {
 104          if (a.isSecure && !b.isSecure) return -1
 105          if (!a.isSecure && b.isSecure) return 1
 106          return 0
 107        })
 108      }
 109  
 110      return filtered.slice(0, maxRelays)
 111    }
 112  
 113    /**
 114     * Select relays for publishing to specific users' inboxes
 115     * Includes mentioned users' read relays
 116     */
 117    selectForMentions(
 118      authorRelayList: RelayList | null,
 119      mentionedRelayLists: RelayList[],
 120      options: RelaySelectorOptions = {}
 121    ): RelayUrl[] {
 122      const { maxRelays = 8 } = options
 123  
 124      const relaySet = new Map<string, RelayUrl>()
 125  
 126      // Add author's write relays first
 127      if (authorRelayList) {
 128        for (const relay of authorRelayList.getWriteRelays()) {
 129          relaySet.set(relay.value, relay)
 130        }
 131      }
 132  
 133      // Add mentioned users' read relays
 134      for (const relayList of mentionedRelayLists) {
 135        for (const relay of relayList.getReadRelays()) {
 136          relaySet.set(relay.value, relay)
 137        }
 138      }
 139  
 140      // Add defaults if needed
 141      if (options.includeDefaultRelays || relaySet.size === 0) {
 142        for (const relay of this.defaultRelays) {
 143          relaySet.set(relay.value, relay)
 144        }
 145      }
 146  
 147      const candidates = Array.from(relaySet.values())
 148  
 149      // Filter onion if needed
 150      let filtered = candidates
 151      if (options.excludeOnion) {
 152        filtered = filtered.filter((r) => !r.isOnion)
 153      }
 154  
 155      return filtered.slice(0, maxRelays)
 156    }
 157  
 158    /**
 159     * Get relay URLs as strings (for compatibility with existing code)
 160     */
 161    selectForPublishingUrls(
 162      relayList: RelayList | null,
 163      options: RelaySelectorOptions = {}
 164    ): string[] {
 165      return this.selectForPublishing(relayList, options).map((r) => r.value)
 166    }
 167  
 168    /**
 169     * Get relay URLs as strings for fetching
 170     */
 171    selectForFetchingUrls(
 172      relayList: RelayList | null,
 173      options: RelaySelectorOptions = {}
 174    ): string[] {
 175      return this.selectForFetching(relayList, options).map((r) => r.value)
 176    }
 177  }
 178  
 179  /**
 180   * Create a RelaySelector with default relays
 181   */
 182  export function createRelaySelector(defaultRelayUrls: string[]): RelaySelector {
 183    const defaultRelays = defaultRelayUrls
 184      .map((url) => RelayUrl.tryCreate(url))
 185      .filter((r): r is RelayUrl => r !== null)
 186  
 187    return new RelaySelector(defaultRelays)
 188  }
 189