@libp2p/mplex 8.0.4 → 9.0.0-fdd80820
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/README.md +3 -3
- package/dist/index.min.js +5 -5
- package/dist/src/index.d.ts +1 -1
- package/dist/src/mplex.d.ts +11 -12
- package/dist/src/mplex.d.ts.map +1 -1
- package/dist/src/mplex.js +94 -67
- package/dist/src/mplex.js.map +1 -1
- package/dist/src/stream.d.ts +15 -9
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +25 -12
- package/dist/src/stream.js.map +1 -1
- package/package.json +16 -110
- package/src/index.ts +1 -1
- package/src/mplex.ts +118 -81
- package/src/stream.ts +36 -15
- package/dist/typedoc-urls.json +0 -4
package/src/mplex.ts
CHANGED
@@ -1,17 +1,18 @@
|
|
1
|
-
import { CodeError } from '@libp2p/
|
1
|
+
import { CodeError } from '@libp2p/interface/errors'
|
2
2
|
import { logger } from '@libp2p/logger'
|
3
3
|
import { abortableSource } from 'abortable-iterator'
|
4
|
-
import {
|
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 {
|
14
|
-
import type {
|
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
|
-
|
43
|
-
|
44
|
-
|
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:
|
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
|
-
|
87
|
-
|
88
|
-
|
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 (
|
135
|
-
if (this.closeController.signal.aborted)
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
this.
|
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
|
-
|
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.
|
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.
|
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
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
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
|
-
|
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
|
}
|
package/dist/typedoc-urls.json
DELETED