@libp2p/utils 4.0.6 → 4.0.7-0b4a2ee79

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