@libp2p/mplex 8.0.4 → 9.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.
package/src/mplex.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { CodeError } from '@libp2p/interfaces/errors'
1
+ import { CodeError } from '@libp2p/interface/errors'
2
2
  import { logger } from '@libp2p/logger'
3
3
  import { abortableSource } from 'abortable-iterator'
4
- import { anySignal } from 'any-signal'
5
- import { pushableV } from 'it-pushable'
4
+ import { pipe } from 'it-pipe'
5
+ import { type PushableV, pushableV } from 'it-pushable'
6
6
  import { RateLimiterMemory } from 'rate-limiter-flexible'
7
7
  import { toString as uint8ArrayToString } from 'uint8arrays'
8
8
  import { Decoder } from './decode.js'
9
9
  import { encode } from './encode.js'
10
10
  import { MessageTypes, MessageTypeNames, type Message } from './message-types.js'
11
- import { createStream } from './stream.js'
11
+ import { createStream, type MplexStream } from './stream.js'
12
12
  import type { MplexInit } from './index.js'
13
- import type { Stream } from '@libp2p/interface-connection'
14
- import type { StreamMuxer, StreamMuxerInit } from '@libp2p/interface-stream-muxer'
13
+ import type { AbortOptions } from '@libp2p/interface'
14
+ import type { Stream } from '@libp2p/interface/connection'
15
+ import type { StreamMuxer, StreamMuxerInit } from '@libp2p/interface/stream-muxer'
15
16
  import type { Sink, Source } from 'it-stream-types'
16
17
  import type { Uint8ArrayList } from 'uint8arraylist'
17
18
 
@@ -21,6 +22,7 @@ const MAX_STREAMS_INBOUND_STREAMS_PER_CONNECTION = 1024
21
22
  const MAX_STREAMS_OUTBOUND_STREAMS_PER_CONNECTION = 1024
22
23
  const MAX_STREAM_BUFFER_SIZE = 1024 * 1024 * 4 // 4MB
23
24
  const DISCONNECT_THRESHOLD = 5
25
+ const CLOSE_TIMEOUT = 500
24
26
 
