@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/LICENSE +4 -0
- package/README.md +186 -0
- package/dist/src/decode.d.ts +7 -0
- package/dist/src/decode.d.ts.map +1 -0
- package/dist/src/decode.js +93 -0
- package/dist/src/decode.js.map +1 -0
- package/dist/src/encode.d.ts +7 -0
- package/dist/src/encode.d.ts.map +1 -0
- package/dist/src/encode.js +65 -0
- package/dist/src/encode.js.map +1 -0
- package/dist/src/index.d.ts +66 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +218 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/message-types.d.ts +51 -0
- package/dist/src/message-types.d.ts.map +1 -0
- package/dist/src/message-types.js +31 -0
- package/dist/src/message-types.js.map +1 -0
- package/dist/src/restrict-size.d.ts +9 -0
- package/dist/src/restrict-size.d.ts.map +1 -0
- package/dist/src/restrict-size.js +32 -0
- package/dist/src/restrict-size.js.map +1 -0
- package/dist/src/stream.d.ts +12 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +146 -0
- package/dist/src/stream.js.map +1 -0
- package/package.json +169 -0
- package/src/decode.ts +132 -0
- package/src/encode.ts +79 -0
- package/src/index.ts +281 -0
- package/src/message-types.ts +79 -0
- package/src/restrict-size.ts +36 -0
- package/src/stream.ts +182 -0
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
|
+
}
|