@libp2p/multistream-select 4.0.9 → 4.0.10

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,46 +1,98 @@
1
- import { CodeError } from '@libp2p/interface'
2
- import { type Uint8ArrayList } from 'uint8arraylist'
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'
3
8
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
4
9
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
10
+ import { MAX_PROTOCOL_LENGTH } from './constants.js'
5
11
  import type { MultistreamSelectInit } from '.'
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'
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')
9
18
 
10
19
  const NewLine = uint8ArrayFromString('\n')
11
20
 
21
+ export function encode (buffer: Uint8Array | Uint8ArrayList): Uint8ArrayList {
22
+ const list = new Uint8ArrayList(buffer, NewLine)
23
+
24
+ return lp.encode.single(list)
25
+ }
26
+
12
27
  /**
13
28
  * `write` encodes and writes a single buffer
14
29
  */
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)
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
+ }
17
38
  }
18
39
 
19
40
  /**
20
41
  * `writeAll` behaves like `write`, except it encodes an array of items as a single write
21
42
  */
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)
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
+ }
24
55
  }
25
56
 
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)
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
+ }
31
76
 
32
- if (buf.byteLength === 0 || buf.get(buf.byteLength - 1) !== NewLine[0]) {
33
- options?.log?.error('Invalid mss message - missing newline', buf)
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
+ }
86
+
87
+ if (buf.get(buf.byteLength - 1) !== NewLine[0]) {
88
+ log.error('Invalid mss message - missing newline - %s', buf.subarray())
34
89
  throw new CodeError('missing newline', 'ERR_INVALID_MULTISTREAM_SELECT_MESSAGE')
35
90
  }
36
91
 
37
92
  return buf.sublist(0, -1) // Remove newline
38
93
  }
39
94
 
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> {
95
+ export async function readString (reader: Reader, options?: AbortOptions): Promise<string> {
44
96
  const buf = await read(reader, options)
45
97
 
46
98
  return uint8ArrayToString(buf.subarray())
package/src/select.ts CHANGED
@@ -1,22 +1,17 @@
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'
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'
5
7
  import { Uint8ArrayList } from 'uint8arraylist'
6
8
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
7
- import { MAX_PROTOCOL_LENGTH } from './constants.js'
8
9
  import * as multistream from './multistream.js'
9
10
  import { PROTOCOL_ID } from './index.js'
10
- import type { MultistreamSelectInit, ProtocolStream } from './index.js'
11
- import type { AbortOptions } from '@libp2p/interface'
12
- import type { Duplex } from 'it-stream-types'
11
+ import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js'
12
+ import type { Duplex, Source } from 'it-stream-types'
13
13
 
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
- }
14
+ const log = logger('libp2p:mss:select')
20
15
 
21
16
  /**
22
17
  * Negotiate a protocol to use from a list of protocols.
@@ -61,281 +56,106 @@ export interface SelectStream extends Duplex<any, any, any> {
61
56
  * // }
62
57
  * ```
63
58
  */
64
- export async function select <Stream extends SelectStream> (stream: Stream, protocols: string | string[], options: MultistreamSelectInit): Promise<ProtocolStream<Stream>> {
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>> {
65
62
  protocols = Array.isArray(protocols) ? [...protocols] : [protocols]
63
+ const { reader, writer, rest, stream: shakeStream } = handshake(stream)
66
64
 
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
- })
75
65
  const protocol = protocols.shift()
76
66
 
77
67
  if (protocol == null) {
78
68
  throw new Error('At least one protocol must be specified')
79
69
  }
80
70
 
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)
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)
85
75
 
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)
76
+ let response = await multistream.readString(reader, options)
77
+ log.trace('select: read "%s"', response)
89
78
 
90
79
  // Read the protocol response if we got the protocolId in return
91
80
  if (response === PROTOCOL_ID) {
92
- options?.log?.trace('select: reading protocol response')
93
- response = await multistream.readString(lp, options)
94
- options?.log?.trace('select: read "%s"', response)
81
+ response = await multistream.readString(reader, options)
82
+ log.trace('select: read "%s"', response)
95
83
  }
96
84
 
97
85
  // We're done
98
86
  if (response === protocol) {
99
- return { stream: lp.unwrap(), protocol }
87
+ rest()
88
+ return { stream: shakeStream, protocol }
100
89
  }
101
90
 
102
91
  // We haven't gotten a valid ack, try the other protocols
103
92
  for (const protocol of protocols) {
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)
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)
109
97
 
110
98
  if (response === protocol) {
111
- return { stream: lp.unwrap(), protocol }
99
+ rest() // End our writer so others can start writing to stream
100
+ return { stream: shakeStream, protocol }
112
101
  }
113
102
  }
114
103
 
104
+ rest()
115
105
  throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL')
116
106
  }
117
107
 
118
108
  /**
119
- * Optimistically negotiates a protocol.
109
+ * Lazily negotiates a protocol.
120
110
  *
121
111
  * It *does not* block writes waiting for the other end to respond. Instead, it
122
112
  * simply assumes the negotiation went successfully and starts writing data.
123
113
  *
124
114
  * Use when it is known that the receiver supports the desired protocol.
125
115
  */
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
-
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()
130
122
  let negotiated = false
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
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)
160
150
  }
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
151
+ if (response !== protocol) {
152
+ throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL')
186
153
  }
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,
154
+ for await (const chunk of byteReader) {
155
+ yield * chunk
156
+ }
157
+ })()
158
+ },
339
159
  protocol
340
160
  }
341
161
  }
@@ -1,9 +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
- "PROTOCOL_ID": "https://libp2p.github.io/js-libp2p/variables/_libp2p_multistream_select.PROTOCOL_ID.html",
7
- "handle": "https://libp2p.github.io/js-libp2p/functions/_libp2p_multistream_select.handle.html",
8
- "select": "https://libp2p.github.io/js-libp2p/functions/_libp2p_multistream_select.select.html"
9
- }