@libp2p/utils 4.0.7 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/src/abstract-stream.d.ts +144 -0
  3. package/dist/src/abstract-stream.d.ts.map +1 -0
  4. package/dist/src/abstract-stream.js +340 -0
  5. package/dist/src/abstract-stream.js.map +1 -0
  6. package/dist/src/address-sort.d.ts +1 -1
  7. package/dist/src/address-sort.d.ts.map +1 -1
  8. package/dist/src/address-sort.js.map +1 -1
  9. package/dist/src/array-equals.js.map +1 -1
  10. package/dist/src/close-source.d.ts +4 -0
  11. package/dist/src/close-source.d.ts.map +1 -0
  12. package/dist/src/close-source.js +11 -0
  13. package/dist/src/close-source.js.map +1 -0
  14. package/dist/src/ip-port-to-multiaddr.d.ts.map +1 -1
  15. package/dist/src/ip-port-to-multiaddr.js +2 -6
  16. package/dist/src/ip-port-to-multiaddr.js.map +1 -1
  17. package/dist/src/is-promise.d.ts +2 -0
  18. package/dist/src/is-promise.d.ts.map +1 -0
  19. package/dist/src/is-promise.js +9 -0
  20. package/dist/src/is-promise.js.map +1 -0
  21. package/dist/src/multiaddr/is-private.js.map +1 -1
  22. package/dist/src/peer-job-queue.d.ts +33 -0
  23. package/dist/src/peer-job-queue.d.ts.map +1 -0
  24. package/dist/src/peer-job-queue.js +81 -0
  25. package/dist/src/peer-job-queue.js.map +1 -0
  26. package/dist/src/stream-to-ma-conn.d.ts +2 -1
  27. package/dist/src/stream-to-ma-conn.d.ts.map +1 -1
  28. package/dist/src/stream-to-ma-conn.js +57 -35
  29. package/dist/src/stream-to-ma-conn.js.map +1 -1
  30. package/dist/src/tracked-map.d.ts +17 -0
  31. package/dist/src/tracked-map.d.ts.map +1 -0
  32. package/dist/src/tracked-map.js +38 -0
  33. package/dist/src/tracked-map.js.map +1 -0
  34. package/dist/typedoc-urls.json +12 -0
  35. package/package.json +43 -9
  36. package/src/abstract-stream.ts +508 -0
  37. package/src/address-sort.ts +1 -1
  38. package/src/close-source.ts +14 -0
  39. package/src/ip-port-to-multiaddr.ts +2 -7
  40. package/src/is-promise.ts +9 -0
  41. package/src/peer-job-queue.ts +118 -0
  42. package/src/stream-to-ma-conn.ts +59 -36
  43. package/src/tracked-map.ts +65 -0
