@libp2p/mplex 1.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/encode.ts ADDED
@@ -0,0 +1,79 @@
1
+ import varint from 'varint'
2
+ import { Message, MessageTypes } from './message-types.js'
3
+ import type { Source } from 'it-stream-types'
4
+
5
+ const POOL_SIZE = 10 * 1024
6
+
7
+ function allocUnsafe (size: number) {
8
+ if (globalThis.Buffer != null) {
9
+ return Buffer.allocUnsafe(size)
10
+ }
11
+
12
+ return new Uint8Array(size)
13
+ }
14
+
15
+ class Encoder {
16
+ private _pool: Uint8Array
17
+ private _poolOffset: number
18
+
19
+ constructor () {
20
+ this._pool = allocUnsafe(POOL_SIZE)
21
+ this._poolOffset = 0
22
+ }
23
+
24
+ /**
25
+ * Encodes the given message and returns it and its header
26
+ */
27
+ write (msg: Message): Uint8Array[] {
28
+ const pool = this._pool
29
+ let offset = this._poolOffset
30
+
31
+ varint.encode(msg.id << 3 | msg.type, pool, offset)
32
+ offset += varint.encode.bytes
33
+
34
+ if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) {
35
+ varint.encode(msg.data.length, pool, offset)
36
+ } else {
37
+ varint.encode(0, pool, offset)
38
+ }
39
+
40
+ offset += varint.encode.bytes
41
+
42
+ const header = pool.slice(this._poolOffset, offset)
43
+
44
+ if (POOL_SIZE - offset < 100) {
45
+ this._pool = allocUnsafe(POOL_SIZE)
46
+ this._poolOffset = 0
47
+ } else {
48
+ this._poolOffset = offset
49
+ }
50
+
51
+ if ((msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) && msg.data != null) {
52
+ return [
53
+ header,
54
+ msg.data instanceof Uint8Array ? msg.data : msg.data.slice()
55
+ ]
56
+ }
57
+
58
+ return [
59
+ header
60
+ ]
61
+ }
62
+ }
63
+
64
+ const encoder = new Encoder()
65
+
66
+ /**
67
+ * Encode and yield one or more messages
68
+ */
69
+ export async function * encode (source: Source<Message | Message[]>) {
70
+ for await (const msg of source) {
71
+ if (Array.isArray(msg)) {
72
+ for (const m of msg) {
73
+ yield * encoder.write(m)
74
+ }
75
+ } else {
76
+ yield * encoder.write(msg)
77
+ }
78
+ }
79
+ }
package/src/index.ts ADDED
@@ -0,0 +1,281 @@
1
+ import { pipe } from 'it-pipe'
2
+ import { Pushable, pushableV } from 'it-pushable'
3
+ import { abortableSource } from 'abortable-iterator'
4
+ import { encode } from './encode.js'
5
+ import { decode } from './decode.js'
6
+ import { restrictSize } from './restrict-size.js'
7
+ import { MessageTypes, MessageTypeNames, Message } from './message-types.js'
8
+ import { createStream } from './stream.js'
9
+ import { toString as uint8ArrayToString } from 'uint8arrays'
10
+ import { trackedMap } from '@libp2p/tracked-map'
11
+ import { logger } from '@libp2p/logger'
12
+ import type { AbortOptions } from '@libp2p/interfaces'
13
+ import type { Sink } from 'it-stream-types'
14
+ import type { Muxer } from '@libp2p/interfaces/stream-muxer'
15
+ import type { Stream } from '@libp2p/interfaces/connection'
16
+ import type { ComponentMetricsTracker } from '@libp2p/interfaces/metrics'
17
+ import each from 'it-foreach'
18
+
19
+ const log = logger('libp2p:mplex')
20
+
21
+ function printMessage (msg: Message) {
22
+ const output: any = {
23
+ ...msg,
24
+ type: `${MessageTypeNames[msg.type]} (${msg.type})`
25
+ }
26
+
27
+ if (msg.type === MessageTypes.NEW_STREAM) {
28
+ output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.slice())
29
+ }
30
+
31
+ if (msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) {
32
+ output.data = uint8ArrayToString(msg.data instanceof Uint8Array ? msg.data : msg.data.slice(), 'base16')
33
+ }
34
+
35
+ return output
36
+ }
37
+
38
+ export interface MplexStream extends Stream {
39
+ source: Pushable<Uint8Array>
40
+ }
41
+
42
+ export interface MplexOptions extends AbortOptions {
43
+ onStream?: (...args: any[]) => void
44
+ onStreamEnd?: (...args: any[]) => void
45
+ maxMsgSize?: number
46
+ metrics?: ComponentMetricsTracker
47
+ }
48
+
49
+ export class Mplex implements Muxer {
50
+ static multicodec = '/mplex/6.7.0'
51
+
52
+ public sink: Sink<Uint8Array>
53
+ public source: AsyncIterable<Uint8Array>
54
+
55
+ private _streamId: number
56
+ private readonly _streams: { initiators: Map<number, MplexStream>, receivers: Map<number, MplexStream> }
57
+ private readonly _options: MplexOptions
58
+ private readonly _source: { push: (val: Message) => void, end: (err?: Error) => void }
59
+
60
+ constructor (options?: MplexOptions) {
61
+ options = options ?? {}
62
+
63
+ this._streamId = 0
64
+ this._streams = {
65
+ /**
66
+ * Stream to ids map
67
+ */
68
+ initiators: trackedMap<number, MplexStream>({ metrics: options.metrics, component: 'mplex', metric: 'initiatorStreams' }),
69
+ /**
70
+ * Stream to ids map
71
+ */
72
+ receivers: trackedMap<number, MplexStream>({ metrics: options.metrics, component: 'mplex', metric: 'receiverStreams' })
73
+ }
74
+ this._options = options
75
+
76
+ /**
77
+ * An iterable sink
78
+ */
79
+ this.sink = this._createSink()
80
+
81
+ /**
82
+ * An iterable source
83
+ */
84
+ const source = this._createSource()
85
+ this._source = source
86
+ this.source = source
87
+ }
88
+
89
+ /**
90
+ * Returns a Map of streams and their ids
91
+ */
92
+ get streams () {
93
+ // Inbound and Outbound streams may have the same ids, so we need to make those unique
94
+ const streams: Stream[] = []
95
+ this._streams.initiators.forEach(stream => {
96
+ streams.push(stream)
97
+ })
98
+ this._streams.receivers.forEach(stream => {
99
+ streams.push(stream)
100
+ })
101
+ return streams
102
+ }
103
+
104
+ /**
105
+ * Initiate a new stream with the given name. If no name is
106
+ * provided, the id of the stream will be used.
107
+ */
108
+ newStream (name?: string): Stream {
109
+ const id = this._streamId++
110
+ name = name == null ? id.toString() : name.toString()
111
+ const registry = this._streams.initiators
112
+ return this._newStream({ id, name, type: 'initiator', registry })
113
+ }
114
+
115
+ /**
116
+ * Called whenever an inbound stream is created
117
+ */
118
+ _newReceiverStream (options: { id: number, name: string }) {
119
+ const { id, name } = options
120
+ const registry = this._streams.receivers
121
+ return this._newStream({ id, name, type: 'receiver', registry })
122
+ }
123
+
124
+ _newStream (options: { id: number, name: string, type: 'initiator' | 'receiver', registry: Map<number, MplexStream> }) {
125
+ const { id, name, type, registry } = options
126
+
127
+ log('new %s stream %s %s', type, id, name)
128
+
129
+ if (registry.has(id)) {
130
+ throw new Error(`${type} stream ${id} already exists!`)
131
+ }
132
+
133
+ const send = (msg: Message) => {
134
+ if (log.enabled) {
135
+ log('%s stream %s send', type, id, printMessage(msg))
136
+ }
137
+
138
+ if (msg.type === MessageTypes.NEW_STREAM || msg.type === MessageTypes.MESSAGE_INITIATOR || msg.type === MessageTypes.MESSAGE_RECEIVER) {
139
+ msg.data = msg.data instanceof Uint8Array ? msg.data : msg.data.slice()
140
+ }
141
+
142
+ this._source.push(msg)
143
+ }
144
+
145
+ const onEnd = () => {
146
+ log('%s stream %s %s ended', type, id, name)
147
+ registry.delete(id)
148
+
149
+ if (this._options.onStreamEnd != null) {
150
+ this._options.onStreamEnd(stream)
151
+ }
152
+ }
153
+
154
+ const stream = createStream({ id, name, send, type, onEnd, maxMsgSize: this._options.maxMsgSize })
155
+ registry.set(id, stream)
156
+ return stream
157
+ }
158
+
159
+ /**
160
+ * Creates a sink with an abortable source. Incoming messages will
161
+ * also have their size restricted. All messages will be varint decoded.
162
+ */
163
+ _createSink () {
164
+ const sink: Sink<Uint8Array> = async source => {
165
+ if (this._options.signal != null) {
166
+ source = abortableSource(source, this._options.signal)
167
+ }
168
+
169
+ try {
170
+ await pipe(
171
+ source,
172
+ source => each(source, (buf) => {
173
+ // console.info('incoming', uint8ArrayToString(buf, 'base64'))
174
+ }),
175
+ decode,
176
+ restrictSize(this._options.maxMsgSize),
177
+ async source => {
178
+ for await (const msg of source) {
179
+ this._handleIncoming(msg)
180
+ }
181
+ }
182
+ )
183
+
184
+ this._source.end()
185
+ } catch (err: any) {
186
+ log('error in sink', err)
187
+ this._source.end(err) // End the source with an error
188
+ }
189
+ }
190
+
191
+ return sink
192
+ }
193
+
194
+ /**
195
+ * Creates a source that restricts outgoing message sizes
196
+ * and varint encodes them
197
+ */
198
+ _createSource () {
199
+ const onEnd = (err?: Error) => {
200
+ const { initiators, receivers } = this._streams
201
+ // Abort all the things!
202
+ for (const s of initiators.values()) {
203
+ s.abort(err)
204
+ }
205
+ for (const s of receivers.values()) {
206
+ s.abort(err)
207
+ }
208
+ }
209
+ const source = pushableV<Message>({ onEnd })
210
+ /*
211
+ const p = pipe(
212
+ source,
213
+ source => each(source, (msgs) => {
214
+ if (log.enabled) {
215
+ msgs.forEach(msg => {
216
+ log('outgoing message', printMessage(msg))
217
+ })
218
+ }
219
+ }),
220
+ source => encode(source),
221
+ source => each(source, (buf) => {
222
+ console.info('outgoing', uint8ArrayToString(buf, 'base64'))
223
+ })
224
+ )
225
+
226
+ return Object.assign(p, {
227
+ push: source.push,
228
+ end: source.end,
229
+ return: source.return
230
+ })
231
+ */
232
+ return Object.assign(encode(source), {
233
+ push: source.push,
234
+ end: source.end,
235
+ return: source.return
236
+ })
237
+ }
238
+
239
+ _handleIncoming (message: Message) {
240
+ const { id, type } = message
241
+
242
+ if (log.enabled) {
243
+ log('incoming message', printMessage(message))
244
+ }
245
+
246
+ // Create a new stream?
247
+ if (message.type === MessageTypes.NEW_STREAM) {
248
+ const stream = this._newReceiverStream({ id, name: uint8ArrayToString(message.data instanceof Uint8Array ? message.data : message.data.slice()) })
249
+
250
+ if (this._options.onStream != null) {
251
+ this._options.onStream(stream)
252
+ }
253
+
254
+ return
255
+ }
256
+
257
+ const list = (type & 1) === 1 ? this._streams.initiators : this._streams.receivers
258
+ const stream = list.get(id)
259
+
260
+ if (stream == null) {
261
+ return log('missing stream %s', id)
262
+ }
263
+
264
+ switch (type) {
265
+ case MessageTypes.MESSAGE_INITIATOR:
266
+ case MessageTypes.MESSAGE_RECEIVER:
267
+ stream.source.push(message.data.slice())
268
+ break
269
+ case MessageTypes.CLOSE_INITIATOR:
270
+ case MessageTypes.CLOSE_RECEIVER:
271
+ stream.close()
272
+ break
273
+ case MessageTypes.RESET_INITIATOR:
274
+ case MessageTypes.RESET_RECEIVER:
275
+ stream.reset()
276
+ break
277
+ default:
278
+ log('unknown message type %s', type)
279
+ }
280
+ }
281
+ }
@@ -0,0 +1,79 @@
1
+ import type { Uint8ArrayList } from 'uint8arraylist'
2
+
3
+ type INITIATOR_NAME = 'NEW_STREAM' | 'MESSAGE' | 'CLOSE' | 'RESET'
4
+ type RECEIVER_NAME = 'MESSAGE' | 'CLOSE' | 'RESET'
5
+ type NAME = 'NEW_STREAM' | 'MESSAGE_INITIATOR' | 'CLOSE_INITIATOR' | 'RESET_INITIATOR' | 'MESSAGE_RECEIVER' | 'CLOSE_RECEIVER' | 'RESET_RECEIVER'
6
+ type CODE = 0 | 1 | 2 | 3 | 4 | 5 | 6
7
+
8
+ export enum MessageTypes {
9
+ NEW_STREAM = 0,
10
+ MESSAGE_RECEIVER = 1,
11
+ MESSAGE_INITIATOR = 2,
12
+ CLOSE_RECEIVER = 3,
13
+ CLOSE_INITIATOR = 4,
14
+ RESET_RECEIVER = 5,
15
+ RESET_INITIATOR = 6
16
+ }
17
+
18
+ export const MessageTypeNames: Record<CODE, NAME> = Object.freeze({
19
+ 0: 'NEW_STREAM',
20
+ 1: 'MESSAGE_RECEIVER',
21
+ 2: 'MESSAGE_INITIATOR',
22
+ 3: 'CLOSE_RECEIVER',
23
+ 4: 'CLOSE_INITIATOR',
24
+ 5: 'RESET_RECEIVER',
25
+ 6: 'RESET_INITIATOR'
26
+ })
27
+
28
+ export const InitiatorMessageTypes: Record<INITIATOR_NAME, CODE> = Object.freeze({
29
+ NEW_STREAM: MessageTypes.NEW_STREAM,
30
+ MESSAGE: MessageTypes.MESSAGE_INITIATOR,
31
+ CLOSE: MessageTypes.CLOSE_INITIATOR,
32
+ RESET: MessageTypes.RESET_INITIATOR
33
+ })
34
+
35
+ export const ReceiverMessageTypes: Record<RECEIVER_NAME, CODE> = Object.freeze({
36
+ MESSAGE: MessageTypes.MESSAGE_RECEIVER,
37
+ CLOSE: MessageTypes.CLOSE_RECEIVER,
38
+ RESET: MessageTypes.RESET_RECEIVER
39
+ })
40
+
41
+ export interface NewStreamMessage {
42
+ id: number
43
+ type: MessageTypes.NEW_STREAM
44
+ data: Uint8Array | Uint8ArrayList
45
+ }
46
+
47
+ export interface MessageReceiverMessage {
48
+ id: number
49
+ type: MessageTypes.MESSAGE_RECEIVER
50
+ data: Uint8Array | Uint8ArrayList
51
+ }
52
+
53
+ export interface MessageInitiatorMessage {
54
+ id: number
55
+ type: MessageTypes.MESSAGE_INITIATOR
56
+ data: Uint8Array | Uint8ArrayList
57
+ }
58
+
59
+ export interface CloseReceiverMessage {
60
+ id: number
61
+ type: MessageTypes.CLOSE_RECEIVER
62
+ }
63
+
64
+ export interface CloseInitiatorMessage {
65
+ id: number
66
+ type: MessageTypes.CLOSE_INITIATOR
67
+ }
68
+
69
+ export interface ResetReceiverMessage {
70
+ id: number
71
+ type: MessageTypes.RESET_RECEIVER
72
+ }
73
+
74
+ export interface ResetInitiatorMessage {
75
+ id: number
76
+ type: MessageTypes.RESET_INITIATOR
77
+ }
78
+
79
+ export type Message = NewStreamMessage | MessageReceiverMessage | MessageInitiatorMessage | CloseReceiverMessage | CloseInitiatorMessage | ResetReceiverMessage | ResetInitiatorMessage
@@ -0,0 +1,36 @@
1
+ import { Message, MessageTypes } from './message-types.js'
2
+ import type { Source, Transform } from 'it-stream-types'
3
+
4
+ export const MAX_MSG_SIZE = 1 << 20 // 1MB
5
+
6
+ /**
7
+ * Creates an iterable transform that restricts message sizes to
8
+ * the given maximum size.
9
+ */
10
+ export function restrictSize (max?: number): Transform<Message | Message[], Message> {
11
+ const maxSize = max ?? MAX_MSG_SIZE
12
+
13
+ const checkSize = (msg: Message) => {
14
+ if (msg.type !== MessageTypes.NEW_STREAM && msg.type !== MessageTypes.MESSAGE_INITIATOR && msg.type !== MessageTypes.MESSAGE_RECEIVER) {
15
+ return
16
+ }
17
+
18
+ if (msg.data.byteLength > maxSize) {
19
+ throw Object.assign(new Error('message size too large!'), { code: 'ERR_MSG_TOO_BIG' })
20
+ }
21
+ }
22
+
23
+ return (source: Source<Message | Message[]>) => {
24
+ return (async function * restrictSize () {
25
+ for await (const msg of source) {
26
+ if (Array.isArray(msg)) {
27
+ msg.forEach(checkSize)
28
+ yield * msg
29
+ } else {
30
+ checkSize(msg)
31
+ yield msg
32
+ }
33
+ }
34
+ })()
35
+ }
36
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,182 @@
1
+ import { abortableSource } from 'abortable-iterator'
2
+ import { pushable } from 'it-pushable'
3
+ import errCode from 'err-code'
4
+ import { MAX_MSG_SIZE } from './restrict-size.js'
5
+ import { anySignal } from 'any-signal'
6
+ import { InitiatorMessageTypes, ReceiverMessageTypes } from './message-types.js'
7
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
8
+ import { Uint8ArrayList } from 'uint8arraylist'
9
+ import { logger } from '@libp2p/logger'
10
+ import type { Message } from './message-types.js'
11
+ import type { Timeline } from '@libp2p/interfaces/connection'
12
+ import type { Source } from 'it-stream-types'
13
+ import type { MplexStream } from './index.js'
14
+
15
+ const log = logger('libp2p:mplex:stream')
16
+
17
+ const ERR_MPLEX_STREAM_RESET = 'ERR_MPLEX_STREAM_RESET'
18
+ const ERR_MPLEX_STREAM_ABORT = 'ERR_MPLEX_STREAM_ABORT'
19
+
20
+ export interface Options {
21
+ id: number
22
+ send: (msg: Message) => void
23
+ name?: string
24
+ onEnd?: (err?: Error) => void
25
+ type?: 'initiator' | 'receiver'
26
+ maxMsgSize?: number
27
+ }
28
+
29
+ export function createStream (options: Options): MplexStream {
30
+ const { id, name, send, onEnd, type = 'initiator', maxMsgSize = MAX_MSG_SIZE } = options
31
+
32
+ const abortController = new AbortController()
33
+ const resetController = new AbortController()
34
+ const Types = type === 'initiator' ? InitiatorMessageTypes : ReceiverMessageTypes
35
+ const externalId = type === 'initiator' ? (`i${id}`) : `r${id}`
36
+ const streamName = `${name == null ? id : name}`
37
+
38
+ let sourceEnded = false
39
+ let sinkEnded = false
40
+ let endErr: Error | undefined
41
+
42
+ const timeline: Timeline = {
43
+ open: Date.now()
44
+ }
45
+
46
+ const onSourceEnd = (err?: Error) => {
47
+ if (sourceEnded) {
48
+ return
49
+ }
50
+
51
+ sourceEnded = true
52
+ log('%s stream %s source end', type, streamName, err)
53
+
54
+ if (err != null && endErr == null) {
55
+ endErr = err
56
+ }
57
+
58
+ if (sinkEnded) {
59
+ stream.timeline.close = Date.now()
60
+
61
+ if (onEnd != null) {
62
+ onEnd(endErr)
63
+ }
64
+ }
65
+ }
66
+
67
+ const onSinkEnd = (err?: Error) => {
68
+ if (sinkEnded) {
69
+ return
70
+ }
71
+
72
+ sinkEnded = true
73
+ log('%s stream %s sink end - err: %o', type, streamName, err)
74
+
75
+ if (err != null && endErr == null) {
76
+ endErr = err
77
+ }
78
+
79
+ if (sourceEnded) {
80
+ timeline.close = Date.now()
81
+
82
+ if (onEnd != null) {
83
+ onEnd(endErr)
84
+ }
85
+ }
86
+ }
87
+
88
+ const stream = {
89
+ // Close for reading
90
+ close: () => {
91
+ stream.source.end()
92
+ },
93
+ // Close for reading and writing (local error)
94
+ abort: (err?: Error) => {
95
+ log('%s stream %s abort', type, streamName, err)
96
+ // End the source with the passed error
97
+ stream.source.end(err)
98
+ abortController.abort()
99
+ onSinkEnd(err)
100
+ },
101
+ // Close immediately for reading and writing (remote error)
102
+ reset: () => {
103
+ const err = errCode(new Error('stream reset'), ERR_MPLEX_STREAM_RESET)
104
+ resetController.abort()
105
+ stream.source.end(err)
106
+ onSinkEnd(err)
107
+ },
108
+ sink: async (source: Source<Uint8Array>) => {
109
+ source = abortableSource(source, anySignal([
110
+ abortController.signal,
111
+ resetController.signal
112
+ ]))
113
+
114
+ try {
115
+ if (type === 'initiator') { // If initiator, open a new stream
116
+ send({ id, type: InitiatorMessageTypes.NEW_STREAM, data: uint8ArrayFromString(streamName) })
117
+ }
118
+
119
+ const uint8ArrayList = new Uint8ArrayList()
120
+
121
+ for await (const data of source) {
122
+ uint8ArrayList.append(data)
123
+
124
+ while (uint8ArrayList.length !== 0) {
125
+ if (uint8ArrayList.length <= maxMsgSize) {
126
+ send({ id, type: Types.MESSAGE, data: uint8ArrayList.subarray() })
127
+ uint8ArrayList.consume(uint8ArrayList.length)
128
+ break
129
+ }
130
+
131
+ const toSend = uint8ArrayList.length - maxMsgSize
132
+ send({ id, type: Types.MESSAGE, data: uint8ArrayList.subarray(0, toSend) })
133
+ uint8ArrayList.consume(toSend)
134
+ }
135
+ }
136
+ } catch (err: any) {
137
+ if (err.type === 'aborted' && err.message === 'The operation was aborted') {
138
+ if (resetController.signal.aborted) {
139
+ err.message = 'stream reset'
140
+ err.code = ERR_MPLEX_STREAM_RESET
141
+ }
142
+
143
+ if (abortController.signal.aborted) {
144
+ err.message = 'stream aborted'
145
+ err.code = ERR_MPLEX_STREAM_ABORT
146
+ }
147
+ }
148
+
149
+ // Send no more data if this stream was remotely reset
150
+ if (err.code === ERR_MPLEX_STREAM_RESET) {
151
+ log('%s stream %s reset', type, name)
152
+ } else {
153
+ log('%s stream %s error', type, name, err)
154
+ try {
155
+ send({ id, type: Types.RESET })
156
+ } catch (err) {
157
+ log('%s stream %s error sending reset', type, name, err)
158
+ }
159
+ }
160
+
161
+ stream.source.end(err)
162
+ onSinkEnd(err)
163
+ return
164
+ }
165
+
166
+ try {
167
+ send({ id, type: Types.CLOSE })
168
+ } catch (err) {
169
+ log('%s stream %s error sending close', type, name, err)
170
+ }
171
+
172
+ onSinkEnd()
173
+ },
174
+ source: pushable<Uint8Array>({
175
+ onEnd: onSourceEnd
176
+ }),
177
+ timeline,
178
+ id: externalId
179
+ }
180
+
181
+ return stream
182
+ }