25
27
  function printMessage (msg: Message): any {
26
28
  const output: any = {
@@ -39,13 +41,13 @@ function printMessage (msg: Message): any {
39
41
  return output
40
42
  }
41
43
 
42
- export interface MplexStream extends Stream {
43
- sourceReadableLength: () => number
44
- sourcePush: (data: Uint8ArrayList) => void
44
+ interface MplexStreamMuxerInit extends MplexInit, StreamMuxerInit {
45
+ /**
46
+ * The default timeout to use in ms when shutting down the muxer.
47
+ */
48
+ closeTimeout?: number
45
49
  }
46
50
 
47
- interface MplexStreamMuxerInit extends MplexInit, StreamMuxerInit {}
48
-
49
51
  export class MplexStreamMuxer implements StreamMuxer {
50
52
  public protocol = '/mplex/6.7.0'
51
53
 
@@ -55,9 +57,10 @@ export class MplexStreamMuxer implements StreamMuxer {
55
57
  private _streamId: number
56
58
  private readonly _streams: { initiators: Map<number, MplexStream>, receivers: Map<number, MplexStream> }
57
59
  private readonly _init: MplexStreamMuxerInit
58
- private readonly _source: { push: (val: Message) => void, end: (err?: Error) => void }
60
+ private readonly _source: PushableV<Message>
59
61
  private readonly closeController: AbortController
60
62
  private readonly rateLimiter: RateLimiterMemory
63
+ private readonly closeTimeout: number
61
64
 
62
65
  constructor (init?: MplexStreamMuxerInit) {
63
66
  init = init ?? {}
@@ -74,6 +77,7 @@ export class MplexStreamMuxer implements StreamMuxer {
74
77
  receivers: new Map<number, MplexStream>()
75
78
  }
76
79
  this._init = init
80
+ this.closeTimeout = init.closeTimeout ?? CLOSE_TIMEOUT
77
81
 
78
82
  /**
79
83
  * An iterable sink
@@ -83,9 +87,24 @@ export class MplexStreamMuxer implements StreamMuxer {
83
87
  /**
84
88
  * An iterable source
85
89
  */
86
- const source = this._createSource()
87
- this._source = source
88
- this.source = source
90
+ this._source = pushableV<Message>({
91
+ objectMode: true,
92
+ onEnd: (): void => {
93
+ // the source has ended, we can't write any more messages to gracefully
94
+ // close streams so all we can do is destroy them
95
+ for (const stream of this._streams.initiators.values()) {
96
+ stream.destroy()
97
+ }
98
+
99
+ for (const stream of this._streams.receivers.values()) {
100
+ stream.destroy()
101
+ }
102
+ }
103
+ })
104
+ this.source = pipe(
105
+ this._source,
106
+ source => encode(source, this._init.minSendBytes)
107
+ )
89
108
 
90
109
  /**
91
110
  * Close controller
@@ -131,15 +150,41 @@ export class MplexStreamMuxer implements StreamMuxer {
131
150
  /**
132
151
  * Close or abort all tracked streams and stop the muxer
133
152
  */
134
- close (err?: Error | undefined): void {
135
- if (this.closeController.signal.aborted) return
153
+ async close (options?: AbortOptions): Promise<void> {
154
+ if (this.closeController.signal.aborted) {
155
+ return
156
+ }
157
+
158
+ const signal = options?.signal ?? AbortSignal.timeout(this.closeTimeout)
159
+
160
+ try {
161
+ // try to gracefully close all streams
162
+ await Promise.all(
163
+ this.streams.map(async s => s.close({
164
+ signal
165
+ }))
166
+ )
136
167
 
137
- if (err != null) {
138
- this.streams.forEach(s => { s.abort(err) })
139
- } else {
140
- this.streams.forEach(s => { s.close() })
168
+ this._source.end()
169
+
170
+ // try to gracefully close the muxer
171
+ await this._source.onEmpty({
172
+ signal
173
+ })
174
+
175
+ this.closeController.abort()
176
+ } catch (err: any) {
177
+ this.abort(err)
141
178
  }
142
- this.closeController.abort()
179
+ }
180
+
181
+ abort (err: Error): void {
182
+ if (this.closeController.signal.aborted) {
183
+ return
184
+ }
185
+
186
+ this.streams.forEach(s => { s.abort(err) })
187
+ this.closeController.abort(err)
143
188
  }
144
189
 
145
190
  /**
@@ -164,7 +209,7 @@ export class MplexStreamMuxer implements StreamMuxer {
164
209
  throw new Error(`${type} stream ${id} already exists!`)
165
210
  }
166
211
 
167
- const send = (msg: Message): void => {
212
+ const send = async (msg: Message): Promise<void> => {
168
213
  if (log.enabled) {
169
214
  log.trace('%s stream %s send', type, id, printMessage(msg))
170
215
  }
@@ -173,7 +218,7 @@ export class MplexStreamMuxer implements StreamMuxer {
173
218
  }
174
219
 
175
220
  const onEnd = (): void => {
176
- log('%s stream with id %s and protocol %s ended', type, id, stream.stat.protocol)
221
+ log('%s stream with id %s and protocol %s ended', type, id, stream.protocol)
177
222
  registry.delete(id)
178
223
 
179
224
  if (this._init.onStreamEnd != null) {
@@ -192,10 +237,10 @@ export class MplexStreamMuxer implements StreamMuxer {
192
237
  */
193
238
  _createSink (): Sink<Source<Uint8ArrayList | Uint8Array>, Promise<void>> {
194
239
  const sink: Sink<Source<Uint8ArrayList | Uint8Array>, Promise<void>> = async source => {
195
- const signal = anySignal([this.closeController.signal, this._init.signal])
196
-
197
240
  try {
198
- source = abortableSource(source, signal)
241
+ source = abortableSource(source, this.closeController.signal, {
242
+ returnOnAbort: true
243
+ })
199
244
 
200
245
  const decoder = new Decoder(this._init.maxMsgSize, this._init.maxUnprocessedMessageQueueSize)
201
246
 
@@ -209,34 +254,12 @@ export class MplexStreamMuxer implements StreamMuxer {
209
254
  } catch (err: any) {
210
255
  log('error in sink', err)
211
256
  this._source.end(err) // End the source with an error
212
- } finally {
213
- signal.clear()
214
257
  }
215
258
  }
216
259
 
217
260
  return sink
218
261
  }
219
262
 
220
- /**
221
- * Creates a source that restricts outgoing message sizes
222
- * and varint encodes them
223
- */
224
- _createSource (): any {
225
- const onEnd = (err?: Error): void => {
226
- this.close(err)
227
- }
228
- const source = pushableV<Message>({
229
- objectMode: true,
230
- onEnd
231
- })
232
-
233
- return Object.assign(encode(source, this._init.minSendBytes), {
234
- push: source.push,
235
- end: source.end,
236
- return: source.return
237
- })
238
- }
239
-
240
263
  async _handleIncoming (message: Message): Promise<void> {
241
264
  const { id, type } = message
242
265
 
@@ -264,7 +287,7 @@ export class MplexStreamMuxer implements StreamMuxer {
264
287
  } catch {
265
288
  log('rate limit hit when opening too many new streams over the inbound stream limit - closing remote connection')
266
289
  // since there's no backpressure in mplex, the only thing we can really do to protect ourselves is close the connection
267
- this._source.end(new Error('Too many open streams'))
290
+ this.abort(new Error('Too many open streams'))
268
291
  return
269
292
  }
270
293
 
@@ -286,43 +309,57 @@ export class MplexStreamMuxer implements StreamMuxer {
286
309
  if (stream == null) {
287
310
  log('missing stream %s for message type %s', id, MessageTypeNames[type])
288
311
 
312
+ // if the remote keeps sending us messages for streams that have been
313
+ // closed or were never opened they may be attacking us so if they do
314
+ // this very quickly all we can do is close the connection
315
+ try {
316
+ await this.rateLimiter.consume('missing-stream', 1)
317
+ } catch {
318
+ log('rate limit hit when receiving messages for streams that do not exist - closing remote connection')
319
+ // since there's no backpressure in mplex, the only thing we can really do to protect ourselves is close the connection
320
+ this.abort(new Error('Too many messages for missing streams'))
321
+ return
322
+ }
323
+
289
324
  return
290
325
  }
291
326
 
292
327
  const maxBufferSize = this._init.maxStreamBufferSize ?? MAX_STREAM_BUFFER_SIZE
293
328
 
294
- switch (type) {
295
- case MessageTypes.MESSAGE_INITIATOR:
296
- case MessageTypes.MESSAGE_RECEIVER:
297
- if (stream.sourceReadableLength() > maxBufferSize) {
298
- // Stream buffer has got too large, reset the stream
299
- this._source.push({
300
- id: message.id,
301
- type: type === MessageTypes.MESSAGE_INITIATOR ? MessageTypes.RESET_RECEIVER : MessageTypes.RESET_INITIATOR
302
- })
303
-
304
- // Inform the stream consumer they are not fast enough
305
- const error = new CodeError('Input buffer full - increase Mplex maxBufferSize to accommodate slow consumers', 'ERR_STREAM_INPUT_BUFFER_FULL')
306
- stream.abort(error)
307
-
308
- return
309
- }
329
+ try {
330
+ switch (type) {
331
+ case MessageTypes.MESSAGE_INITIATOR:
332
+ case MessageTypes.MESSAGE_RECEIVER:
333
+ if (stream.sourceReadableLength() > maxBufferSize) {
334
+ // Stream buffer has got too large, reset the stream
335
+ this._source.push({
336
+ id: message.id,
337
+ type: type === MessageTypes.MESSAGE_INITIATOR ? MessageTypes.RESET_RECEIVER : MessageTypes.RESET_INITIATOR
338
+ })
339
+
340
+ // Inform the stream consumer they are not fast enough
341
+ throw new CodeError('Input buffer full - increase Mplex maxBufferSize to accommodate slow consumers', 'ERR_STREAM_INPUT_BUFFER_FULL')
342
+ }
310
343
 
311
- // We got data from the remote, push it into our local stream
312
- stream.sourcePush(message.data)
313
- break
314
- case MessageTypes.CLOSE_INITIATOR:
315
- case MessageTypes.CLOSE_RECEIVER:
316
- // We should expect no more data from the remote, stop reading
317
- stream.closeRead()
318
- break
319
- case MessageTypes.RESET_INITIATOR:
320
- case MessageTypes.RESET_RECEIVER:
321
- // Stop reading and writing to the stream immediately
322
- stream.reset()
323
- break
324
- default:
325
- log('unknown message type %s', type)
344
+ // We got data from the remote, push it into our local stream
345
+ stream.sourcePush(message.data)
346
+ break
347
+ case MessageTypes.CLOSE_INITIATOR:
348
+ case MessageTypes.CLOSE_RECEIVER:
349
+ // The remote has stopped writing, so we can stop reading
350
+ stream.remoteCloseWrite()
351
+ break
352
+ case MessageTypes.RESET_INITIATOR:
353
+ case MessageTypes.RESET_RECEIVER:
354
+ // The remote has errored, stop reading and writing to the stream immediately
355
+ stream.reset()
356
+ break
357
+ default:
358
+ log('unknown message type %s', type)
359
+ }
360
+ } catch (err: any) {
361
+ log.error('error while processing message', err)
362
+ stream.abort(err)
326
363
  }
327
364
  }
328
365
  }
package/src/stream.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream'
1
+ import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/stream-muxer/stream'
2
+ import { logger } from '@libp2p/logger'
2
3
  import { Uint8ArrayList } from 'uint8arraylist'
3
4
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
4
5
  import { MAX_MSG_SIZE } from './decode.js'
@@ -7,7 +8,7 @@ import type { Message } from './message-types.js'
7
8
 
8
9
  export interface Options {
9
10
  id: number
10
- send: (msg: Message) => void
11
+ send: (msg: Message) => Promise<void>
11
12
  name?: string
12
13
  onEnd?: (err?: Error) => void
13
14
  type?: 'initiator' | 'receiver'
@@ -17,14 +18,21 @@ export interface Options {
17
18
  interface MplexStreamInit extends AbstractStreamInit {
18
19
  streamId: number
19
20
  name: string
20
- send: (msg: Message) => void
21
+ send: (msg: Message) => Promise<void>
22
+
23
+ /**
24
+ * The maximum allowable data size, any data larger than this will be
25
+ * chunked and sent in multiple data messages
26
+ */
27
+ maxDataSize: number
21
28
  }
22
29
 
23
- class MplexStream extends AbstractStream {
30
+ export class MplexStream extends AbstractStream {
24
31
  private readonly name: string
25
32
  private readonly streamId: number
26
- private readonly send: (msg: Message) => void
33
+ private readonly send: (msg: Message) => Promise<void>
27
34
  private readonly types: Record<string, number>
35
+ private readonly maxDataSize: number
28
36
 
29
37
  constructor (init: MplexStreamInit) {
30
38
  super(init)
@@ -33,25 +41,37 @@ class MplexStream extends AbstractStream {
33
41
  this.send = init.send
34
42
  this.name = init.name
35
43
  this.streamId = init.streamId
44
+ this.maxDataSize = init.maxDataSize
36
45
  }
37
46
 
38
- sendNewStream (): void {
39
- this.send({ id: this.streamId, type: InitiatorMessageTypes.NEW_STREAM, data: new Uint8ArrayList(uint8ArrayFromString(this.name)) })
47
+ async sendNewStream (): Promise<void> {
48
+ await this.send({ id: this.streamId, type: InitiatorMessageTypes.NEW_STREAM, data: new Uint8ArrayList(uint8ArrayFromString(this.name)) })
40
49
  }
41
50
 
42
- sendData (data: Uint8ArrayList): void {
43
- this.send({ id: this.streamId, type: this.types.MESSAGE, data })
51
+ async sendData (data: Uint8ArrayList): Promise<void> {
52
+ data = data.sublist()
53
+
54
+ while (data.byteLength > 0) {
55
+ const toSend = Math.min(data.byteLength, this.maxDataSize)
56
+ await this.send({
57
+ id: this.streamId,
58
+ type: this.types.MESSAGE,
59
+ data: data.sublist(0, toSend)
60
+ })
61
+
62
+ data.consume(toSend)
63
+ }
44
64
  }
45
65
 
46
- sendReset (): void {
47
- this.send({ id: this.streamId, type: this.types.RESET })
66
+ async sendReset (): Promise<void> {
67
+ await this.send({ id: this.streamId, type: this.types.RESET })
48
68
  }
49
69
 
50
- sendCloseWrite (): void {
51
- this.send({ id: this.streamId, type: this.types.CLOSE })
70
+ async sendCloseWrite (): Promise<void> {
71
+ await this.send({ id: this.streamId, type: this.types.CLOSE })
52
72
  }
53
73
 
54
- sendCloseRead (): void {
74
+ async sendCloseRead (): Promise<void> {
55
75
  // mplex does not support close read, only close write
56
76
  }
57
77
  }
@@ -66,6 +86,7 @@ export function createStream (options: Options): MplexStream {
66
86
  direction: type === 'initiator' ? 'outbound' : 'inbound',
67
87
  maxDataSize: maxMsgSize,
68
88
  onEnd,
69
- send
89
+ send,
90
+ log: logger(`libp2p:mplex:stream:${type}:${id}`)
70
91
  })
71
92
  }
@@ -1,4 +0,0 @@
1
- {
2
- "MplexInit": "https://libp2p.github.io/js-libp2p-mplex/interfaces/MplexInit.html",
3
- "mplex": "https://libp2p.github.io/js-libp2p-mplex/functions/mplex.html"
4
- }