@libp2p/multistream-select 4.0.6 → 4.0.7-551622a96

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