@@ -0,0 +1,508 @@
1
+ import { CodeError } from '@libp2p/interface'
2
+ import { type Pushable, pushable } from 'it-pushable'
3
+ import defer, { type DeferredPromise } from 'p-defer'
4
+ import { raceSignal } from 'race-signal'
5
+ import { Uint8ArrayList } from 'uint8arraylist'
6
+ import { closeSource } from './close-source.js'
7
+ import type { AbortOptions, Direction, ReadStatus, Stream, StreamStatus, StreamTimeline, WriteStatus } from '@libp2p/interface'
8
+ import type { Logger } from '@libp2p/logger'
9
+ import type { Source } from 'it-stream-types'
10
+
11
+ const ERR_STREAM_RESET = 'ERR_STREAM_RESET'
12
+ const ERR_SINK_INVALID_STATE = 'ERR_SINK_INVALID_STATE'
13
+ const DEFAULT_SEND_CLOSE_WRITE_TIMEOUT = 5000
14
+
15
+ export interface AbstractStreamInit {
16
+ /**
17
+ * A unique identifier for this stream
18
+ */
19
+ id: string
20
+
21
+ /**
22
+ * The stream direction
23
+ */
24
+ direction: Direction
25
+
26
+ /**
27
+ * A Logger implementation used to log stream-specific information
28
+ */
29
+ log: Logger
30
+
31
+ /**
32
+ * User specific stream metadata
33
+ */
34
+ metadata?: Record<string, unknown>
35
+
36
+ /**
37
+ * Invoked when the stream ends
38
+ */
39
+ onEnd?(err?: Error | undefined): void
40
+
41
+ /**
42
+ * Invoked when the readable end of the stream is closed
43
+ */
44
+ onCloseRead?(): void
45
+
46
+ /**
47
+ * Invoked when the writable end of the stream is closed
48
+ */
49
+ onCloseWrite?(): void
50
+
51
+ /**
52
+ * Invoked when the the stream has been reset by the remote
53
+ */
54
+ onReset?(): void
55
+
56
+ /**
57
+ * Invoked when the the stream has errored
58
+ */
59
+ onAbort?(err: Error): void
60
+
61
+ /**
62
+ * How long to wait in ms for stream data to be written to the underlying
63
+ * connection when closing the writable end of the stream. (default: 500)
64
+ */
65
+ closeTimeout?: number
66
+
67
+ /**
68
+ * After the stream sink has closed, a limit on how long it takes to send
69
+ * a close-write message to the remote peer.
70
+ */
71
+ sendCloseWriteTimeout?: number
72
+ }
73
+
74
+ function isPromise <T = unknown> (thing: any): thing is Promise<T> {
75
+ if (thing == null) {
76
+ return false
77
+ }
78
+
79
+ return typeof thing.then === 'function' &&
80
+ typeof thing.catch === 'function' &&
81
+ typeof thing.finally === 'function'
82
+ }
83
+
84
+ export abstract class AbstractStream implements Stream {
85
+ public id: string
86
+ public direction: Direction
87
+ public timeline: StreamTimeline
88
+ public protocol?: string
89
+ public metadata: Record<string, unknown>
90
+ public source: AsyncGenerator<Uint8ArrayList, void, unknown>
91
+ public status: StreamStatus
92
+ public readStatus: ReadStatus
93
+ public writeStatus: WriteStatus
94
+ public readonly log: Logger
95
+
96
+ private readonly sinkController: AbortController
97
+ private readonly sinkEnd: DeferredPromise<void>
98
+ private readonly closed: DeferredPromise<void>
99
+ private endErr: Error | undefined
100
+ private readonly streamSource: Pushable<Uint8ArrayList>
101
+ private readonly onEnd?: (err?: Error | undefined) => void
102
+ private readonly onCloseRead?: () => void
103
+ private readonly onCloseWrite?: () => void
104
+ private readonly onReset?: () => void
105
+ private readonly onAbort?: (err: Error) => void
106
+ private readonly sendCloseWriteTimeout: number
107
+
108
+ constructor (init: AbstractStreamInit) {
109
+ this.sinkController = new AbortController()
110
+ this.sinkEnd = defer()
111
+ this.closed = defer()
112
+ this.log = init.log
113
+
114
+ // stream status
115
+ this.status = 'open'
116
+ this.readStatus = 'ready'
117
+ this.writeStatus = 'ready'
118
+
119
+ this.id = init.id
120
+ this.metadata = init.metadata ?? {}
121
+ this.direction = init.direction
122
+ this.timeline = {
123
+ open: Date.now()
124
+ }
125
+ this.sendCloseWriteTimeout = init.sendCloseWriteTimeout ?? DEFAULT_SEND_CLOSE_WRITE_TIMEOUT
126
+
127
+ this.onEnd = init.onEnd
128
+ this.onCloseRead = init?.onCloseRead
129
+ this.onCloseWrite = init?.onCloseWrite
130
+ this.onReset = init?.onReset
131
+ this.onAbort = init?.onAbort
132
+
133
+ this.source = this.streamSource = pushable<Uint8ArrayList>({
134
+ onEnd: (err) => {
135
+ if (err != null) {
136
+ this.log.trace('source ended with error', err)
137
+ } else {
138
+ this.log.trace('source ended')
139
+ }
140
+
141
+ this.onSourceEnd(err)
142
+ }
143
+ })
144
+
145
+ // necessary because the libp2p upgrader wraps the sink function
146
+ this.sink = this.sink.bind(this)
147
+ }
148
+
149
+ async sink (source: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
150
+ if (this.writeStatus !== 'ready') {
151
+ throw new CodeError(`writable end state is "${this.writeStatus}" not "ready"`, ERR_SINK_INVALID_STATE)
152
+ }
153
+
154
+ try {
155
+ this.writeStatus = 'writing'
156
+
157
+ const options: AbortOptions = {
158
+ signal: this.sinkController.signal
159
+ }
160
+
161
+ if (this.direction === 'outbound') { // If initiator, open a new stream
162
+ const res = this.sendNewStream(options)
163
+
164
+ if (isPromise(res)) {
165
+ await res
166
+ }
167
+ }
168
+
169
+ const abortListener = (): void => {
170
+ closeSource(source, this.log)
171
+ }
172
+
173
+ try {
174
+ this.sinkController.signal.addEventListener('abort', abortListener)
175
+
176
+ this.log.trace('sink reading from source')
177
+
178
+ for await (let data of source) {
179
+ data = data instanceof Uint8Array ? new Uint8ArrayList(data) : data
180
+
181
+ const res = this.sendData(data, options)
182
+
183
+ if (isPromise(res)) { // eslint-disable-line max-depth
184
+ await res
185
+ }
186
+ }
187
+ } finally {
188
+ this.sinkController.signal.removeEventListener('abort', abortListener)
189
+ }
190
+
191
+ this.log.trace('sink finished reading from source, write status is "%s"', this.writeStatus)
192
+
193
+ if (this.writeStatus === 'writing') {
194
+ this.writeStatus = 'closing'
195
+
196
+ this.log.trace('send close write to remote')
197
+ await this.sendCloseWrite({
198
+ signal: AbortSignal.timeout(this.sendCloseWriteTimeout)
199
+ })
200
+
201
+ this.writeStatus = 'closed'
202
+ }
203
+
204
+ this.onSinkEnd()
205
+ } catch (err: any) {
206
+ this.log.trace('sink ended with error, calling abort with error', err)
207
+ this.abort(err)
208
+
209
+ throw err
210
+ } finally {
211
+ this.log.trace('resolve sink end')
212
+ this.sinkEnd.resolve()
213
+ }
214
+ }
215
+
216
+ protected onSourceEnd (err?: Error): void {
217
+ if (this.timeline.closeRead != null) {
218
+ return
219
+ }
220
+
221
+ this.timeline.closeRead = Date.now()
222
+ this.readStatus = 'closed'
223
+
224
+ if (err != null && this.endErr == null) {
225
+ this.endErr = err
226
+ }
227
+
228
+ this.onCloseRead?.()
229
+
230
+ if (this.timeline.closeWrite != null) {
231
+ this.log.trace('source and sink ended')
232
+ this.timeline.close = Date.now()
233
+
234
+ if (this.status !== 'aborted' && this.status !== 'reset') {
235
+ this.status = 'closed'
236
+ }
237
+
238
+ if (this.onEnd != null) {
239
+ this.onEnd(this.endErr)
240
+ }
241
+
242
+ this.closed.resolve()
243
+ } else {
244
+ this.log.trace('source ended, waiting for sink to end')
245
+ }
246
+ }
247
+
248
+ protected onSinkEnd (err?: Error): void {
249
+ if (this.timeline.closeWrite != null) {
250
+ return
251
+ }
252
+
253
+ this.timeline.closeWrite = Date.now()
254
+ this.writeStatus = 'closed'
255
+
256
+ if (err != null && this.endErr == null) {
257
+ this.endErr = err
258
+ }
259
+
260
+ this.onCloseWrite?.()
261
+
262
+ if (this.timeline.closeRead != null) {
263
+ this.log.trace('sink and source ended')
264
+ this.timeline.close = Date.now()
265
+
266
+ if (this.status !== 'aborted' && this.status !== 'reset') {
267
+ this.status = 'closed'
268
+ }
269
+
270
+ if (this.onEnd != null) {
271
+ this.onEnd(this.endErr)
272
+ }
273
+
274
+ this.closed.resolve()
275
+ } else {
276
+ this.log.trace('sink ended, waiting for source to end')
277
+ }
278
+ }
279
+
280
+ // Close for both Reading and Writing
281
+ async close (options?: AbortOptions): Promise<void> {
282
+ this.log.trace('closing gracefully')
283
+
284
+ this.status = 'closing'
285
+
286
+ await Promise.all([
287
+ this.closeRead(options),
288
+ this.closeWrite(options)
289
+ ])
290
+
291
+ // wait for read and write ends to close
292
+ await raceSignal(this.closed.promise, options?.signal)
293
+
294
+ this.status = 'closed'
295
+
296
+ this.log.trace('closed gracefully')
297
+ }
298
+
299
+ async closeRead (options: AbortOptions = {}): Promise<void> {
300
+ if (this.readStatus === 'closing' || this.readStatus === 'closed') {
301
+ return
302
+ }
303
+
304
+ this.log.trace('closing readable end of stream with starting read status "%s"', this.readStatus)
305
+
306
+ const readStatus = this.readStatus
307
+ this.readStatus = 'closing'
308
+
309
+ if (this.status !== 'reset' && this.status !== 'aborted' && this.timeline.closeRead == null) {
310
+ this.log.trace('send close read to remote')
311
+ await this.sendCloseRead(options)
312
+ }
313
+
314
+ if (readStatus === 'ready') {
315
+ this.log.trace('ending internal source queue with %d queued bytes', this.streamSource.readableLength)
316
+ this.streamSource.end()
317
+ }
318
+
319
+ this.log.trace('closed readable end of stream')
320
+ }
321
+
322
+ async closeWrite (options: AbortOptions = {}): Promise<void> {
323
+ if (this.writeStatus === 'closing' || this.writeStatus === 'closed') {
324
+ return
325
+ }
326
+
327
+ this.log.trace('closing writable end of stream with starting write status "%s"', this.writeStatus)
328
+
329
+ if (this.writeStatus === 'ready') {
330
+ this.log.trace('sink was never sunk, sink an empty array')
331
+
332
+ await raceSignal(this.sink([]), options.signal)
333
+ }
334
+
335
+ if (this.writeStatus === 'writing') {
336
+ // stop reading from the source passed to `.sink` in the microtask queue
337
+ // - this lets any data queued by the user in the current tick get read
338
+ // before we exit
339
+ await new Promise((resolve, reject) => {
340
+ queueMicrotask(() => {
341
+ this.log.trace('aborting source passed to .sink')
342
+ this.sinkController.abort()
343
+ raceSignal(this.sinkEnd.promise, options.signal)
344
+ .then(resolve, reject)
345
+ })
346
+ })
347
+ }
348
+
349
+ this.writeStatus = 'closed'
350
+
351
+ this.log.trace('closed writable end of stream')
352
+ }
353
+
354
+ /**
355
+ * Close immediately for reading and writing and send a reset message (local
356
+ * error)
357
+ */
358
+ abort (err: Error): void {
359
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
360
+ return
361
+ }
362
+
363
+ this.log('abort with error', err)
364
+
365
+ // try to send a reset message
366
+ this.log('try to send reset to remote')
367
+ const res = this.sendReset()
368
+
369
+ if (isPromise(res)) {
370
+ res.catch((err) => {
371
+ this.log.error('error sending reset message', err)
372
+ })
373
+ }
374
+
375
+ this.status = 'aborted'
376
+ this.timeline.abort = Date.now()
377
+ this._closeSinkAndSource(err)
378
+ this.onAbort?.(err)
379
+ }
380
+
381
+ /**
382
+ * Receive a reset message - close immediately for reading and writing (remote
383
+ * error)
384
+ */
385
+ reset (): void {
386
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
387
+ return
388
+ }
389
+
390
+ const err = new CodeError('stream reset', ERR_STREAM_RESET)
391
+
392
+ this.status = 'reset'
393
+ this.timeline.reset = Date.now()
394
+ this._closeSinkAndSource(err)
395
+ this.onReset?.()
396
+ }
397
+
398
+ _closeSinkAndSource (err?: Error): void {
399
+ this._closeSink(err)
400
+ this._closeSource(err)
401
+ }
402
+
403
+ _closeSink (err?: Error): void {
404
+ // if the sink function is running, cause it to end
405
+ if (this.writeStatus === 'writing') {
406
+ this.log.trace('end sink source')
407
+ this.sinkController.abort()
408
+ }
409
+
410
+ this.onSinkEnd(err)
411
+ }
412
+
413
+ _closeSource (err?: Error): void {
414
+ // if the source is not ending, end it
415
+ if (this.readStatus !== 'closing' && this.readStatus !== 'closed') {
416
+ this.log.trace('ending source with %d bytes to be read by consumer', this.streamSource.readableLength)
417
+ this.readStatus = 'closing'
418
+ this.streamSource.end(err)
419
+ }
420
+ }
421
+
422
+ /**
423
+ * The remote closed for writing so we should expect to receive no more
424
+ * messages
425
+ */
426
+ remoteCloseWrite (): void {
427
+ if (this.readStatus === 'closing' || this.readStatus === 'closed') {
428
+ this.log('received remote close write but local source is already closed')
429
+ return
430
+ }
431
+
432
+ this.log.trace('remote close write')
433
+ this._closeSource()
434
+ }
435
+
436
+ /**
437
+ * The remote closed for reading so we should not send any more
438
+ * messages
439
+ */
440
+ remoteCloseRead (): void {
441
+ if (this.writeStatus === 'closing' || this.writeStatus === 'closed') {
442
+ this.log('received remote close read but local sink is already closed')
443
+ return
444
+ }
445
+
446
+ this.log.trace('remote close read')
447
+ this._closeSink()
448
+ }
449
+
450
+ /**
451
+ * The underlying muxer has closed, no more messages can be sent or will
452
+ * be received, close immediately to free up resources
453
+ */
454
+ destroy (): void {
455
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
456
+ this.log('received destroy but we are already closed')
457
+ return
458
+ }
459
+
460
+ this.log.trace('stream destroyed')
461
+
462
+ this._closeSinkAndSource()
463
+ }
464
+
465
+ /**
466
+ * When an extending class reads data from it's implementation-specific source,
467
+ * call this method to allow the stream consumer to read the data.
468
+ */
469
+ sourcePush (data: Uint8ArrayList): void {
470
+ this.streamSource.push(data)
471
+ }
472
+
473
+ /**
474
+ * Returns the amount of unread data - can be used to prevent large amounts of
475
+ * data building up when the stream consumer is too slow.
476
+ */
477
+ sourceReadableLength (): number {
478
+ return this.streamSource.readableLength
479
+ }
480
+
481
+ /**
482
+ * Send a message to the remote muxer informing them a new stream is being
483
+ * opened
484
+ */
485
+ abstract sendNewStream (options?: AbortOptions): void | Promise<void>
486
+
487
+ /**
488
+ * Send a data message to the remote muxer
489
+ */
490
+ abstract sendData (buf: Uint8ArrayList, options?: AbortOptions): void | Promise<void>
491
+
492
+ /**
493
+ * Send a reset message to the remote muxer
494
+ */
495
+ abstract sendReset (options?: AbortOptions): void | Promise<void>
496
+
497
+ /**
498
+ * Send a message to the remote muxer, informing them no more data messages
499
+ * will be sent by this end of the stream
500
+ */
501
+ abstract sendCloseWrite (options?: AbortOptions): void | Promise<void>
502
+
503
+ /**
504
+ * Send a message to the remote muxer, informing them no more data messages
505
+ * will be read by this end of the stream
506
+ */
507
+ abstract sendCloseRead (options?: AbortOptions): void | Promise<void>
508
+ }
@@ -22,7 +22,7 @@
22
22
 
