@libp2p/webrtc 2.0.2 → 2.0.4

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/stream.ts CHANGED
@@ -1,30 +1,22 @@
1
+ import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface-stream-muxer/stream'
2
+ import { CodeError } from '@libp2p/interfaces/errors'
1
3
  import { logger } from '@libp2p/logger'
2
4
  import * as lengthPrefixed from 'it-length-prefixed'
3
- import merge from 'it-merge'
4
- import { pipe } from 'it-pipe'
5
- import { pushable } from 'it-pushable'
6
- import defer, { type DeferredPromise } from 'p-defer'
5
+ import { type Pushable, pushable } from 'it-pushable'
6
+ import { pEvent, TimeoutError } from 'p-event'
7
7
  import { Uint8ArrayList } from 'uint8arraylist'
8
8
  import { Message } from './pb/message.js'
9
- import type { Stream, StreamStat, Direction } from '@libp2p/interface-connection'
10
- import type { Source } from 'it-stream-types'
9
+ import type { Direction, Stream } from '@libp2p/interface-connection'
11
10
 
12
11
  const log = logger('libp2p:webrtc:stream')
13
12
 
14
- /**
15
- * Constructs a default StreamStat
16
- */
17
- export function defaultStat (dir: Direction): StreamStat {
18
- return {
19
- direction: dir,
20
- timeline: {
21
- open: 0,
22
- close: undefined
23
- }
24
- }
13
+ export interface DataChannelOpts {
14
+ maxMessageSize: number
15
+ maxBufferedAmount: number
16
+ bufferedAmountLowEventTimeout: number
25
17
  }
26
18
 
27
- interface StreamInitOpts {
19
+ export interface WebRTCStreamInit extends AbstractStreamInit {
28
20
  /**
29
21
  * The network channel used for bidirectional peer-to-peer transfers of
30
22
  * arbitrary data
@@ -33,216 +25,85 @@ interface StreamInitOpts {
33
25
  */
34
26
  channel: RTCDataChannel
35
27
 
36
- /**
37
- * User defined stream metadata
38
- */
39
- metadata?: Record<string, any>
40
-
41
- /**
42
- * Stats about this stream
43
- */
44
- stat: StreamStat
45
-
46
- /**
47
- * Callback to invoke when the stream is closed.
48
- */
49
- closeCb?: (stream: WebRTCStream) => void
50
- }
51
-
52
- /*
53
- * State transitions for a stream
54
- */
55
- interface StreamStateInput {
56
- /**
57
- * Outbound conections are opened by the local node, inbound streams are
58
- * opened by the remote
59
- */
60
- direction: 'inbound' | 'outbound'
61
-
62
- /**
63
- * Message flag from the protobufs
64
- *
65
- * 0 = FIN
66
- * 1 = STOP_SENDING
67
- * 2 = RESET
68
- */
69
- flag: Message.Flag
70
- }
71
-
72
- export enum StreamStates {
73
- OPEN,
74
- READ_CLOSED,
75
- WRITE_CLOSED,
76
- CLOSED,
28
+ dataChannelOptions?: Partial<DataChannelOpts>
77
29
  }
78
30
 
79
- // Checked by the Typescript compiler. If this fails it's because the switch
80
- // statement is not exhaustive.
81
- function unreachableBranch (x: never): never {
82
- throw new Error('Case not handled in switch')
83
- }
84
-
85
- class StreamState {
86
- state: StreamStates = StreamStates.OPEN
87
-
88
- isWriteClosed (): boolean {
89
- return (this.state === StreamStates.CLOSED || this.state === StreamStates.WRITE_CLOSED)
90
- }
91
-
92
- transition ({ direction, flag }: StreamStateInput): [StreamStates, StreamStates] {
93
- const prev = this.state
94
-
95
- // return early if the stream is closed
96
- if (this.state === StreamStates.CLOSED) {
97
- return [prev, StreamStates.CLOSED]
98
- }
99
-
100
- if (direction === 'inbound') {
101
- switch (flag) {
102
- case Message.Flag.FIN:
103
- if (this.state === StreamStates.OPEN) {
104
- this.state = StreamStates.READ_CLOSED
105
- } else if (this.state === StreamStates.WRITE_CLOSED) {
106
- this.state = StreamStates.CLOSED
107
- }
108
- break
109
-
110
- case Message.Flag.STOP_SENDING:
111
- if (this.state === StreamStates.OPEN) {
112
- this.state = StreamStates.WRITE_CLOSED
113
- } else if (this.state === StreamStates.READ_CLOSED) {
114
- this.state = StreamStates.CLOSED
115
- }
116
- break
117
-
118
- case Message.Flag.RESET:
119
- this.state = StreamStates.CLOSED
120
- break
121
- default:
122
- unreachableBranch(flag)
123
- }
124
- } else {
125
- switch (flag) {
126
- case Message.Flag.FIN:
127
- if (this.state === StreamStates.OPEN) {
128
- this.state = StreamStates.WRITE_CLOSED
129
- } else if (this.state === StreamStates.READ_CLOSED) {
130
- this.state = StreamStates.CLOSED
131
- }
132
- break
133
-
134
- case Message.Flag.STOP_SENDING:
135
- if (this.state === StreamStates.OPEN) {
136
- this.state = StreamStates.READ_CLOSED
137
- } else if (this.state === StreamStates.WRITE_CLOSED) {
138
- this.state = StreamStates.CLOSED
139
- }
140
- break
141
-
142
- case Message.Flag.RESET:
143
- this.state = StreamStates.CLOSED
144
- break
145
-
146
- default:
147
- unreachableBranch(flag)
148
- }
149
- }
150
- return [prev, this.state]
151
- }
152
- }
31
+ // Max message size that can be sent to the DataChannel
32
+ const MAX_MESSAGE_SIZE = 16 * 1024
153
33
 
154
- export class WebRTCStream implements Stream {
155
- /**
156
- * Unique identifier for a stream
157
- */
158
- id: string
34
+ // How much can be buffered to the DataChannel at once
35
+ const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024
159
36
 
160
- /**
161
- * Stats about this stream
162
- */
163
- stat: StreamStat
37
+ // How long time we wait for the 'bufferedamountlow' event to be emitted
38
+ const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000
164
39
 
165
- /**
166
- * User defined stream metadata
167
- */
168
- metadata: Record<string, any>
40
+ // protobuf field definition overhead
41
+ const PROTOBUF_OVERHEAD = 3
169
42
 
43
+ class WebRTCStream extends AbstractStream {
170
44
  /**
171
45
  * The data channel used to send and receive data
172
46
  */
173
47
  private readonly channel: RTCDataChannel
174
48
 
175
49
  /**
176
- * The current state of the stream
50
+ * Data channel options
177
51
  */
178
- streamState = new StreamState()
179
-
180
- /**
181
- * Read unwrapped protobuf data from the underlying datachannel.
182
- * _src is exposed to the user via the `source` getter to .
183
- */
184
- private readonly _src: AsyncGenerator<Uint8ArrayList, any, unknown>
52
+ private readonly dataChannelOptions: DataChannelOpts
185
53
 
186
54
  /**
187
55
  * push data from the underlying datachannel to the length prefix decoder
188
56
  * and then the protobuf decoder.
189
57
  */
190
- private readonly _innersrc = pushable()
191
-
192
- /**
193
- * Deferred promise that resolves when the underlying datachannel is in the
194
- * open state.
195
- */
196
- opened: DeferredPromise<void> = defer()
197
-
198
- /**
199
- * sinkCreated is set to true once the sinkFunction is invoked
200
- */
201
- _sinkCalled: boolean = false
58
+ private readonly incomingData: Pushable<Uint8Array>
202
59
 
203
- /**
204
- * Triggers a generator which can be used to close the sink.
205
- */
206
- closeWritePromise: DeferredPromise<void> = defer()
60
+ private messageQueue?: Uint8ArrayList
207
61
 
208
- /**
209
- * Callback to invoke when the stream is closed.
210
- */
211
- closeCb?: (stream: WebRTCStream) => void
62
+ constructor (init: WebRTCStreamInit) {
63
+ super(init)
212
64
 
213
- constructor (opts: StreamInitOpts) {
214
- this.channel = opts.channel
65
+ this.channel = init.channel
215
66
  this.channel.binaryType = 'arraybuffer'
216
- this.id = this.channel.label
67
+ this.incomingData = pushable()
68
+ this.messageQueue = new Uint8ArrayList()
69
+ this.dataChannelOptions = {
70
+ bufferedAmountLowEventTimeout: init.dataChannelOptions?.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT,
71
+ maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT,
72
+ maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE
73
+ }
217
74
 
218
- this.stat = opts.stat
75
+ // set up initial state
219
76
  switch (this.channel.readyState) {
220
77
  case 'open':
221
- this.opened.resolve()
222
78
  break
223
79
 
224
80
  case 'closed':
225
81
  case 'closing':
226
- this.streamState.state = StreamStates.CLOSED
227
82
  if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) {
228
- this.stat.timeline.close = new Date().getTime()
83
+ this.stat.timeline.close = Date.now()
229
84
  }
230
- this.opened.resolve()
231
85
  break
232
86
  case 'connecting':
233
87
  // noop
234
88
  break
235
89
 
236
90
  default:
237
- unreachableBranch(this.channel.readyState)
91
+ log.error('unknown datachannel state %s', this.channel.readyState)
92
+ throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE')
238
93
  }
239
94
 
240
- this.metadata = opts.metadata ?? {}
241
-
242
95
  // handle RTCDataChannel events
243
96
  this.channel.onopen = (_evt) => {
244
97
  this.stat.timeline.open = new Date().getTime()
245
- this.opened.resolve()
98
+
99
+ if (this.messageQueue != null) {
100
+ // send any queued messages
101
+ this._sendMessage(this.messageQueue)
102
+ .catch(err => {
103
+ this.abort(err)
104
+ })
105
+ this.messageQueue = undefined
106
+ }
246
107
  }
247
108
 
248
109
  this.channel.onclose = (_evt) => {
@@ -256,207 +117,157 @@ export class WebRTCStream implements Stream {
256
117
 
257
118
  const self = this
258
119
 
259
- // reader pipe
260
- this.channel.onmessage = async ({ data }) => {
261
- if (data === null || data.length === 0) {
120
+ this.channel.onmessage = async (event: MessageEvent<ArrayBuffer>) => {
121
+ const { data } = event
122
+
123
+ if (data === null || data.byteLength === 0) {
262
124
  return
263
125
  }
264
- this._innersrc.push(new Uint8Array(data as ArrayBufferLike))
126
+
127
+ this.incomingData.push(new Uint8Array(data, 0, data.byteLength))
265
128
  }
266
129
 
267
130
  // pipe framed protobuf messages through a length prefixed decoder, and
268
131
  // surface data from the `Message.message` field through a source.
269
- this._src = pipe(
270
- this._innersrc,
271
- (source) => lengthPrefixed.decode(source),
272
- (source) => (async function * () {
273
- for await (const buf of source) {
274
- const message = self.processIncomingProtobuf(buf.subarray())
275
- if (message != null) {
276
- yield new Uint8ArrayList(message)
277
- }
132
+ Promise.resolve().then(async () => {
133
+ for await (const buf of lengthPrefixed.decode(this.incomingData)) {
134
+ const message = self.processIncomingProtobuf(buf.subarray())
135
+
136
+ if (message != null) {
137
+ self.sourcePush(new Uint8ArrayList(message))
278
138
  }
279
- })()
280
- )
139
+ }
140
+ })
141
+ .catch(err => {
142
+ log.error('error processing incoming data channel messages', err)
143
+ })
281
144
  }
282
145
 
283
- // If user attempts to set a new source this should be a noop
284
- set source (_src: AsyncGenerator<Uint8ArrayList, any, unknown>) { }
285
-
286
- get source (): AsyncGenerator<Uint8ArrayList, any, unknown> {
287
- return this._src
146
+ sendNewStream (): void {
147
+ // opening new streams is handled by WebRTC so this is a noop
288
148
  }
289
149
 
290
- /**
291
- * Write data to the remote peer.
292
- * It takes care of wrapping data in a protobuf and adding the length prefix.
293
- */
294
- async sink (src: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
295
- if (this._sinkCalled) {
296
- throw new Error('sink already called on this stream')
150
+ async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise<void> {
151
+ if (checkBuffer && this.channel.bufferedAmount > this.dataChannelOptions.maxBufferedAmount) {
152
+ try {
153
+ await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout })
154
+ } catch (err: any) {
155
+ if (err instanceof TimeoutError) {
156
+ this.abort(err)
157
+ throw new Error('Timed out waiting for DataChannel buffer to clear')
158
+ }
159
+
160
+ throw err
161
+ }
297
162
  }
298
- // await stream opening before sending data
299
- await this.opened.promise
300
- try {
301
- await this._sink(src)
302
- } finally {
303
- this.closeWrite()
163
+
164
+ if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') {
165
+ throw new CodeError('Invalid datachannel state - closed or closing', 'ERR_INVALID_STATE')
304
166
  }
305
- }
306
167
 
307
- /**
308
- * Closable sink implementation
309
- */
310
- private async _sink (src: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
311
- const closeWrite = this._closeWriteIterable()
312
- for await (const buf of merge(closeWrite, src)) {
313
- if (this.streamState.isWriteClosed()) {
314
- return
168
+ if (this.channel.readyState === 'open') {
169
+ // send message without copying data
170
+ for (const buf of data) {
171
+ this.channel.send(buf)
172
+ }
173
+ } else if (this.channel.readyState === 'connecting') {
174
+ // queue message for when we are open
175
+ if (this.messageQueue == null) {
176
+ this.messageQueue = new Uint8ArrayList()
315
177
  }
316
- const msgbuf = Message.encode({ message: buf.subarray() })
317
- const sendbuf = lengthPrefixed.encode.single(msgbuf)
318
178
 
319
- this.channel.send(sendbuf.subarray())
179
+ this.messageQueue.append(data)
180
+ } else {
181
+ log.error('unknown datachannel state %s', this.channel.readyState)
182
+ throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE')
320
183
  }
321
184
  }
322
185
 
323
- /**
324
- * Handle incoming
325
- */
326
- processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined {
327
- const message = Message.decode(buffer)
186
+ async sendData (data: Uint8ArrayList): Promise<void> {
187
+ const msgbuf = Message.encode({ message: data.subarray() })
188
+ const sendbuf = lengthPrefixed.encode.single(msgbuf)
328
189
 
329
- if (message.flag !== undefined) {
330
- const [currentState, nextState] = this.streamState.transition({ direction: 'inbound', flag: message.flag })
331
-
332
- if (currentState !== nextState) {
333
- switch (nextState) {
334
- case StreamStates.READ_CLOSED:
335
- this._innersrc.end()
336
- break
337
- case StreamStates.WRITE_CLOSED:
338
- this.closeWritePromise.resolve()
339
- break
340
- case StreamStates.CLOSED:
341
- this.close()
342
- break
343
- // StreamStates.OPEN will never be a nextState
344
- case StreamStates.OPEN:
345
- break
346
- default:
347
- unreachableBranch(nextState)
348
- }
349
- }
350
- }
190
+ await this._sendMessage(sendbuf)
191
+ }
351
192
 
352
- return message.message
193
+ async sendReset (): Promise<void> {
194
+ await this._sendFlag(Message.Flag.RESET)
353
195
  }
354
196
 
355
- /**
356
- * Close a stream for reading and writing
357
- */
358
- close (): void {
359
- this.stat.timeline.close = new Date().getTime()
360
- this.streamState.state = StreamStates.CLOSED
361
- this._innersrc.end()
362
- this.closeWritePromise.resolve()
363
- this.channel.close()
364
-
365
- if (this.closeCb !== undefined) {
366
- this.closeCb(this)
367
- }
197
+ async sendCloseWrite (): Promise<void> {
198
+ await this._sendFlag(Message.Flag.FIN)
199
+ }
200
+
201
+ async sendCloseRead (): Promise<void> {
202
+ await this._sendFlag(Message.Flag.STOP_SENDING)
368
203
  }
369
204
 
370
205
  /**
371
- * Close a stream for reading only
206
+ * Handle incoming
372
207
  */
373
- closeRead (): void {
374
- const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: Message.Flag.STOP_SENDING })
375
- if (currentState === nextState) {
376
- // No change, no op
377
- return
378
- }
208
+ private processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined {
209
+ const message = Message.decode(buffer)
379
210
 
380
- if (currentState === StreamStates.OPEN || currentState === StreamStates.WRITE_CLOSED) {
381
- this._sendFlag(Message.Flag.STOP_SENDING)
382
- this._innersrc.end()
383
- }
211
+ if (message.flag !== undefined) {
212
+ if (message.flag === Message.Flag.FIN) {
213
+ // We should expect no more data from the remote, stop reading
214
+ this.incomingData.end()
215
+ this.closeRead()
216
+ }
384
217
 
385
- if (nextState === StreamStates.CLOSED) {
386
- this.close()
387
- }
388
- }
218
+ if (message.flag === Message.Flag.RESET) {
219
+ // Stop reading and writing to the stream immediately
220
+ this.reset()
221
+ }
389
222
 
390
- /**
391
- * Close a stream for writing only
392
- */
393
- closeWrite (): void {
394
- const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: Message.Flag.FIN })
395
- if (currentState === nextState) {
396
- // No change, no op
397
- return
223
+ if (message.flag === Message.Flag.STOP_SENDING) {
224
+ // The remote has stopped reading
225
+ this.closeWrite()
226
+ }
398
227
  }
399
228
 
400
- if (currentState === StreamStates.OPEN || currentState === StreamStates.READ_CLOSED) {
401
- this._sendFlag(Message.Flag.FIN)
402
- this.closeWritePromise.resolve()
403
- }
229
+ return message.message
230
+ }
404
231
 
405
- if (nextState === StreamStates.CLOSED) {
406
- this.close()
407
- }
232
+ private async _sendFlag (flag: Message.Flag): Promise<void> {
233
+ log.trace('Sending flag: %s', flag.toString())
234
+ const msgbuf = Message.encode({ flag })
235
+ const prefixedBuf = lengthPrefixed.encode.single(msgbuf)
236
+
237
+ await this._sendMessage(prefixedBuf, false)
408
238
  }
239
+ }
409
240
 
241
+ export interface WebRTCStreamOptions {
410
242
  /**
411
- * Call when a local error occurs, should close the stream for reading and writing
243
+ * The network channel used for bidirectional peer-to-peer transfers of
244
+ * arbitrary data
245
+ *
246
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel}
412
247
  */
413
- abort (err: Error): void {
414
- log.error(`An error occurred, closing the stream for reading and writing: ${err.message}`)
415
- this.close()
416
- }
248
+ channel: RTCDataChannel
417
249
 
418
250
  /**
419
- * Close the stream for writing, and indicate to the remote side this is being done 'abruptly'
420
- *
421
- * @see this.closeWrite
251
+ * The stream direction
422
252
  */
423
- reset (): void {
424
- const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: Message.Flag.RESET })
425
- if (currentState === nextState) {
426
- // No change, no op
427
- return
428
- }
253
+ direction: Direction
429
254
 
430
- this._sendFlag(Message.Flag.RESET)
431
- this.close()
432
- }
255
+ dataChannelOptions?: Partial<DataChannelOpts>
433
256
 
434
- private _sendFlag (flag: Message.Flag): void {
435
- try {
436
- log.trace('Sending flag: %s', flag.toString())
437
- const msgbuf = Message.encode({ flag })
438
- this.channel.send(lengthPrefixed.encode.single(msgbuf).subarray())
439
- } catch (err) {
440
- if (err instanceof Error) {
441
- log.error(`Exception while sending flag ${flag}: ${err.message}`)
442
- }
443
- }
444
- }
257
+ maxMsgSize?: number
445
258
 
446
- private _closeWriteIterable (): Source<Uint8ArrayList | Uint8Array> {
447
- const self = this
448
- return {
449
- async * [Symbol.asyncIterator] () {
450
- await self.closeWritePromise.promise
451
- yield new Uint8Array(0)
452
- }
453
- }
454
- }
259
+ onEnd?: (err?: Error | undefined) => void
260
+ }
455
261
 
456
- eq (stream: Stream): boolean {
457
- if (stream instanceof WebRTCStream) {
458
- return stream.channel.id === this.channel.id
459
- }
460
- return false
461
- }
262
+ export function createStream (options: WebRTCStreamOptions): Stream {
263
+ const { channel, direction, onEnd, dataChannelOptions } = options
264
+
265
+ return new WebRTCStream({
266
+ id: direction === 'inbound' ? (`i${channel.id}`) : `r${channel.id}`,
267
+ direction,
268
+ maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD,
269
+ dataChannelOptions,
270
+ onEnd,
271
+ channel
272
+ })
462
273
  }