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