@libp2p/circuit-relay-v2 2.1.4 → 2.1.5-3bc9769b8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. package/dist/index.min.js +4 -4
  2. package/dist/src/constants.d.ts +0 -5
  3. package/dist/src/constants.d.ts.map +1 -1
  4. package/dist/src/constants.js +0 -5
  5. package/dist/src/constants.js.map +1 -1
  6. package/dist/src/errors.d.ts +22 -0
  7. package/dist/src/errors.d.ts.map +1 -1
  8. package/dist/src/errors.js +22 -0
  9. package/dist/src/errors.js.map +1 -1
  10. package/dist/src/index.d.ts +16 -1
  11. package/dist/src/index.d.ts.map +1 -1
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/pb/index.d.ts +12 -1
  14. package/dist/src/pb/index.d.ts.map +1 -1
  15. package/dist/src/pb/index.js +78 -2
  16. package/dist/src/pb/index.js.map +1 -1
  17. package/dist/src/server/index.d.ts.map +1 -1
  18. package/dist/src/server/index.js +19 -10
  19. package/dist/src/server/index.js.map +1 -1
  20. package/dist/src/server/reservation-store.d.ts +5 -9
  21. package/dist/src/server/reservation-store.d.ts.map +1 -1
  22. package/dist/src/server/reservation-store.js +32 -33
  23. package/dist/src/server/reservation-store.js.map +1 -1
  24. package/dist/src/server/reservation-voucher.d.ts +1 -1
  25. package/dist/src/transport/discovery.d.ts +1 -0
  26. package/dist/src/transport/discovery.d.ts.map +1 -1
  27. package/dist/src/transport/discovery.js +38 -8
  28. package/dist/src/transport/discovery.js.map +1 -1
  29. package/dist/src/transport/index.d.ts +0 -6
  30. package/dist/src/transport/index.d.ts.map +1 -1
  31. package/dist/src/transport/index.js.map +1 -1
  32. package/dist/src/transport/listener.d.ts +3 -0
  33. package/dist/src/transport/listener.d.ts.map +1 -1
  34. package/dist/src/transport/listener.js +57 -27
  35. package/dist/src/transport/listener.js.map +1 -1
  36. package/dist/src/transport/reservation-store.d.ts +37 -14
  37. package/dist/src/transport/reservation-store.d.ts.map +1 -1
  38. package/dist/src/transport/reservation-store.js +144 -74
  39. package/dist/src/transport/reservation-store.js.map +1 -1
  40. package/dist/src/transport/transport.d.ts +2 -13
  41. package/dist/src/transport/transport.d.ts.map +1 -1
  42. package/dist/src/transport/transport.js +30 -54
  43. package/dist/src/transport/transport.js.map +1 -1
  44. package/dist/src/utils.d.ts +10 -1
  45. package/dist/src/utils.d.ts.map +1 -1
  46. package/dist/src/utils.js +22 -8
  47. package/dist/src/utils.js.map +1 -1
  48. package/package.json +14 -12
  49. package/src/constants.ts +0 -7
  50. package/src/errors.ts +25 -0
  51. package/src/index.ts +19 -1
  52. package/src/pb/index.proto +20 -1
  53. package/src/pb/index.ts +99 -3
  54. package/src/server/index.ts +21 -11
  55. package/src/server/reservation-store.ts +38 -42
  56. package/src/server/reservation-voucher.ts +2 -2
  57. package/src/transport/discovery.ts +47 -9
  58. package/src/transport/index.ts +0 -7
  59. package/src/transport/listener.ts +75 -35
  60. package/src/transport/reservation-store.ts +209 -95
  61. package/src/transport/transport.ts +34 -76
  62. package/src/utils.ts +29 -8
  63. package/dist/typedoc-urls.json +0 -23