23
23
  import { Circuit } from '@multiformats/multiaddr-matcher'
24
24
  import { isPrivate } from './multiaddr/is-private.js'
25
- import type { Address } from '@libp2p/interface/peer-store'
25
+ import type { Address } from '@libp2p/interface'
26
26
 
27
27
  /**
28
28
  * Compare function for array.sort() that moves public addresses to the start
@@ -0,0 +1,14 @@
1
+ import { getIterator } from 'get-iterator'
2
+ import { isPromise } from './is-promise.js'
3
+ import type { Logger } from '@libp2p/logger'
4
+ import type { Source } from 'it-stream-types'
5
+
6
+ export function closeSource (source: Source<unknown>, log: Logger): void {
7
+ const res = getIterator(source).return?.()
8
+
9
+ if (isPromise(res)) {
10
+ res.catch(err => {
11
+ log.error('could not cause iterator to return', err)
12
+ })
13
+ }
14
+ }
@@ -1,10 +1,7 @@
1
1
  import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
2
- import { CodeError } from '@libp2p/interface/errors'
3
- import { logger } from '@libp2p/logger'
2
+ import { CodeError } from '@libp2p/interface'
4
3
  import { type Multiaddr, multiaddr } from '@multiformats/multiaddr'
5
4
 
6
- const log = logger('libp2p:ip-port-to-multiaddr')
7
-
8
5
  export const Errors = {
9
6
  ERR_INVALID_IP_PARAMETER: 'ERR_INVALID_IP_PARAMETER',
10
7
  ERR_INVALID_PORT_PARAMETER: 'ERR_INVALID_PORT_PARAMETER',
@@ -35,7 +32,5 @@ export function ipPortToMultiaddr (ip: string, port: number | string): Multiaddr
35
32
  return multiaddr(`/ip6/${ip}/tcp/${port}`)
36
33
  }
37
34
 
38
- const errMsg = `invalid ip:port for creating a multiaddr: ${ip}:${port}`
39
- log.error(errMsg)
40
- throw new CodeError(errMsg, Errors.ERR_INVALID_IP)
35
+ throw new CodeError(`invalid ip:port for creating a multiaddr: ${ip}:${port}`, Errors.ERR_INVALID_IP)
41
36
  }
@@ -0,0 +1,9 @@
1
+ export function isPromise <T = unknown> (thing: any): thing is Promise<T> {
2
+ if (thing == null) {
3
+ return false
4
+ }
5
+
6
+ return typeof thing.then === 'function' &&
7
+ typeof thing.catch === 'function' &&
8
+ typeof thing.finally === 'function'
9
+ }
@@ -0,0 +1,118 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+
3
+ import { CodeError, ERR_INVALID_PARAMETERS } from '@libp2p/interface'
4
+ import PQueue from 'p-queue'
5
+ import type { PeerId } from '@libp2p/interface'
6
+ import type { QueueAddOptions, Options, Queue } from 'p-queue'
7
+
8
+ // Port of lower_bound from https://en.cppreference.com/w/cpp/algorithm/lower_bound
9
+ // Used to compute insertion index to keep queue sorted after insertion
10
+ function lowerBound<T> (array: readonly T[], value: T, comparator: (a: T, b: T) => number): number {
11
+ let first = 0
12
+ let count = array.length
13
+
14
+ while (count > 0) {
15
+ const step = Math.trunc(count / 2)
16
+ let it = first + step
17
+
18
+ if (comparator(array[it]!, value) <= 0) {
19
+ first = ++it
20
+ count -= step + 1
21
+ } else {
22
+ count = step
23
+ }
24
+ }
25
+
26
+ return first
27
+ }
28
+
29
+ interface RunFunction { (): Promise<unknown> }
30
+
31
+ export interface PeerPriorityQueueOptions extends QueueAddOptions {
32
+ peerId: PeerId
33
+ }
34
+
35
+ interface PeerJob {
36
+ priority: number
37
+ peerId: PeerId
38
+ run: RunFunction
39
+ }
40
+
41
+ /**
42
+ * Port of https://github.com/sindresorhus/p-queue/blob/main/source/priority-queue.ts
43
+ * that adds support for filtering jobs by peer id
44
+ */
45
+ class PeerPriorityQueue implements Queue<RunFunction, PeerPriorityQueueOptions> {
46
+ readonly #queue: PeerJob[] = []
47
+
48
+ enqueue (run: RunFunction, options?: Partial<PeerPriorityQueueOptions>): void {
49
+ const peerId = options?.peerId
50
+ const priority = options?.priority ?? 0
51
+
52
+ if (peerId == null) {
53
+ throw new CodeError('missing peer id', ERR_INVALID_PARAMETERS)
54
+ }
55
+
56
+ const element: PeerJob = {
57
+ priority,
58
+ peerId,
59
+ run
60
+ }
61
+
62
+ if (this.size > 0 && this.#queue[this.size - 1]!.priority >= priority) {
63
+ this.#queue.push(element)
64
+ return
65
+ }
66
+
67
+ const index = lowerBound(
68
+ this.#queue, element,
69
+ (a: Readonly<PeerPriorityQueueOptions>, b: Readonly<PeerPriorityQueueOptions>) => b.priority! - a.priority!
70
+ )
71
+ this.#queue.splice(index, 0, element)
72
+ }
73
+
74
+ dequeue (): RunFunction | undefined {
75
+ const item = this.#queue.shift()
76
+ return item?.run
77
+ }
78
+
79
+ filter (options: Readonly<Partial<PeerPriorityQueueOptions>>): RunFunction[] {
80
+ if (options.peerId != null) {
81
+ const peerId = options.peerId
82
+
83
+ return this.#queue.filter(
84
+ (element: Readonly<PeerPriorityQueueOptions>) => peerId.equals(element.peerId)
85
+ ).map((element: Readonly<{ run: RunFunction }>) => element.run)
86
+ }
87
+
88
+ return this.#queue.filter(
89
+ (element: Readonly<PeerPriorityQueueOptions>) => element.priority === options.priority
90
+ ).map((element: Readonly<{ run: RunFunction }>) => element.run)
91
+ }
92
+
93
+ get size (): number {
94
+ return this.#queue.length
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Extends PQueue to add support for querying queued jobs by peer id
100
+ */
101
+ export class PeerJobQueue extends PQueue<PeerPriorityQueue, PeerPriorityQueueOptions> {
102
+ constructor (options: Options<PeerPriorityQueue, PeerPriorityQueueOptions> = {}) {
103
+ super({
104
+ ...options,
105
+ queueClass: PeerPriorityQueue
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Returns true if this queue has a job for the passed peer id that has not yet
111
+ * started to run
112
+ */
113
+ hasJob (peerId: PeerId): boolean {
114
+ return this.sizeBy({
115
+ peerId
116
+ }) > 0
117
+ }
118
+ }