RelayStrategy.ts raw

   1  import { Pubkey } from '../shared/value-objects/Pubkey'
   2  import { RelayUrl } from '../shared/value-objects/RelayUrl'
   3  
   4  /**
   5   * Strategy types for relay selection
   6   */
   7  export type RelayStrategyType =
   8    | 'user_write_relays' // Use owner's write relays
   9    | 'user_read_relays' // Use owner's read relays
  10    | 'author_write_relays' // Use each author's write relays (NIP-65 optimization)
  11    | 'specific_relays' // Use a specific relay set
  12    | 'single_relay' // Use a single relay
  13    | 'big_relays' // Use fallback big relays
  14  
  15  /**
  16   * Interface for resolving relay lists for pubkeys
  17   */
  18  export interface RelayListResolver {
  19    getWriteRelays(pubkey: Pubkey): Promise<RelayUrl[]>
  20    getReadRelays(pubkey: Pubkey): Promise<RelayUrl[]>
  21    getBigRelays(): RelayUrl[]
  22  }
  23  
  24  /**
  25   * RelayStrategy Value Object
  26   *
  27   * Determines which relays to query for a given feed configuration.
  28   * Immutable and encapsulates relay selection logic.
  29   */
  30  export class RelayStrategy {
  31    private constructor(
  32      private readonly _type: RelayStrategyType,
  33      private readonly _relays: readonly RelayUrl[],
  34      private readonly _relaySetId: string | null
  35    ) {}
  36  
  37    /**
  38     * Use the current user's write relays
  39     */
  40    static userWriteRelays(): RelayStrategy {
  41      return new RelayStrategy('user_write_relays', [], null)
  42    }
  43  
  44    /**
  45     * Use the current user's read relays
  46     */
  47    static userReadRelays(): RelayStrategy {
  48      return new RelayStrategy('user_read_relays', [], null)
  49    }
  50  
  51    /**
  52     * Use each author's write relays (for optimized following feeds)
  53     */
  54    static authorWriteRelays(): RelayStrategy {
  55      return new RelayStrategy('author_write_relays', [], null)
  56    }
  57  
  58    /**
  59     * Use specific relays from a relay set
  60     */
  61    static specific(relays: RelayUrl[], setId?: string): RelayStrategy {
  62      if (relays.length === 0) {
  63        throw new Error('Specific relay strategy requires at least one relay')
  64      }
  65      return new RelayStrategy('specific_relays', [...relays], setId ?? null)
  66    }
  67  
  68    /**
  69     * Use a single relay
  70     */
  71    static single(relay: RelayUrl): RelayStrategy {
  72      return new RelayStrategy('single_relay', [relay], null)
  73    }
  74  
  75    /**
  76     * Use fallback big relays
  77     */
  78    static bigRelays(): RelayStrategy {
  79      return new RelayStrategy('big_relays', [], null)
  80    }
  81  
  82    /**
  83     * Create from relay URLs (convenience factory)
  84     */
  85    static fromUrls(urls: string[], setId?: string): RelayStrategy {
  86      const relays = urls
  87        .map((url) => RelayUrl.tryCreate(url))
  88        .filter((r): r is RelayUrl => r !== null)
  89  
  90      if (relays.length === 0) {
  91        return RelayStrategy.bigRelays()
  92      }
  93  
  94      if (relays.length === 1) {
  95        return RelayStrategy.single(relays[0])
  96      }
  97  
  98      return RelayStrategy.specific(relays, setId)
  99    }
 100  
 101    get type(): RelayStrategyType {
 102      return this._type
 103    }
 104  
 105    get relays(): readonly RelayUrl[] {
 106      return this._relays
 107    }
 108  
 109    get relaySetId(): string | null {
 110      return this._relaySetId
 111    }
 112  
 113    /**
 114     * Check if this strategy has static relays (doesn't need resolution)
 115     */
 116    get hasStaticRelays(): boolean {
 117      return (
 118        this._type === 'specific_relays' ||
 119        this._type === 'single_relay' ||
 120        this._type === 'big_relays'
 121      )
 122    }
 123  
 124    /**
 125     * Check if this strategy requires per-author relay resolution
 126     */
 127    get requiresPerAuthorResolution(): boolean {
 128      return this._type === 'author_write_relays'
 129    }
 130  
 131    /**
 132     * Resolve relay URLs based on the strategy
 133     *
 134     * For static strategies, returns the configured relays.
 135     * For dynamic strategies, uses the resolver to look up relays.
 136     */
 137    async resolve(
 138      resolver: RelayListResolver,
 139      ownerPubkey?: Pubkey
 140    ): Promise<RelayUrl[]> {
 141      switch (this._type) {
 142        case 'specific_relays':
 143        case 'single_relay':
 144          return [...this._relays]
 145  
 146        case 'big_relays':
 147          return resolver.getBigRelays()
 148  
 149        case 'user_write_relays':
 150          if (!ownerPubkey) {
 151            return resolver.getBigRelays()
 152          }
 153          return resolver.getWriteRelays(ownerPubkey)
 154  
 155        case 'user_read_relays':
 156          if (!ownerPubkey) {
 157            return resolver.getBigRelays()
 158          }
 159          return resolver.getReadRelays(ownerPubkey)
 160  
 161        case 'author_write_relays':
 162          // This requires per-author resolution, return empty
 163          // The caller should use resolveForAuthors instead
 164          return []
 165      }
 166    }
 167  
 168    /**
 169     * Resolve relay URLs for multiple authors (for optimized subscriptions)
 170     *
 171     * Returns a map of relay URL -> list of pubkeys to query at that relay.
 172     * This enables NIP-65 mailbox-style optimized queries.
 173     */
 174    async resolveForAuthors(
 175      resolver: RelayListResolver,
 176      authors: Pubkey[]
 177    ): Promise<Map<string, Pubkey[]>> {
 178      const relayToAuthors = new Map<string, Pubkey[]>()
 179  
 180      if (this._type !== 'author_write_relays') {
 181        // For non-author strategies, resolve once and map all authors
 182        const relays = await this.resolve(resolver)
 183        for (const relay of relays) {
 184          relayToAuthors.set(relay.value, [...authors])
 185        }
 186        return relayToAuthors
 187      }
 188  
 189      // For author_write_relays, resolve per author
 190      const bigRelays = resolver.getBigRelays()
 191  
 192      for (const author of authors) {
 193        let authorRelays = await resolver.getWriteRelays(author)
 194  
 195        // Fall back to big relays if no write relays found
 196        if (authorRelays.length === 0) {
 197          authorRelays = bigRelays
 198        }
 199  
 200        for (const relay of authorRelays) {
 201          const existing = relayToAuthors.get(relay.value)
 202          if (existing) {
 203            existing.push(author)
 204          } else {
 205            relayToAuthors.set(relay.value, [author])
 206          }
 207        }
 208      }
 209  
 210      return relayToAuthors
 211    }
 212  
 213    equals(other: RelayStrategy): boolean {
 214      if (this._type !== other._type) return false
 215      if (this._relaySetId !== other._relaySetId) return false
 216      if (this._relays.length !== other._relays.length) return false
 217      for (let i = 0; i < this._relays.length; i++) {
 218        if (!this._relays[i].equals(other._relays[i])) return false
 219      }
 220      return true
 221    }
 222  
 223    toString(): string {
 224      switch (this._type) {
 225        case 'user_write_relays':
 226          return 'user_write_relays'
 227        case 'user_read_relays':
 228          return 'user_read_relays'
 229        case 'author_write_relays':
 230          return 'author_write_relays'
 231        case 'big_relays':
 232          return 'big_relays'
 233        case 'single_relay':
 234          return `single:${this._relays[0]?.value}`
 235        case 'specific_relays':
 236          return this._relaySetId
 237            ? `set:${this._relaySetId}`
 238            : `specific:[${this._relays.map((r) => r.value).join(',')}]`
 239      }
 240    }
 241  }
 242