@libp2p/multistream-select 6.0.28 → 6.0.29-a02cb0461

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/handle.ts CHANGED
@@ -1,11 +1,11 @@
1
+ import { lpStream } from '@libp2p/utils'
1
2
  import { encode } from 'it-length-prefixed'
2
- import { lpStream } from 'it-length-prefixed-stream'
3
3
  import { Uint8ArrayList } from 'uint8arraylist'
4
4
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
5
5
  import { MAX_PROTOCOL_LENGTH, PROTOCOL_ID } from './constants.js'
6
- import * as multistream from './multistream.js'
7
- import type { MultistreamSelectInit, ProtocolStream } from './index.js'
8
- import type { Duplex } from 'it-stream-types'
6
+ import { readString } from './multistream.js'
7
+ import type { MultistreamSelectInit } from './index.js'
8
+ import type { MultiaddrConnection, MessageStream } from '@libp2p/interface'
9
9
 
10
10
  /**
11
11
  * Handle multistream protocol selections for the given list of protocols.
@@ -53,34 +53,38 @@ import type { Duplex } from 'it-stream-types'
53
53
  * })
54
54
  * ```
55
55
  */
56
- export async function handle <Stream extends Duplex<any, any, any>> (stream: Stream, protocols: string | string[], options: MultistreamSelectInit): Promise<ProtocolStream<Stream>> {
56
+ export async function handle <Stream extends MessageStream = MultiaddrConnection> (stream: Stream, protocols: string | string[], options: MultistreamSelectInit = {}): Promise<string> {
57
57
  protocols = Array.isArray(protocols) ? protocols : [protocols]
58
- options.log.trace('handle: available protocols %s', protocols)
58
+
59
+ const log = stream.log.newScope('mss:handle')
59
60
 
60
61
  const lp = lpStream(stream, {
61
62
  ...options,
62
63
  maxDataLength: MAX_PROTOCOL_LENGTH,
63
- maxLengthLength: 2 // 2 bytes is enough to length-prefix MAX_PROTOCOL_LENGTH
64
+ maxLengthLength: 2, // 2 bytes is enough to length-prefix MAX_PROTOCOL_LENGTH
65
+ stopPropagation: true
64
66
  })
65
67
 
66
68
  while (true) {
67
- options.log.trace('handle: reading incoming string')
68
- const protocol = await multistream.readString(lp, options)
69
- options.log.trace('handle: read "%s"', protocol)
69
+ log.trace('reading incoming string')
70
+ const protocol = await readString(lp, options)
71
+ log.trace('read "%s"', protocol)
70
72
 
71
73
  if (protocol === PROTOCOL_ID) {
72
- options.log.trace('handle: respond with "%s" for "%s"', PROTOCOL_ID, protocol)
73
- await multistream.write(lp, uint8ArrayFromString(`${PROTOCOL_ID}\n`), options)
74
- options.log.trace('handle: responded with "%s" for "%s"', PROTOCOL_ID, protocol)
74
+ log.trace('respond with "%s" for "%s"', PROTOCOL_ID, protocol)
75
+ await lp.write(uint8ArrayFromString(`${PROTOCOL_ID}\n`), options)
76
+ log.trace('responded with "%s" for "%s"', PROTOCOL_ID, protocol)
75
77
  continue
76
78
  }
77
79
 
78
80
  if (protocols.includes(protocol)) {
79
- options.log.trace('handle: respond with "%s" for "%s"', protocol, protocol)
80
- await multistream.write(lp, uint8ArrayFromString(`${protocol}\n`), options)
81
- options.log.trace('handle: responded with "%s" for "%s"', protocol, protocol)
81
+ log.trace('respond with "%s" for "%s"', protocol, protocol)
82
+ await lp.write(uint8ArrayFromString(`${protocol}\n`), options)
83
+ log.trace('responded with "%s" for "%s"', protocol, protocol)
84
+
85
+ lp.unwrap()
82
86
 
83
- return { stream: lp.unwrap(), protocol }
87
+ return protocol
84
88
  }
85
89
 
86
90
  if (protocol === 'ls') {
@@ -90,14 +94,14 @@ export async function handle <Stream extends Duplex<any, any, any>> (stream: Str
90
94
  uint8ArrayFromString('\n')
91
95
  )
92
96
 
93
- options.log.trace('handle: respond with "%s" for %s', protocols, protocol)
94
- await multistream.write(lp, protos, options)
95
- options.log.trace('handle: responded with "%s" for %s', protocols, protocol)
97
+ log.trace('respond with "%s" for %s', protocols, protocol)
98
+ await lp.write(protos, options)
99
+ log.trace('responded with "%s" for %s', protocols, protocol)
96
100
  continue
97
101
  }
98
102
 
99
- options.log.trace('handle: respond with "na" for "%s"', protocol)
100
- await multistream.write(lp, uint8ArrayFromString('na\n'), options)
101
- options.log('handle: responded with "na" for "%s"', protocol)
103
+ log.trace('respond with "na" for "%s"', protocol)
104
+ await lp.write(uint8ArrayFromString('na\n'), options)
105
+ log('responded with "na" for "%s"', protocol)
102
106
  }
103
107
  }
package/src/index.ts CHANGED
@@ -21,17 +21,12 @@
21
21
  */
22
22
 
23
23
  import { PROTOCOL_ID } from './constants.js'
24
- import type { AbortOptions, LoggerOptions } from '@libp2p/interface'
25
- import type { LengthPrefixedStreamOpts } from 'it-length-prefixed-stream'
24
+ import type { AbortOptions } from '@libp2p/interface'
25
+ import type { LengthPrefixedStreamOpts } from '@libp2p/utils'
26
26
 
27
27
  export { PROTOCOL_ID }
28
28
 
29
- export interface ProtocolStream<Stream> {
30
- stream: Stream
31
- protocol: string
32
- }
33
-
34
- export interface MultistreamSelectInit extends AbortOptions, LoggerOptions, Partial<LengthPrefixedStreamOpts> {
29
+ export interface MultistreamSelectInit extends AbortOptions, Partial<LengthPrefixedStreamOpts> {
35
30
  /**
36
31
  * When false, and only a single protocol is being negotiated, use optimistic
37
32
  * select to send both the protocol name and the first data buffer in the
@@ -43,5 +38,4 @@ export interface MultistreamSelectInit extends AbortOptions, LoggerOptions, Part
43
38
  }
44
39
 
45
40
  export { select } from './select.js'
46
- export type { SelectStream } from './select.js'
47
41
  export { handle } from './handle.js'
@@ -1,46 +1,21 @@
1
1
  import { InvalidMessageError } from '@libp2p/interface'
2
2
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
3
3
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
4
- import type { AbortOptions, LoggerOptions } from '@libp2p/interface'
5
- import type { LengthPrefixedStream } from 'it-length-prefixed-stream'
6
- import type { Duplex, Source } from 'it-stream-types'
7
- import type { Uint8ArrayList } from 'uint8arraylist'
4
+ import type { AbortOptions } from '@libp2p/interface'
5
+ import type { LengthPrefixedStream } from '@libp2p/utils'
8
6
 
9
7
  const NewLine = uint8ArrayFromString('\n')
10
8
 
11
9
  /**
12
- * `write` encodes and writes a single buffer
13
- */
14
- export async function write (writer: LengthPrefixedStream<Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>, Source<Uint8Array>>>, buffer: Uint8Array | Uint8ArrayList, options?: AbortOptions): Promise<void> {
15
- await writer.write(buffer, options)
16
- }
17
-
18
- /**
19
- * `writeAll` behaves like `write`, except it encodes an array of items as a single write
20
- */
21
- export async function writeAll (writer: LengthPrefixedStream<Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>, Source<Uint8Array>>>, buffers: Uint8Array[], options?: AbortOptions): Promise<void> {
22
- await writer.writeV(buffers, options)
23
- }
24
-
25
- /**
26
- * Read a length-prefixed buffer from the passed stream, stripping the final newline character
10
+ * Read a length-prefixed string from the passed stream, stripping the final newline character
27
11
  */
28
- export async function read (reader: LengthPrefixedStream<Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>, Source<Uint8Array>>>, options: AbortOptions & LoggerOptions): Promise<Uint8ArrayList> {
12
+ export async function readString (reader: LengthPrefixedStream<any>, options?: AbortOptions): Promise<string> {
29
13
  const buf = await reader.read(options)
14
+ const arr = buf.subarray()
30
15
 
31
- if (buf.byteLength === 0 || buf.get(buf.byteLength - 1) !== NewLine[0]) {
32
- options.log.error('Invalid mss message - missing newline', buf)
16
+ if (arr.byteLength === 0 || arr[arr.length - 1] !== NewLine[0]) {
33
17
  throw new InvalidMessageError('Missing newline')
34
18
  }
35
19
 
36
- return buf.sublist(0, -1) // Remove newline
37
- }
38
-
39
- /**
40
- * Read a length-prefixed string from the passed stream, stripping the final newline character
41
- */
42
- export async function readString (reader: LengthPrefixedStream<Duplex<AsyncGenerator<Uint8Array | Uint8ArrayList>, Source<Uint8Array>>>, options: AbortOptions & LoggerOptions): Promise<string> {
43
- const buf = await read(reader, options)
44
-
45
- return uint8ArrayToString(buf.subarray())
20
+ return uint8ArrayToString(arr).trimEnd()
46
21
  }
package/src/select.ts CHANGED
@@ -1,23 +1,11 @@
1
1
  import { UnsupportedProtocolError } from '@libp2p/interface'
2
- import { lpStream } from 'it-length-prefixed-stream'
3
- import pDefer from 'p-defer'
4
- import { raceSignal } from 'race-signal'
5
- import * as varint from 'uint8-varint'
6
- import { Uint8ArrayList } from 'uint8arraylist'
2
+ import { lpStream } from '@libp2p/utils'
7
3
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
8
4
  import { MAX_PROTOCOL_LENGTH } from './constants.js'
9
- import * as multistream from './multistream.js'
5
+ import { readString } from './multistream.js'
10
6
  import { PROTOCOL_ID } from './index.js'
11
- import type { MultistreamSelectInit, ProtocolStream } from './index.js'
12
- import type { AbortOptions } from '@libp2p/interface'
13
- import type { Duplex } from 'it-stream-types'
14
-
15
- export interface SelectStream extends Duplex<any, any, any> {
16
- readStatus?: string
17
- closeWrite?(options?: AbortOptions): Promise<void>
18
- closeRead?(options?: AbortOptions): Promise<void>
19
- close?(options?: AbortOptions): Promise<void>
20
- }
7
+ import type { MultistreamSelectInit } from './index.js'
8
+ import type { MessageStream } from '@libp2p/interface'
21
9
 
22
10
  /**
23
11
  * Negotiate a protocol to use from a list of protocols.
@@ -62,304 +50,57 @@ export interface SelectStream extends Duplex<any, any, any> {
62
50
  * // }
63
51
  * ```
64
52
  */
65
- export async function select <Stream extends SelectStream> (stream: Stream, protocols: string | string[], options: MultistreamSelectInit): Promise<ProtocolStream<Stream>> {
53
+ export async function select <Stream extends MessageStream> (stream: Stream, protocols: string | string[], options: MultistreamSelectInit = {}): Promise<string> {
66
54
  protocols = Array.isArray(protocols) ? [...protocols] : [protocols]
67
55
 
68
- if (protocols.length === 1 && options.negotiateFully === false) {
69
- return optimisticSelect(stream, protocols[0], options)
70
- }
71
-
72
- const lp = lpStream(stream, {
73
- ...options,
74
- maxDataLength: MAX_PROTOCOL_LENGTH
75
- })
76
- const protocol = protocols.shift()
77
-
78
- if (protocol == null) {
56
+ if (protocols.length === 0) {
79
57
  throw new Error('At least one protocol must be specified')
80
58
  }
81
59
 
82
- options.log.trace('select: write ["%s", "%s"]', PROTOCOL_ID, protocol)
83
- const p1 = uint8ArrayFromString(`${PROTOCOL_ID}\n`)
84
- const p2 = uint8ArrayFromString(`${protocol}\n`)
85
- await multistream.writeAll(lp, [p1, p2], options)
86
-
87
- options.log.trace('select: reading multistream-select header')
88
- let response = await multistream.readString(lp, options)
89
- options.log.trace('select: read "%s"', response)
90
-
91
- // Read the protocol response if we got the protocolId in return
92
- if (response === PROTOCOL_ID) {
93
- options.log.trace('select: reading protocol response')
94
- response = await multistream.readString(lp, options)
95
- options.log.trace('select: read "%s"', response)
96
- }
97
-
98
- // We're done
99
- if (response === protocol) {
100
- return { stream: lp.unwrap(), protocol }
101
- }
102
-
103
- // We haven't gotten a valid ack, try the other protocols
104
- for (const protocol of protocols) {
105
- options.log.trace('select: write "%s"', protocol)
106
- await multistream.write(lp, uint8ArrayFromString(`${protocol}\n`), options)
107
- options.log.trace('select: reading protocol response')
108
- const response = await multistream.readString(lp, options)
109
- options.log.trace('select: read "%s" for "%s"', response, protocol)
110
-
111
- if (response === protocol) {
112
- return { stream: lp.unwrap(), protocol }
113
- }
114
- }
115
-
116
- throw new UnsupportedProtocolError('protocol selection failed')
117
- }
118
-
119
- /**
120
- * Optimistically negotiates a protocol.
121
- *
122
- * It *does not* block writes waiting for the other end to respond. Instead, it
123
- * simply assumes the negotiation went successfully and starts writing data.
124
- *
125
- * Use when it is known that the receiver supports the desired protocol.
126
- */
127
- function optimisticSelect <Stream extends SelectStream> (stream: Stream, protocol: string, options: MultistreamSelectInit): ProtocolStream<Stream> {
128
- const originalSink = stream.sink.bind(stream)
129
- const originalSource = stream.source
130
-
131
- let negotiated = false
132
- let negotiating = false
133
- const doneNegotiating = pDefer()
134
-
135
- let sentProtocol = false
136
- let sendingProtocol = false
137
- const doneSendingProtocol = pDefer()
138
-
139
- let readProtocol = false
140
- let readingProtocol = false
141
- const doneReadingProtocol = pDefer()
142
-
143
- const lp = lpStream({
144
- sink: originalSink,
145
- source: originalSource
146
- }, {
60
+ const log = stream.log.newScope('mss:select')
61
+ const lp = lpStream(stream, {
147
62
  ...options,
148
- maxDataLength: MAX_PROTOCOL_LENGTH
63
+ maxDataLength: MAX_PROTOCOL_LENGTH,
64
+ stopPropagation: true
149
65
  })
150
66
 
151
- stream.sink = async source => {
152
- const { sink } = lp.unwrap()
153
-
154
- await sink(async function * () {
155
- let sentData = false
156
-
157
- for await (const buf of source) {
158
- // started reading before the source yielded, wait for protocol send
159
- if (sendingProtocol) {
160
- await doneSendingProtocol.promise
161
- }
162
-
163
- // writing before reading, send the protocol and the first chunk of data
164
- if (!sentProtocol) {
165
- sendingProtocol = true
166
-
167
- options.log.trace('optimistic: write ["%s", "%s", data(%d)] in sink', PROTOCOL_ID, protocol, buf.byteLength)
168
-
169
- const protocolString = `${protocol}\n`
170
-
171
- // send protocols in first chunk of data written to transport
172
- yield new Uint8ArrayList(
173
- Uint8Array.from([19]), // length of PROTOCOL_ID plus newline
174
- uint8ArrayFromString(`${PROTOCOL_ID}\n`),
175
- varint.encode(protocolString.length),
176
- uint8ArrayFromString(protocolString),
177
- buf
178
- ).subarray()
179
-
180
- options.log.trace('optimistic: wrote ["%s", "%s", data(%d)] in sink', PROTOCOL_ID, protocol, buf.byteLength)
181
-
182
- sentProtocol = true
183
- sendingProtocol = false
184
- doneSendingProtocol.resolve()
185
-
186
- // read the negotiation response but don't block more sending
187
- negotiate()
188
- .catch(err => {
189
- options.log.error('could not finish optimistic protocol negotiation of %s', protocol, err)
190
- })
191
- } else {
192
- yield buf
193
- }
194
-
195
- sentData = true
196
- }
197
-
198
- // special case - the source passed to the sink has ended but we didn't
199
- // negotiated the protocol yet so do it now
200
- if (!sentData) {
201
- await negotiate()
202
- }
203
- }())
204
- }
205
-
206
- async function negotiate (): Promise<void> {
207
- if (negotiating) {
208
- options.log.trace('optimistic: already negotiating %s stream', protocol)
209
- await doneNegotiating.promise
210
- return
211
- }
212
-
213
- negotiating = true
214
-
215
- try {
216
- // we haven't sent the protocol yet, send it now
217
- if (!sentProtocol) {
218
- options.log.trace('optimistic: doing send protocol for %s stream', protocol)
219
- await doSendProtocol()
67
+ for (let i = 0; i < protocols.length; i++) {
68
+ const protocol = protocols[i]
69
+ let response: string
70
+
71
+ if (i === 0) {
72
+ // Write the multistream-select header along with the first protocol
73
+ log.trace('write ["%s", "%s"]', PROTOCOL_ID, protocol)
74
+ const p1 = uint8ArrayFromString(`${PROTOCOL_ID}\n`)
75
+ const p2 = uint8ArrayFromString(`${protocol}\n`)
76
+ await lp.writeV([p1, p2], options)
77
+
78
+ log.trace('reading multistream-select header')
79
+ response = await readString(lp, options)
80
+ log.trace('read "%s"', response)
81
+
82
+ // Read the protocol response if we got the protocolId in return
83
+ if (response !== PROTOCOL_ID) {
84
+ log.error('did not read multistream-select header from response')
85
+ break
220
86
  }
221
-
222
- // if we haven't read the protocol response yet, do it now
223
- if (!readProtocol) {
224
- options.log.trace('optimistic: doing read protocol for %s stream', protocol)
225
- await doReadProtocol()
226
- }
227
- } finally {
228
- negotiating = false
229
- negotiated = true
230
- doneNegotiating.resolve()
231
- }
232
- }
233
-
234
- async function doSendProtocol (): Promise<void> {
235
- if (sendingProtocol) {
236
- await doneSendingProtocol.promise
237
- return
238
- }
239
-
240
- sendingProtocol = true
241
-
242
- try {
243
- options.log.trace('optimistic: write ["%s", "%s", data] in source', PROTOCOL_ID, protocol)
244
- await lp.writeV([
245
- uint8ArrayFromString(`${PROTOCOL_ID}\n`),
246
- uint8ArrayFromString(`${protocol}\n`)
247
- ])
248
- options.log.trace('optimistic: wrote ["%s", "%s", data] in source', PROTOCOL_ID, protocol)
249
- } finally {
250
- sentProtocol = true
251
- sendingProtocol = false
252
- doneSendingProtocol.resolve()
87
+ } else {
88
+ // We haven't gotten a valid ack, try the other protocols
89
+ log.trace('write "%s"', protocol)
90
+ await lp.write(uint8ArrayFromString(`${protocol}\n`), options)
253
91
  }
254
- }
255
-
256
- async function doReadProtocol (): Promise<void> {
257
- if (readingProtocol) {
258
- await doneReadingProtocol.promise
259
- return
260
- }
261
-
262
- readingProtocol = true
263
-
264
- try {
265
- options.log.trace('optimistic: reading multistream select header')
266
- let response = await multistream.readString(lp, options)
267
- options.log.trace('optimistic: read multistream select header "%s"', response)
268
-
269
- if (response === PROTOCOL_ID) {
270
- response = await multistream.readString(lp, options)
271
- }
272
-
273
- options.log.trace('optimistic: read protocol "%s", expecting "%s"', response, protocol)
274
-
275
- if (response !== protocol) {
276
- throw new UnsupportedProtocolError('protocol selection failed')
277
- }
278
- } finally {
279
- readProtocol = true
280
- readingProtocol = false
281
- doneReadingProtocol.resolve()
282
- }
283
- }
284
92
 
285
- stream.source = (async function * () {
286
- // make sure we've done protocol negotiation before we read stream data
287
- await negotiate()
93
+ log.trace('reading protocol response')
94
+ response = await readString(lp, options)
95
+ log.trace('read "%s"', response)
288
96
 
289
- options.log.trace('optimistic: reading data from "%s" stream', protocol)
290
- yield * lp.unwrap().source
291
- })()
292
-
293
- if (stream.closeRead != null) {
294
- const originalCloseRead = stream.closeRead.bind(stream)
295
-
296
- stream.closeRead = async (opts) => {
297
- // we need to read & write to negotiate the protocol so ensure we've done
298
- // this before closing the readable end of the stream
299
- if (!negotiated) {
300
- await negotiate().catch(err => {
301
- options.log.error('could not negotiate protocol before close read', err)
302
- })
303
- }
304
-
305
- // protocol has been negotiated, ok to close the readable end
306
- await originalCloseRead(opts)
307
- }
308
- }
309
-
310
- if (stream.closeWrite != null) {
311
- const originalCloseWrite = stream.closeWrite.bind(stream)
312
-
313
- stream.closeWrite = async (opts) => {
314
- // we need to read & write to negotiate the protocol so ensure we've done
315
- // this before closing the writable end of the stream
316
- if (!negotiated) {
317
- await negotiate().catch(err => {
318
- options.log.error('could not negotiate protocol before close write', err)
319
- })
320
- }
321
-
322
- // protocol has been negotiated, ok to close the writable end
323
- await originalCloseWrite(opts)
324
- }
325
- }
326
-
327
- if (stream.close != null) {
328
- const originalClose = stream.close.bind(stream)
329
-
330
- stream.close = async (opts) => {
331
- // if we are in the process of negotiation, let it finish before closing
332
- // because we may have unsent early data
333
- const tasks = []
334
-
335
- if (sendingProtocol) {
336
- tasks.push(doneSendingProtocol.promise)
337
- }
338
-
339
- if (readingProtocol) {
340
- tasks.push(doneReadingProtocol.promise)
341
- }
342
-
343
- if (tasks.length > 0) {
344
- // let the in-flight protocol negotiation finish gracefully
345
- await raceSignal(
346
- Promise.all(tasks),
347
- opts?.signal
348
- )
349
- } else {
350
- // no protocol negotiation attempt has occurred so don't start one
351
- negotiated = true
352
- negotiating = false
353
- doneNegotiating.resolve()
354
- }
97
+ if (response === protocol) {
98
+ log.trace('selected "%s" after negotiation', response)
99
+ lp.unwrap()
355
100
 
356
- // protocol has been negotiated, ok to close the writable end
357
- await originalClose(opts)
101
+ return protocol
358
102
  }
359
103
  }
360
104
 
361
- return {
362
- stream,
363
- protocol
364
- }
105
+ throw new UnsupportedProtocolError(`Protocol selection failed - could not negotiate ${protocols}`)
365
106
  }
@@ -1,10 +0,0 @@
1
- {
2
- "MultistreamSelectInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_multistream-select.MultistreamSelectInit.html",
3
- ".:MultistreamSelectInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_multistream-select.MultistreamSelectInit.html",
4
- "ProtocolStream": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_multistream-select.ProtocolStream.html",
5
- ".:ProtocolStream": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_multistream-select.ProtocolStream.html",
6
- "SelectStream": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_multistream-select.SelectStream.html",
7
- "PROTOCOL_ID": "https://libp2p.github.io/js-libp2p/variables/_libp2p_multistream-select.PROTOCOL_ID.html",
8
- "handle": "https://libp2p.github.io/js-libp2p/functions/_libp2p_multistream-select.handle.html",
9
- "select": "https://libp2p.github.io/js-libp2p/functions/_libp2p_multistream-select.select.html"
10
- }