@@ -1,15 +1,18 @@
1
- import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
1
+ import { ListenError, TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
2
2
  import { PeerMap } from '@libp2p/peer-collections'
3
- import { createBloomFilter } from '@libp2p/utils/filters'
3
+ import { createScalableCuckooFilter } from '@libp2p/utils/filters'
4
4
  import { PeerQueue } from '@libp2p/utils/peer-queue'
5
5
  import { multiaddr } from '@multiformats/multiaddr'
6
+ import { Circuit } from '@multiformats/multiaddr-matcher'
6
7
  import { pbStream } from 'it-protobuf-stream'
7
- import { DEFAULT_MAX_RESERVATION_QUEUE_LENGTH, DEFAULT_RESERVATION_COMPLETION_TIMEOUT, DEFAULT_RESERVATION_CONCURRENCY, KEEP_ALIVE_TAG, RELAY_TAG, RELAY_V2_HOP_CODEC } from '../constants.js'
8
+ import { nanoid } from 'nanoid'
9
+ import { DEFAULT_MAX_RESERVATION_QUEUE_LENGTH, DEFAULT_RESERVATION_COMPLETION_TIMEOUT, DEFAULT_RESERVATION_CONCURRENCY, KEEP_ALIVE_TAG, RELAY_V2_HOP_CODEC } from '../constants.js'
10
+ import { DoubleRelayError, HadEnoughRelaysError, RelayQueueFullError } from '../errors.js'
8
11
  import { HopMessage, Status } from '../pb/index.js'
9
12
  import { getExpirationMilliseconds } from '../utils.js'
10
13
  import type { Reservation } from '../pb/index.js'
11
- import type { TypedEventTarget, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, PeerId, PeerStore, Startable, Metrics, Peer } from '@libp2p/interface'
12
- import type { ConnectionManager, TransportManager } from '@libp2p/interface-internal'
14
+ import type { AbortOptions, TypedEventTarget, Libp2pEvents, ComponentLogger, Logger, PeerId, PeerStore, Startable, Metrics, Peer, Connection } from '@libp2p/interface'
15
+ import type { ConnectionManager } from '@libp2p/interface-internal'
13
16
  import type { Filter } from '@libp2p/utils/filters'
14
17
 
15
18
  // allow refreshing a relay reservation if it will expire in the next 10 minutes
@@ -24,7 +27,6 @@ const REFRESH_TIMEOUT_MIN = 30 * 1000
24
27
  export interface ReservationStoreComponents {
25
28
  peerId: PeerId
26
29
  connectionManager: ConnectionManager
27
- transportManager: TransportManager
28
30
  peerStore: PeerStore
29
31
  events: TypedEventTarget<Libp2pEvents>
30
32
  logger: ComponentLogger
@@ -44,11 +46,6 @@ export interface ReservationStoreInit {
44
46
  */
45
47
  reservationConcurrency?: number
46
48
 
47
- /**
48
- * How many discovered relays to allow in the reservation store
49
- */
50
- discoverRelays?: number
51
-
52
49
  /**
53
50
  * Limit the number of potential relays we will dial
54
51
  *
@@ -66,37 +63,60 @@ export interface ReservationStoreInit {
66
63
 
67
64
  export type RelayType = 'discovered' | 'configured'
68
65
 
69
- interface RelayEntry {
66
+ export interface DiscoveredRelayEntry {
70
67
  timeout: ReturnType<typeof setTimeout>
71
- type: RelayType
68
+ type: 'discovered'
72
69
  reservation: Reservation
73
70
 
74
71
  /**
75
72
  * Stores the id of the connection we have to the relay
76
73
  */
77
74
  connection: string
75
+
76
+ /**
77
+ * Stores the identifier returned when the reservation was requested
78
+ */
79
+ id: string
80
+ }
81
+
82
+ export interface ConfiguredRelayEntry {
83
+ timeout: ReturnType<typeof setTimeout>
84
+ type: 'configured'
85
+ reservation: Reservation
86
+
87
+ /**
88
+ * Stores the id of the connection we have to the relay
89
+ */
90
+ connection: string
91
+ }
92
+
93
+ export type RelayEntry = DiscoveredRelayEntry | ConfiguredRelayEntry
94
+
95
+ export interface RelayReservation {
96
+ relay: PeerId
97
+ details: RelayEntry
78
98
  }
79
99
 
80
100
  export interface ReservationStoreEvents {
81
101
  'relay:not-enough-relays': CustomEvent
82
- 'relay:removed': CustomEvent<PeerId>
83
- 'relay:created-reservation': CustomEvent<PeerId>
102
+ 'relay:found-enough-relays': CustomEvent
103
+ 'relay:removed': CustomEvent<RelayReservation>
104
+ 'relay:created-reservation': CustomEvent<RelayReservation>
84
105
  }
85
106
 
86
107
  export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents> implements Startable {
87
108
  private readonly peerId: PeerId
88
109
  private readonly connectionManager: ConnectionManager
89
- private readonly transportManager: TransportManager
90
110
  private readonly peerStore: PeerStore
91
111
  private readonly events: TypedEventTarget<Libp2pEvents>
92
- private readonly reserveQueue: PeerQueue
112
+ private readonly reserveQueue: PeerQueue<RelayReservation>
93
113
  private readonly reservations: PeerMap<RelayEntry>
94
- private readonly maxDiscoveredRelays: number
114
+ private readonly pendingReservations: string[]
95
115
  private readonly maxReservationQueueLength: number
96
116
  private readonly reservationCompletionTimeout: number
97
117
  private started: boolean
98
118
  private readonly log: Logger
99
- private readonly relayFilter: Filter
119
+ private relayFilter: Filter
100
120
 
101
121
  constructor (components: ReservationStoreComponents, init?: ReservationStoreInit) {
102
122
  super()
@@ -104,15 +124,14 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
104
124
  this.log = components.logger.forComponent('libp2p:circuit-relay:transport:reservation-store')
105
125
  this.peerId = components.peerId
106
126
  this.connectionManager = components.connectionManager
107
- this.transportManager = components.transportManager
108
127
  this.peerStore = components.peerStore
109
128
  this.events = components.events
110
129
  this.reservations = new PeerMap()
111
- this.maxDiscoveredRelays = init?.discoverRelays ?? 0
130
+ this.pendingReservations = []
112
131
  this.maxReservationQueueLength = init?.maxReservationQueueLength ?? DEFAULT_MAX_RESERVATION_QUEUE_LENGTH
113
132
  this.reservationCompletionTimeout = init?.reservationCompletionTimeout ?? DEFAULT_RESERVATION_COMPLETION_TIMEOUT
114
133
  this.started = false
115
- this.relayFilter = createBloomFilter(100)
134
+ this.relayFilter = createScalableCuckooFilter(100)
116
135
 
117
136
  // ensure we don't listen on multiple relays simultaneously
118
137
  this.reserveQueue = new PeerQueue({
@@ -132,7 +151,7 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
132
151
  return
133
152
  }
134
153
 
135
- this.#removeReservation(evt.detail.remotePeer, reservation)
154
+ this.#removeReservation(evt.detail.remotePeer)
136
155
  .catch(err => {
137
156
  this.log('could not remove relay %p - %e', evt.detail, err)
138
157
  })
@@ -153,7 +172,7 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
153
172
  .then(async () => {
154
173
  const relayPeers: Peer[] = await this.peerStore.all({
155
174
  filters: [(peer) => {
156
- return peer.tags.has(RELAY_TAG)
175
+ return peer.tags.has(KEEP_ALIVE_TAG)
157
176
  }]
158
177
  })
159
178
 
@@ -164,17 +183,18 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
164
183
  relayPeers.map(async peer => {
165
184
  await this.peerStore.merge(peer.id, {
166
185
  tags: {
167
- [RELAY_TAG]: undefined,
168
186
  [KEEP_ALIVE_TAG]: undefined
169
187
  }
170
188
  })
171
189
  })
172
190
  )
173
191
 
174
- if (this.reservations.size < this.maxDiscoveredRelays) {
175
- this.log('not enough relays %d/%d', this.reservations.size, this.maxDiscoveredRelays)
176
- this.safeDispatchEvent('relay:not-enough-relays', {})
177
- }
192
+ this.log('redialing %d old relays', relayPeers.length)
193
+ await Promise.all(
194
+ relayPeers.map(async peer => this.addRelay(peer.id, 'discovered'))
195
+ )
196
+
197
+ this.#checkReservationCount()
178
198
  })
179
199
  .catch(err => {
180
200
  this.log.error(err)
@@ -190,36 +210,46 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
190
210
  this.started = false
191
211
  }
192
212
 
213
+ reserveRelay (): string {
214
+ const id = nanoid()
215
+
216
+ this.pendingReservations.push(id)
217
+
218
+ this.#checkReservationCount()
219
+
220
+ return id
221
+ }
222
+
193
223
  /**
194
224
  * If the number of current relays is beneath the configured `maxReservations`
195
225
  * value, and the passed peer id is not our own, and we have a non-relayed
196
226
  * connection to the remote, and the remote peer speaks the hop protocol, try
197
227
  * to reserve a slot on the remote peer
198
228
  */
199
- async addRelay (peerId: PeerId, type: RelayType): Promise<void> {
229
+ async addRelay (peerId: PeerId, type: RelayType): Promise<RelayReservation> {
200
230
  if (this.peerId.equals(peerId)) {
201
231
  this.log.trace('not trying to use self as relay')
202
- return
232
+ throw new ListenError('Cannot use self as relay')
203
233
  }
204
234
 
205
235
  if (this.reserveQueue.size > this.maxReservationQueueLength) {
206
- this.log.trace('not adding potential relay peer %p as the queue is full', peerId)
207
- return
236
+ throw new RelayQueueFullError('The reservation queue is full')
208
237
  }
209
238
 
210
- if (this.reserveQueue.has(peerId)) {
239
+ const existingJob = this.reserveQueue.find(peerId)
240
+
241
+ if (existingJob != null) {
211
242
  this.log.trace('potential relay peer %p is already in the reservation queue', peerId)
212
- return
243
+ return existingJob.join()
213
244
  }
214
245
 
215
246
  if (this.relayFilter.has(peerId.toMultihash().bytes)) {
216
- this.log.trace('potential relay peer %p has failed previously, not trying again', peerId)
217
- return
247
+ throw new ListenError('The relay was previously invalid')
218
248
  }
219
249
 
220
250
  this.log.trace('try to reserve relay slot with %p', peerId)
221
251
 
222
- await this.reserveQueue.add(async () => {
252
+ return this.reserveQueue.add(async () => {
223
253
  const start = Date.now()
224
254
 
225
255
  try {
@@ -227,24 +257,31 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
227
257
  const existingReservation = this.reservations.get(peerId)
228
258
 
229
259
  if (existingReservation != null) {
230
- if (this.connectionManager.getConnections(peerId).map(conn => conn.id).includes(existingReservation.connection) && getExpirationMilliseconds(existingReservation.reservation.expire) > REFRESH_WINDOW) {
231
- this.log('already have relay reservation with %p but we are still connected and it does not expire soon', peerId)
232
- return
260
+ const connections = this.connectionManager.getConnections(peerId)
261
+ let connected = false
262
+
263
+ if (connections.length === 0) {
264
+ this.log('already have relay reservation with %p but we are no longer connected', peerId)
233
265
  }
234
266
 
235
- this.log('already have relay reservation with %p but the original connection is no longer open', peerId)
236
- await this.#removeReservation(peerId, existingReservation)
237
- }
267
+ if (connections.map(conn => conn.id).includes(existingReservation.connection)) {
268
+ this.log('already have relay reservation with %p and the original connection is still open', peerId)
269
+ connected = true
270
+ }
238
271
 
239
- if (type === 'discovered' && [...this.reservations.values()].reduce((acc, curr) => {
240
- if (curr.type === 'discovered') {
241
- acc++
272
+ if (connected && getExpirationMilliseconds(existingReservation.reservation.expire) > REFRESH_WINDOW) {
273
+ this.log('already have relay reservation with %p but we are still connected and it does not expire soon', peerId)
274
+ return {
275
+ relay: peerId,
276
+ details: existingReservation
277
+ } satisfies RelayReservation
242
278
  }
243
279
 
244
- return acc
245
- }, 0) >= this.maxDiscoveredRelays) {
246
- this.log.trace('already have enough discovered relays')
247
- return
280
+ await this.#removeReservation(peerId)
281
+ }
282
+
283
+ if (type === 'discovered' && this.pendingReservations.length === 0) {
284
+ throw new HadEnoughRelaysError('Not making reservation on discovered relay because we do not need any more relays')
248
285
  }
249
286
 
250
287
  const signal = AbortSignal.timeout(this.reservationCompletionTimeout)
@@ -254,44 +291,67 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
254
291
  signal
255
292
  })
256
293
 
257
- if (connection.remoteAddr.protoNames().includes('p2p-circuit')) {
258
- this.log('not creating reservation over relayed connection')
259
- return
294
+ if (Circuit.matches(connection.remoteAddr)) {
295
+ throw new DoubleRelayError('not creating reservation over relayed connection')
260
296
  }
261
297
 
262
298
  const reservation = await this.#createReservation(connection, {
263
299
  signal
264
300
  })
265
301
 
266
- this.log('created reservation on relay peer %p', peerId)
267
-
268
302
  const expiration = getExpirationMilliseconds(reservation.expire)
269
303
 
304
+ this.log('created reservation on relay peer %p, expiry date is %s', peerId, new Date(Date.now() + expiration).toString())
305
+
270
306
  // sets a lower bound on the timeout, and also don't let it go over
271
307
  // 2^31 - 1 (setTimeout will only accept signed 32 bit integers)
272
308
  const timeoutDuration = Math.min(Math.max(expiration - REFRESH_TIMEOUT, REFRESH_TIMEOUT_MIN), Math.pow(2, 31) - 1)
273
309
 
274
310
  const timeout = setTimeout(() => {
275
- this.addRelay(peerId, type).catch(err => {
276
- this.log.error('could not refresh reservation to relay %p', peerId, err)
277
- })
311
+ this.log('refresh reservation to relay %p', peerId)
312
+
313
+ this.addRelay(peerId, type)
314
+ .catch(async err => {
315
+ this.log.error('could not refresh reservation to relay %p - %e', peerId, err)
316
+ await this.#removeReservation(peerId)
317
+ })
318
+ .catch(err => {
319
+ this.log.error('could not remove expired reservation to relay %p - %e', peerId, err)
320
+ })
278
321
  }, timeoutDuration)
279
322
 
323
+ let res: RelayEntry
324
+
325
+ // assign a reservation id if one was requested
326
+ if (type === 'discovered') {
327
+ const id = this.pendingReservations.pop()
328
+
329
+ if (id == null) {
330
+ throw new HadEnoughRelaysError('Made reservation on relay but did not need any more discovered relays')
331
+ }
332
+
333
+ res = {
334
+ timeout,
335
+ reservation,
336
+ type,
337
+ connection: connection.id,
338
+ id
339
+ }
340
+ } else {
341
+ res = {
342
+ timeout,
343
+ reservation,
344
+ type,
345
+ connection: connection.id
346
+ }
347
+ }
348
+
280
349
  // we've managed to create a reservation successfully
281
- this.reservations.set(peerId, {
282
- timeout,
283
- reservation,
284
- type,
285
- connection: connection.id
286
- })
350
+ this.reservations.set(peerId, res)
287
351
 
288
352
  // ensure we don't close the connection to the relay
289
353
  await this.peerStore.merge(peerId, {
290
354
  tags: {
291
- [RELAY_TAG]: {
292
- value: 1,
293
- ttl: expiration
294
- },
295
355
  [KEEP_ALIVE_TAG]: {
296
356
  value: 1,
297
357
  ttl: expiration
@@ -299,27 +359,37 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
299
359
  }
300
360
  })
301
361
 
302
- // listen on multiaddr that only the circuit transport is listening for
303
- await this.transportManager.listen([multiaddr(`/p2p/${peerId.toString()}/p2p-circuit`)])
362
+ // check to see if we have discovered enough relays
363
+ this.#checkReservationCount()
364
+
365
+ const result: RelayReservation = {
366
+ relay: peerId,
367
+ details: res
368
+ }
304
369
 
305
370
  this.safeDispatchEvent('relay:created-reservation', {
306
- detail: peerId
371
+ detail: result
307
372
  })
308
- } catch (err) {
309
- this.log.error('could not reserve slot on %p after %dms', peerId, Date.now() - start, err)
310
373
 
311
- // cancel the renewal timeout if it's been set
312
- const reservation = this.reservations.get(peerId)
374
+ return result
375
+ } catch (err: any) {
376
+ if (!(type === 'discovered' && err.name === 'HadEnoughRelaysError')) {
377
+ this.log.error('could not reserve slot on %p after %dms - %e', peerId, Date.now() - start, err)
378
+ }
313
379
 
314
- if (reservation != null) {
315
- clearTimeout(reservation.timeout)
380
+ // don't try this peer again if dialing failed or they do not support
381
+ // the hop protocol
382
+ if (err.name === 'DialError' || err.name === 'UnsupportedProtocolError') {
383
+ this.relayFilter.add(peerId.toMultihash().bytes)
316
384
  }
317
385
 
318
386
  // if listening failed, remove the reservation
319
- this.reservations.delete(peerId)
387
+ this.#removeReservation(peerId)
388
+ .catch(err => {
389
+ this.log.error('could not remove reservation on %p after reserving slot failed - %e', peerId, err)
390
+ })
320
391
 
321
- // don't try this peer again
322
- this.relayFilter.add(peerId.toMultihash().bytes)
392
+ throw err
323
393
  }
324
394
  }, {
325
395
  peerId
@@ -334,16 +404,26 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
334
404
  return this.reservations.get(peerId)?.reservation
335
405
  }
336
406
 
337
- reservationCount (): number {
338
- return this.reservations.size
407
+ reservationCount (type?: RelayType): number {
408
+ if (type == null) {
409
+ return this.reservations.size
410
+ }
411
+
412
+ return [...this.reservations.values()].reduce((acc, curr) => {
413
+ if (curr.type === type) {
414
+ acc++
415
+ }
416
+
417
+ return acc
418
+ }, 0)
339
419
  }
340
420
 
341
- async cancelReservations (): Promise<void> {
342
- await Promise.all(
343
- [...this.reservations.entries()].map(async ([peerId, reservation]) => {
344
- await this.#removeReservation(peerId, reservation)
345
- })
346
- )
421
+ cancelReservations (): void {
422
+ [...this.reservations.values()].forEach(reservation => {
423
+ clearTimeout(reservation.timeout)
424
+ })
425
+
426
+ this.reservations.clear()
347
427
  }
348
428
 
349
429
  async #createReservation (connection: Connection, options: AbortOptions): Promise<Reservation> {
@@ -353,11 +433,14 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
353
433
  const stream = await connection.newStream(RELAY_V2_HOP_CODEC, options)
354
434
  const pbstr = pbStream(stream)
355
435
  const hopstr = pbstr.pb(HopMessage)
436
+
437
+ this.log.trace('send RESERVE to %p', connection.remotePeer)
356
438
  await hopstr.write({ type: HopMessage.Type.RESERVE }, options)
357
439
 
358
440
  let response: HopMessage
359
441
 
360
442
  try {
443
+ this.log.trace('read response from %p', connection.remotePeer)
361
444
  response = await hopstr.read(options)
362
445
  } catch (err: any) {
363
446
  stream.abort(err)
@@ -368,6 +451,8 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
368
451
  }
369
452
  }
370
453
 
454
+ this.log.trace('read response from %p', response)
455
+
371
456
  if (response.status === Status.OK && response.reservation != null) {
372
457
  // check that the returned relay has the relay address - this can be
373
458
  // omitted when requesting a reservation from a go-libp2p relay we
@@ -405,24 +490,53 @@ export class ReservationStore extends TypedEventEmitter<ReservationStoreEvents>
405
490
  /**
406
491
  * Remove listen relay
407
492
  */
408
- async #removeReservation (peerId: PeerId, reservation: RelayEntry): Promise<void> {
493
+ async #removeReservation (peerId: PeerId): Promise<void> {
494
+ const reservation = this.reservations.get(peerId)
495
+
496
+ if (reservation == null) {
497
+ return
498
+ }
499
+
409
500
  this.log('removing relay reservation with %p from local store', peerId)
410
501
  clearTimeout(reservation.timeout)
411
502
  this.reservations.delete(peerId)
412
503
 
504
+ // discover a new relay for this discovery request
505
+ if (reservation.type === 'discovered') {
506
+ this.pendingReservations.push(
507
+ reservation.id
508
+ )
509
+ }
510
+
413
511
  // untag the relay
414
512
  await this.peerStore.merge(peerId, {
415
513
  tags: {
416
- [RELAY_TAG]: undefined,
417
514
  [KEEP_ALIVE_TAG]: undefined
418
515
  }
419
516
  })
420
517
 
421
- this.safeDispatchEvent('relay:removed', { detail: peerId })
518
+ this.safeDispatchEvent('relay:removed', {
519
+ detail: {
520
+ relay: peerId,
521
+ details: reservation
522
+ }
523
+ })
524
+
525
+ // maybe trigger discovery of new relays
526
+ this.#checkReservationCount()
527
+ }
528
+
529
+ #checkReservationCount (): void {
530
+ if (this.pendingReservations.length === 0) {
531
+ this.log.trace('have discovered enough relays')
532
+ this.reserveQueue.clear()
533
+ this.safeDispatchEvent('relay:found-enough-relays')
422
534
 
423
- if (this.reservations.size < this.maxDiscoveredRelays) {
424
- this.log('not enough relays %d/%d', this.reservations.size, this.maxDiscoveredRelays)
425
- this.safeDispatchEvent('relay:not-enough-relays', {})
535
+ return
426
536
  }
537
+
538
+ this.relayFilter = createScalableCuckooFilter(100)
539
+ this.log('not discovered enough relays %d/%d', this.reservations.size, this.pendingReservations.length)
540
+ this.safeDispatchEvent('relay:not-enough-relays')
427
541
  }
428
542
  }