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