@libp2p/multistream-select 4.0.6-05b52d69c → 4.0.6-0b4a2ee79

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/select.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import { CodeError } from '@libp2p/interface/errors'
2
- import { handshake } from 'it-handshake'
3
- import merge from 'it-merge'
4
- import { pushable } from 'it-pushable'
5
- import { reader } from 'it-reader'
2
+ import { lpStream } from 'it-length-prefixed-stream'
3
+ import pDefer from 'p-defer'
4
+ import * as varint from 'uint8-varint'
6
5
  import { Uint8ArrayList } from 'uint8arraylist'
7
6
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
7
+ import { MAX_PROTOCOL_LENGTH } from './constants.js'
8
8
  import * as multistream from './multistream.js'
9
9
  import { PROTOCOL_ID } from './index.js'
10
- import type { ByteArrayInit, ByteListInit, MultistreamSelectInit, ProtocolStream } from './index.js'
11
- 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
+
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
+ }
12
20
 
13
21
  /**
14
22
  * Negotiate a protocol to use from a list of protocols.
@@ -53,12 +61,17 @@ import type { Duplex, Source } from 'it-stream-types'
53
61
  * // }
54
62
  * ```
55
63
  */
56
- export async function select (stream: Duplex<AsyncGenerator<Uint8Array>, Source<Uint8Array>>, protocols: string | string[], options: ByteArrayInit): Promise<ProtocolStream<Uint8Array>>
57
- export async function select (stream: Duplex<AsyncGenerator<Uint8ArrayList | Uint8Array>, Source<Uint8ArrayList | Uint8Array>>, protocols: string | string[], options?: ByteListInit): Promise<ProtocolStream<Uint8ArrayList, Uint8ArrayList | Uint8Array>>
58
- 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>> {
59
65
  protocols = Array.isArray(protocols) ? [...protocols] : [protocols]
60
- const { reader, writer, rest, stream: shakeStream } = handshake(stream)
61
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
+ })
62
75
  const protocol = protocols.shift()
63
76
 
64
77
  if (protocol == null) {
@@ -66,93 +79,263 @@ export async function select (stream: any, protocols: string | string[], options
66
79
  }
67
80
 
68
81
  options?.log.trace('select: write ["%s", "%s"]', PROTOCOL_ID, protocol)
69
- const p1 = uint8ArrayFromString(PROTOCOL_ID)
70
- const p2 = uint8ArrayFromString(protocol)
71
- multistream.writeAll(writer, [p1, p2], options)
82
+ const p1 = uint8ArrayFromString(`${PROTOCOL_ID}\n`)
83
+ const p2 = uint8ArrayFromString(`${protocol}\n`)
84
+ await multistream.writeAll(lp, [p1, p2], options)
72
85
 
73
- let response = await multistream.readString(reader, options)
86
+ options?.log.trace('select: reading multistream-select header')
87
+ let response = await multistream.readString(lp, options)
74
88
  options?.log.trace('select: read "%s"', response)
75
89
 
76
90
  // Read the protocol response if we got the protocolId in return
77
91
  if (response === PROTOCOL_ID) {
78
- response = await multistream.readString(reader, options)
92
+ options?.log.trace('select: reading protocol response')
93
+ response = await multistream.readString(lp, options)
79
94
  options?.log.trace('select: read "%s"', response)
80
95
  }
81
96
 
82
97
  // We're done
83
98
  if (response === protocol) {
84
- rest()
85
- return { stream: shakeStream, protocol }
99
+ return { stream: lp.unwrap(), protocol }
86
100
  }
87
101
 
88
102
  // We haven't gotten a valid ack, try the other protocols
89
103
  for (const protocol of protocols) {
90
104
  options?.log.trace('select: write "%s"', protocol)
91
- multistream.write(writer, uint8ArrayFromString(protocol), options)
92
- const response = await multistream.readString(reader, options)
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)
93
108
  options?.log.trace('select: read "%s" for "%s"', response, protocol)
94
109
 
95
110
  if (response === protocol) {
96
- rest() // End our writer so others can start writing to stream
97
- return { stream: shakeStream, protocol }
111
+ return { stream: lp.unwrap(), protocol }
98
112
  }
99
113
  }
100
114
 
101
- rest()
102
115
  throw new CodeError('protocol selection failed', 'ERR_UNSUPPORTED_PROTOCOL')
103
116
  }
104
117
 
105
118
  /**
106
- * Lazily negotiates a protocol.
119
+ * Optimistically negotiates a protocol.
107
120
  *
108
121
  * It *does not* block writes waiting for the other end to respond. Instead, it
109
122
  * simply assumes the negotiation went successfully and starts writing data.
110
123
  *
111
124
  * Use when it is known that the receiver supports the desired protocol.
112
125
  */
113
- export function lazySelect (stream: Duplex<Source<Uint8Array>, Source<Uint8Array>>, protocol: string): ProtocolStream<Uint8Array>
114
- export function lazySelect (stream: Duplex<Source<Uint8ArrayList | Uint8Array>, Source<Uint8ArrayList | Uint8Array>>, protocol: string): ProtocolStream<Uint8ArrayList, Uint8ArrayList | Uint8Array>
115
- export function lazySelect (stream: Duplex<any>, protocol: string): ProtocolStream<any> {
116
- // This is a signal to write the multistream headers if the consumer tries to
117
- // read from the source
118
- 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
+
119
130
  let negotiated = false
120
- return {
121
- stream: {
122
- sink: async source => {
123
- await stream.sink((async function * () {
124
- let first = true
125
- for await (const chunk of merge(source, negotiateTrigger)) {
126
- if (first) {
127
- first = false
128
- negotiated = true
129
- negotiateTrigger.end()
130
- const p1 = uint8ArrayFromString(PROTOCOL_ID)
131
- const p2 = uint8ArrayFromString(protocol)
132
- const list = new Uint8ArrayList(multistream.encode(p1), multistream.encode(p2))
133
- if (chunk.length > 0) list.append(chunk)
134
- yield * list
135
- } else {
136
- yield chunk
137
- }
138
- }
139
- })())
140
- },
141
- source: (async function * () {
142
- if (!negotiated) negotiateTrigger.push(new Uint8Array())
143
- const byteReader = reader(stream.source)
144
- let response = await multistream.readString(byteReader)
145
- if (response === PROTOCOL_ID) {
146
- response = await multistream.readString(byteReader)
147
- }
148
- if (response !== protocol) {
149
- 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
150
160
  }
151
- for await (const chunk of byteReader) {
152
- 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
153
186
  }
154
- })()
155
- },
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,
156
339
  protocol
157
340
  }
158
341
  }