@libp2p/webrtc 5.2.24-6059227cb → 5.2.24-87bc8d4fb
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/README.md +20 -10
- package/dist/index.min.js +18 -18
- package/dist/index.min.js.map +4 -4
- package/dist/src/constants.d.ts +23 -4
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +23 -4
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +22 -20
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +22 -12
- package/dist/src/index.js.map +1 -1
- package/dist/src/maconn.d.ts +58 -0
- package/dist/src/maconn.d.ts.map +1 -0
- package/dist/src/maconn.js +56 -0
- package/dist/src/maconn.js.map +1 -0
- package/dist/src/muxer.d.ts +46 -14
- package/dist/src/muxer.d.ts.map +1 -1
- package/dist/src/muxer.js +138 -30
- package/dist/src/muxer.js.map +1 -1
- package/dist/src/private-to-private/initiate-connection.d.ts +3 -2
- package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -1
- package/dist/src/private-to-private/initiate-connection.js +5 -37
- package/dist/src/private-to-private/initiate-connection.js.map +1 -1
- package/dist/src/private-to-private/signaling-stream-handler.d.ts +4 -4
- package/dist/src/private-to-private/signaling-stream-handler.d.ts.map +1 -1
- package/dist/src/private-to-private/signaling-stream-handler.js +7 -19
- package/dist/src/private-to-private/signaling-stream-handler.js.map +1 -1
- package/dist/src/private-to-private/transport.d.ts +9 -2
- package/dist/src/private-to-private/transport.d.ts.map +1 -1
- package/dist/src/private-to-private/transport.js +15 -30
- package/dist/src/private-to-private/transport.js.map +1 -1
- package/dist/src/private-to-private/util.d.ts +2 -3
- package/dist/src/private-to-private/util.d.ts.map +1 -1
- package/dist/src/private-to-private/util.js +14 -26
- package/dist/src/private-to-private/util.js.map +1 -1
- package/dist/src/private-to-public/listener.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.js +15 -21
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.d.ts +8 -0
- package/dist/src/private-to-public/transport.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.js +2 -3
- package/dist/src/private-to-public/transport.js.map +1 -1
- package/dist/src/private-to-public/utils/connect.d.ts +1 -1
- package/dist/src/private-to-public/utils/connect.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/connect.js +14 -17
- package/dist/src/private-to-public/utils/connect.js.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +4 -4
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +2 -13
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
- package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/sdp.js +13 -25
- package/dist/src/private-to-public/utils/sdp.js.map +1 -1
- package/dist/src/private-to-public/utils/stun-listener.js +1 -1
- package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
- package/dist/src/stream.d.ts +26 -14
- package/dist/src/stream.d.ts.map +1 -1
- package/dist/src/stream.js +204 -134
- package/dist/src/stream.js.map +1 -1
- package/dist/src/util.d.ts +1 -3
- package/dist/src/util.d.ts.map +1 -1
- package/dist/src/util.js +0 -19
- package/dist/src/util.js.map +1 -1
- package/dist/src/webrtc/index.d.ts +1 -1
- package/dist/src/webrtc/index.d.ts.map +1 -1
- package/dist/src/webrtc/index.js +1 -1
- package/dist/src/webrtc/index.js.map +1 -1
- package/package.json +29 -26
- package/src/constants.ts +28 -5
- package/src/index.ts +22 -21
- package/src/maconn.ts +101 -0
- package/src/muxer.ts +169 -39
- package/src/private-to-private/initiate-connection.ts +8 -46
- package/src/private-to-private/signaling-stream-handler.ts +10 -23
- package/src/private-to-private/transport.ts +25 -33
- package/src/private-to-private/util.ts +16 -33
- package/src/private-to-public/listener.ts +15 -22
- package/src/private-to-public/transport.ts +12 -3
- package/src/private-to-public/utils/connect.ts +15 -18
- package/src/private-to-public/utils/get-rtcpeerconnection.ts +4 -16
- package/src/private-to-public/utils/sdp.ts +13 -29
- package/src/private-to-public/utils/stun-listener.ts +1 -1
- package/src/stream.ts +237 -153
- package/src/util.ts +1 -22
- package/src/webrtc/index.ts +1 -1
- package/dist/src/rtcpeerconnection-to-conn.d.ts +0 -12
- package/dist/src/rtcpeerconnection-to-conn.d.ts.map +0 -1
- package/dist/src/rtcpeerconnection-to-conn.js +0 -46
- package/dist/src/rtcpeerconnection-to-conn.js.map +0 -1
- package/src/rtcpeerconnection-to-conn.ts +0 -66
@@ -1,10 +1,10 @@
|
|
1
|
+
import { PeerConnection } from '@ipshipyard/node-datachannel'
|
2
|
+
import { RTCPeerConnection } from '@ipshipyard/node-datachannel/polyfill'
|
1
3
|
import { Crypto } from '@peculiar/webcrypto'
|
2
|
-
import { PeerConnection } from 'node-datachannel'
|
3
|
-
import { RTCPeerConnection } from 'node-datachannel/polyfill'
|
4
4
|
import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.js'
|
5
5
|
import { generateTransportCertificate } from './generate-certificates.js'
|
6
6
|
import type { TransportCertificate } from '../../index.js'
|
7
|
-
import type { CertificateFingerprint } from 'node-datachannel'
|
7
|
+
import type { CertificateFingerprint } from '@ipshipyard/node-datachannel'
|
8
8
|
|
9
9
|
const crypto = new Crypto()
|
10
10
|
|
@@ -14,7 +14,7 @@ interface DirectRTCPeerConnectionInit extends RTCConfiguration {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
export class DirectRTCPeerConnection extends RTCPeerConnection {
|
17
|
-
private peerConnection: PeerConnection
|
17
|
+
private readonly peerConnection: PeerConnection
|
18
18
|
private readonly ufrag: string
|
19
19
|
|
20
20
|
constructor (init: DirectRTCPeerConnectionInit) {
|
@@ -22,18 +22,6 @@ export class DirectRTCPeerConnection extends RTCPeerConnection {
|
|
22
22
|
|
23
23
|
this.peerConnection = init.peerConnection
|
24
24
|
this.ufrag = init.ufrag
|
25
|
-
|
26
|
-
// make sure C++ peer connection is garbage collected
|
27
|
-
// https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
|
28
|
-
this.addEventListener('connectionstatechange', () => {
|
29
|
-
switch (this.connectionState) {
|
30
|
-
case 'closed':
|
31
|
-
this.peerConnection.close()
|
32
|
-
break
|
33
|
-
default:
|
34
|
-
break
|
35
|
-
}
|
36
|
-
})
|
37
25
|
}
|
38
26
|
|
39
27
|
async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
|
@@ -1,11 +1,10 @@
|
|
1
1
|
import { InvalidParametersError } from '@libp2p/interface'
|
2
|
-
import {
|
3
|
-
import { CODE_CERTHASH, multiaddr } from '@multiformats/multiaddr'
|
2
|
+
import { multiaddr } from '@multiformats/multiaddr'
|
4
3
|
import { base64url } from 'multiformats/bases/base64'
|
5
4
|
import { bases, digest } from 'multiformats/basics'
|
6
5
|
import * as Digest from 'multiformats/hashes/digest'
|
7
6
|
import { sha256 } from 'multiformats/hashes/sha2'
|
8
|
-
import { MAX_MESSAGE_SIZE } from '../../constants.js'
|
7
|
+
import { CODEC_CERTHASH, MAX_MESSAGE_SIZE } from '../../constants.js'
|
9
8
|
import { InvalidFingerprintError, UnsupportedHashAlgorithmError } from '../../error.js'
|
10
9
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
11
10
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
@@ -28,8 +27,8 @@ export function getFingerprintFromSdp (sdp: string | undefined): string | undefi
|
|
28
27
|
|
29
28
|
// Extract the certhash from a multiaddr
|
30
29
|
export function certhash (ma: Multiaddr): string {
|
31
|
-
const
|
32
|
-
const certhash =
|
30
|
+
const tups = ma.stringTuples()
|
31
|
+
const certhash = tups.filter((tup) => tup[0] === CODEC_CERTHASH).map((tup) => tup[1])[0]
|
33
32
|
|
34
33
|
if (certhash === undefined || certhash === '') {
|
35
34
|
throw new InvalidParametersError(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`)
|
@@ -101,20 +100,15 @@ export function toSupportedHashFunction (code: number): 'sha-1' | 'sha-256' | 's
|
|
101
100
|
* ice-lite mode and DTLS active mode.
|
102
101
|
*/
|
103
102
|
export function serverAnswerFromMultiaddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
|
104
|
-
const { host, port,
|
105
|
-
|
106
|
-
if (type !== 'ip4' && type !== 'ip6') {
|
107
|
-
throw new InvalidParametersError(`Multiaddr ${ma} was not an IPv4 or IPv6 address`)
|
108
|
-
}
|
109
|
-
|
103
|
+
const { host, port, family } = ma.toOptions()
|
110
104
|
const fingerprint = ma2Fingerprint(ma)
|
111
105
|
const sdp = `v=0
|
112
|
-
o=- 0 0 IN IP${
|
106
|
+
o=- 0 0 IN IP${family} ${host}
|
113
107
|
s=-
|
114
108
|
t=0 0
|
115
109
|
a=ice-lite
|
116
110
|
m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
|
117
|
-
c=IN IP${
|
111
|
+
c=IN IP${family} ${host}
|
118
112
|
a=mid:0
|
119
113
|
a=ice-options:ice2
|
120
114
|
a=ice-ufrag:${ufrag}
|
@@ -137,16 +131,11 @@ a=end-of-candidates
|
|
137
131
|
* Create an offer SDP message from a multiaddr
|
138
132
|
*/
|
139
133
|
export function clientOfferFromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
|
140
|
-
const { host, port,
|
141
|
-
|
142
|
-
if (type !== 'ip4' && type !== 'ip6') {
|
143
|
-
throw new InvalidParametersError(`Multiaddr ${ma} was not an IPv4 or IPv6 address`)
|
144
|
-
}
|
145
|
-
|
134
|
+
const { host, port, family } = ma.toOptions()
|
146
135
|
const sdp = `v=0
|
147
|
-
o=- 0 0 IN IP${
|
136
|
+
o=- 0 0 IN IP${family} ${host}
|
148
137
|
s=-
|
149
|
-
c=IN IP${
|
138
|
+
c=IN IP${family} ${host}
|
150
139
|
t=0 0
|
151
140
|
a=ice-options:ice2,trickle
|
152
141
|
m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
|
@@ -177,13 +166,8 @@ export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessi
|
|
177
166
|
|
178
167
|
const lineBreak = desc.sdp.includes('\r\n') ? '\r\n' : '\n'
|
179
168
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
.replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + lineBreak)
|
184
|
-
} catch {
|
185
|
-
// fails under Node.js
|
186
|
-
}
|
187
|
-
|
169
|
+
desc.sdp = desc.sdp
|
170
|
+
.replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + lineBreak)
|
171
|
+
.replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + lineBreak)
|
188
172
|
return desc
|
189
173
|
}
|
package/src/stream.ts
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
import {
|
2
|
-
import { AbstractStream } from '@libp2p/utils'
|
1
|
+
import { StreamStateError, TimeoutError } from '@libp2p/interface'
|
2
|
+
import { AbstractStream } from '@libp2p/utils/abstract-stream'
|
3
|
+
import { anySignal } from 'any-signal'
|
3
4
|
import * as lengthPrefixed from 'it-length-prefixed'
|
4
5
|
import { pushable } from 'it-pushable'
|
5
|
-
import
|
6
|
+
import pDefer from 'p-defer'
|
7
|
+
import pTimeout from 'p-timeout'
|
8
|
+
import { raceEvent } from 'race-event'
|
6
9
|
import { raceSignal } from 'race-signal'
|
7
10
|
import { Uint8ArrayList } from 'uint8arraylist'
|
8
|
-
import {
|
11
|
+
import { BUFFERED_AMOUNT_LOW_TIMEOUT, FIN_ACK_TIMEOUT, MAX_BUFFERED_AMOUNT, MAX_MESSAGE_SIZE, OPEN_TIMEOUT, PROTOBUF_OVERHEAD } from './constants.js'
|
9
12
|
import { Message } from './private-to-public/pb/message.js'
|
10
|
-
import { isFirefox } from './util.js'
|
11
13
|
import type { DataChannelOptions } from './index.js'
|
12
|
-
import type { AbortOptions,
|
13
|
-
import type { AbstractStreamInit
|
14
|
+
import type { AbortOptions, Direction, Logger } from '@libp2p/interface'
|
15
|
+
import type { AbstractStreamInit } from '@libp2p/utils/abstract-stream'
|
14
16
|
import type { Pushable } from 'it-pushable'
|
17
|
+
import type { DeferredPromise } from 'p-defer'
|
15
18
|
|
16
19
|
export interface WebRTCStreamInit extends AbstractStreamInit, DataChannelOptions {
|
17
20
|
/**
|
@@ -36,40 +39,124 @@ export class WebRTCStream extends AbstractStream {
|
|
36
39
|
* and then the protobuf decoder.
|
37
40
|
*/
|
38
41
|
private readonly incomingData: Pushable<Uint8Array>
|
42
|
+
|
39
43
|
private readonly maxBufferedAmount: number
|
40
|
-
|
41
|
-
private
|
44
|
+
|
45
|
+
private readonly bufferedAmountLowEventTimeout: number
|
46
|
+
|
47
|
+
/**
|
48
|
+
* The maximum size of a message in bytes
|
49
|
+
*/
|
50
|
+
private readonly maxMessageSize: number
|
51
|
+
|
52
|
+
/**
|
53
|
+
* When this promise is resolved, the remote has sent us a FIN flag
|
54
|
+
*/
|
55
|
+
private readonly receiveFinAck: DeferredPromise<void>
|
56
|
+
private readonly finAckTimeout: number
|
57
|
+
private readonly openTimeout: number
|
58
|
+
private readonly closeController: AbortController
|
42
59
|
|
43
60
|
constructor (init: WebRTCStreamInit) {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
61
|
+
// override onEnd to send/receive FIN_ACK before closing the stream
|
62
|
+
const originalOnEnd = init.onEnd
|
63
|
+
init.onEnd = (err?: Error): void => {
|
64
|
+
this.log.trace('readable and writeable ends closed with status "%s"', this.status)
|
65
|
+
|
66
|
+
void Promise.resolve(async () => {
|
67
|
+
if (this.timeline.abort != null || this.timeline.reset !== null) {
|
68
|
+
return
|
69
|
+
}
|
70
|
+
|
71
|
+
// wait for FIN_ACK if we haven't received it already
|
72
|
+
try {
|
73
|
+
await pTimeout(this.receiveFinAck.promise, {
|
74
|
+
milliseconds: this.finAckTimeout
|
75
|
+
})
|
76
|
+
} catch (err) {
|
77
|
+
this.log.error('error receiving FIN_ACK', err)
|
78
|
+
}
|
79
|
+
})
|
80
|
+
.then(() => {
|
81
|
+
// stop processing incoming messages
|
82
|
+
this.incomingData.end()
|
83
|
+
|
84
|
+
// final cleanup
|
85
|
+
originalOnEnd?.(err)
|
86
|
+
})
|
87
|
+
.catch(err => {
|
88
|
+
this.log.error('error ending stream', err)
|
89
|
+
})
|
90
|
+
.finally(() => {
|
91
|
+
this.channel.close()
|
92
|
+
})
|
93
|
+
}
|
94
|
+
|
95
|
+
super(init)
|
48
96
|
|
49
97
|
this.channel = init.channel
|
50
98
|
this.channel.binaryType = 'arraybuffer'
|
51
99
|
this.incomingData = pushable<Uint8Array>()
|
100
|
+
this.bufferedAmountLowEventTimeout = init.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT
|
52
101
|
this.maxBufferedAmount = init.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT
|
53
|
-
this.
|
102
|
+
this.maxMessageSize = (init.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD
|
103
|
+
this.receiveFinAck = pDefer()
|
104
|
+
this.finAckTimeout = init.closeTimeout ?? FIN_ACK_TIMEOUT
|
105
|
+
this.openTimeout = init.openTimeout ?? OPEN_TIMEOUT
|
106
|
+
this.closeController = new AbortController()
|
107
|
+
|
108
|
+
// set up initial state
|
109
|
+
switch (this.channel.readyState) {
|
110
|
+
case 'open':
|
111
|
+
this.timeline.open = new Date().getTime()
|
112
|
+
break
|
113
|
+
|
114
|
+
case 'closed':
|
115
|
+
case 'closing':
|
116
|
+
if (this.timeline.close === undefined || this.timeline.close === 0) {
|
117
|
+
this.timeline.close = Date.now()
|
118
|
+
}
|
119
|
+
break
|
120
|
+
case 'connecting':
|
121
|
+
// noop
|
122
|
+
break
|
123
|
+
|
124
|
+
default:
|
125
|
+
this.log.error('unknown datachannel state %s', this.channel.readyState)
|
126
|
+
throw new StreamStateError('Unknown datachannel state')
|
127
|
+
}
|
54
128
|
|
55
129
|
// handle RTCDataChannel events
|
56
|
-
this.channel.
|
57
|
-
this.
|
130
|
+
this.channel.onopen = (_evt) => {
|
131
|
+
this.timeline.open = new Date().getTime()
|
132
|
+
}
|
133
|
+
|
134
|
+
this.channel.onclose = (_evt) => {
|
135
|
+
this.log.trace('received onclose event')
|
58
136
|
|
59
|
-
|
60
|
-
this.
|
137
|
+
// stop any in-progress writes
|
138
|
+
this.closeController.abort()
|
139
|
+
|
140
|
+
// if the channel has closed we'll never receive a FIN_ACK so resolve the
|
141
|
+
// promise so we don't try to wait later
|
142
|
+
this.receiveFinAck.resolve()
|
143
|
+
|
144
|
+
void this.close().catch(err => {
|
145
|
+
this.log.error('error closing stream after channel closed', err)
|
146
|
+
})
|
61
147
|
}
|
62
148
|
|
63
149
|
this.channel.onerror = (evt) => {
|
64
|
-
|
150
|
+
this.log.trace('received onerror event')
|
65
151
|
|
66
|
-
|
152
|
+
// stop any in-progress writes
|
153
|
+
this.closeController.abort()
|
67
154
|
|
155
|
+
const err = (evt as RTCErrorEvent).error
|
68
156
|
this.abort(err)
|
69
157
|
}
|
70
158
|
|
71
159
|
this.channel.onmessage = async (event: MessageEvent<ArrayBuffer>) => {
|
72
|
-
this.log('incoming message %d bytes', event.data.byteLength)
|
73
160
|
const { data } = event
|
74
161
|
|
75
162
|
if (data === null || data.byteLength === 0) {
|
@@ -79,186 +166,187 @@ export class WebRTCStream extends AbstractStream {
|
|
79
166
|
this.incomingData.push(new Uint8Array(data, 0, data.byteLength))
|
80
167
|
}
|
81
168
|
|
82
|
-
|
83
|
-
this.channel.bufferedAmountLowThreshold = 0
|
84
|
-
|
85
|
-
this.channel.onbufferedamountlow = () => {
|
86
|
-
if (this.writableNeedsDrain) {
|
87
|
-
this.safeDispatchEvent('drain')
|
88
|
-
}
|
89
|
-
}
|
90
|
-
|
91
|
-
if (this.channel.readyState !== 'open') {
|
92
|
-
this.log('channel ready state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState)
|
93
|
-
pEvent(this.channel, 'open', {
|
94
|
-
rejectionEvents: [
|
95
|
-
'close',
|
96
|
-
'error'
|
97
|
-
]
|
98
|
-
})
|
99
|
-
.then(() => {
|
100
|
-
this.log('channel ready state is now "%s", dispatching drain', this.channel.readyState)
|
101
|
-
this.safeDispatchEvent('drain')
|
102
|
-
})
|
103
|
-
.catch(err => {
|
104
|
-
this.abort(err.error ?? err)
|
105
|
-
})
|
106
|
-
}
|
169
|
+
const self = this
|
107
170
|
|
108
171
|
// pipe framed protobuf messages through a length prefixed decoder, and
|
109
172
|
// surface data from the `Message.message` field through a source.
|
110
173
|
Promise.resolve().then(async () => {
|
111
174
|
for await (const buf of lengthPrefixed.decode(this.incomingData)) {
|
112
|
-
|
175
|
+
const message = self.processIncomingProtobuf(buf)
|
176
|
+
|
177
|
+
if (message != null) {
|
178
|
+
self.sourcePush(new Uint8ArrayList(message))
|
179
|
+
}
|
113
180
|
}
|
114
181
|
})
|
115
182
|
.catch(err => {
|
116
183
|
this.log.error('error processing incoming data channel messages', err)
|
117
184
|
})
|
118
|
-
|
119
|
-
// close when both writable ends are closed or an error occurs
|
120
|
-
const cleanUpDatachannelOnClose = (): void => {
|
121
|
-
if (this.channel.readyState === 'open') {
|
122
|
-
this.log.trace('stream closed, closing underlying datachannel')
|
123
|
-
this.channel.close()
|
124
|
-
}
|
125
|
-
}
|
126
|
-
this.addEventListener('close', cleanUpDatachannelOnClose)
|
127
185
|
}
|
128
186
|
|
129
187
|
sendNewStream (): void {
|
130
188
|
// opening new streams is handled by WebRTC so this is a noop
|
131
189
|
}
|
132
190
|
|
133
|
-
_sendMessage (data: Uint8ArrayList): void {
|
134
|
-
if (this.channel.readyState
|
191
|
+
async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise<void> {
|
192
|
+
if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') {
|
135
193
|
throw new StreamStateError(`Invalid datachannel state - ${this.channel.readyState}`)
|
136
194
|
}
|
137
195
|
|
138
|
-
this.
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
196
|
+
if (this.channel.readyState !== 'open') {
|
197
|
+
const timeout = AbortSignal.timeout(this.openTimeout)
|
198
|
+
const signal = anySignal([
|
199
|
+
this.closeController.signal,
|
200
|
+
timeout
|
201
|
+
])
|
202
|
+
|
203
|
+
try {
|
204
|
+
this.log('channel state is "%s" and not "open", waiting for "open" event before sending data', this.channel.readyState)
|
205
|
+
await raceEvent(this.channel, 'open', signal)
|
206
|
+
} finally {
|
207
|
+
signal.clear()
|
208
|
+
}
|
147
209
|
|
148
|
-
|
149
|
-
for (const buf of data) {
|
150
|
-
this.channel.send(buf)
|
210
|
+
this.log('channel state is now "%s", sending data', this.channel.readyState)
|
151
211
|
}
|
152
|
-
}
|
153
212
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
213
|
+
if (checkBuffer && this.channel.bufferedAmount > this.maxBufferedAmount) {
|
214
|
+
const timeout = AbortSignal.timeout(this.bufferedAmountLowEventTimeout)
|
215
|
+
const signal = anySignal([
|
216
|
+
this.closeController.signal,
|
217
|
+
timeout
|
218
|
+
])
|
219
|
+
|
220
|
+
try {
|
221
|
+
this.log('channel buffer is %d, wait for "bufferedamountlow" event', this.channel.bufferedAmount)
|
222
|
+
await raceEvent(this.channel, 'bufferedamountlow', signal)
|
223
|
+
} catch (err: any) {
|
224
|
+
if (timeout.aborted) {
|
225
|
+
throw new TimeoutError(`Timed out waiting for DataChannel buffer to clear after ${this.bufferedAmountLowEventTimeout}ms`)
|
226
|
+
}
|
227
|
+
|
228
|
+
throw err
|
229
|
+
} finally {
|
230
|
+
signal.clear()
|
159
231
|
}
|
160
232
|
}
|
161
233
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
lengthPrefixed.encode.single(Message.encode({
|
169
|
-
message: data.subarray()
|
170
|
-
}))
|
171
|
-
)
|
172
|
-
|
173
|
-
/*
|
174
|
-
// TODO: enable this when FF and rust-libp2p are not broken
|
175
|
-
// send message without copying data
|
176
|
-
for (const message of data) {
|
177
|
-
this._sendMessage(
|
178
|
-
lengthPrefixed.encode.single(Message.encode({
|
179
|
-
message
|
180
|
-
}))
|
181
|
-
)
|
182
|
-
}
|
234
|
+
try {
|
235
|
+
this.log.trace('sending message, channel state "%s"', this.channel.readyState)
|
236
|
+
// send message without copying data
|
237
|
+
this.channel.send(data.subarray())
|
238
|
+
} catch (err: any) {
|
239
|
+
this.log.error('error while sending message', err)
|
183
240
|
}
|
184
|
-
|
241
|
+
}
|
185
242
|
|
186
|
-
|
187
|
-
|
188
|
-
|
243
|
+
async sendData (data: Uint8ArrayList): Promise<void> {
|
244
|
+
const bytesTotal = data.byteLength
|
245
|
+
// sending messages is an async operation so use a copy of the list as it
|
246
|
+
// may be changed beneath us
|
247
|
+
data = data.sublist()
|
248
|
+
|
249
|
+
while (data.byteLength > 0) {
|
250
|
+
const toSend = Math.min(data.byteLength, this.maxMessageSize)
|
251
|
+
const buf = data.subarray(0, toSend)
|
252
|
+
const messageBuf = Message.encode({ message: buf })
|
253
|
+
const sendBuf = lengthPrefixed.encode.single(messageBuf)
|
254
|
+
this.log.trace('sending %d/%d bytes on channel', buf.byteLength, bytesTotal)
|
255
|
+
await this._sendMessage(sendBuf)
|
256
|
+
|
257
|
+
data.consume(toSend)
|
189
258
|
}
|
259
|
+
|
260
|
+
this.log.trace('finished sending data, channel state "%s"', this.channel.readyState)
|
190
261
|
}
|
191
262
|
|
192
|
-
sendReset (
|
263
|
+
async sendReset (): Promise<void> {
|
193
264
|
try {
|
194
|
-
this.
|
195
|
-
this._sendFlag(Message.Flag.RESET)
|
196
|
-
this.receivedFinAck?.reject(err)
|
265
|
+
await this._sendFlag(Message.Flag.RESET)
|
197
266
|
} catch (err) {
|
198
267
|
this.log.error('failed to send reset - %e', err)
|
268
|
+
} finally {
|
269
|
+
this.channel.close()
|
199
270
|
}
|
200
271
|
}
|
201
272
|
|
202
|
-
async sendCloseWrite (options
|
203
|
-
this.
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
273
|
+
async sendCloseWrite (options: AbortOptions): Promise<void> {
|
274
|
+
if (this.channel.readyState !== 'open') {
|
275
|
+
this.receiveFinAck.resolve()
|
276
|
+
return
|
277
|
+
}
|
278
|
+
|
279
|
+
const sent = await this._sendFlag(Message.Flag.FIN)
|
280
|
+
|
281
|
+
if (sent) {
|
282
|
+
this.log.trace('awaiting FIN_ACK')
|
283
|
+
try {
|
284
|
+
await raceSignal(this.receiveFinAck.promise, options?.signal, {
|
285
|
+
errorMessage: 'sending close-write was aborted before FIN_ACK was received',
|
286
|
+
errorName: 'FinAckNotReceivedError'
|
287
|
+
})
|
288
|
+
} catch (err) {
|
289
|
+
this.log.error('failed to await FIN_ACK', err)
|
290
|
+
}
|
291
|
+
} else {
|
292
|
+
this.log.trace('sending FIN failed, not awaiting FIN_ACK')
|
293
|
+
}
|
294
|
+
|
295
|
+
// if we've attempted to receive a FIN_ACK, do not try again
|
296
|
+
this.receiveFinAck.resolve()
|
216
297
|
}
|
217
298
|
|
218
|
-
async sendCloseRead (
|
219
|
-
this.
|
220
|
-
|
299
|
+
async sendCloseRead (): Promise<void> {
|
300
|
+
if (this.channel.readyState !== 'open') {
|
301
|
+
return
|
302
|
+
}
|
303
|
+
|
304
|
+
await this._sendFlag(Message.Flag.STOP_SENDING)
|
221
305
|
}
|
222
306
|
|
223
307
|
/**
|
224
308
|
* Handle incoming
|
225
309
|
*/
|
226
|
-
private processIncomingProtobuf (buffer: Uint8ArrayList):
|
310
|
+
private processIncomingProtobuf (buffer: Uint8ArrayList): Uint8Array | undefined {
|
227
311
|
const message = Message.decode(buffer)
|
228
312
|
|
229
|
-
// ignore data messages if we've closed the readable end already
|
230
|
-
if (message.message != null && (this.readStatus === 'readable' || this.readStatus === 'paused')) {
|
231
|
-
this.onData(new Uint8ArrayList(message.message))
|
232
|
-
}
|
233
|
-
|
234
313
|
if (message.flag !== undefined) {
|
235
314
|
this.log.trace('incoming flag %s, write status "%s", read status "%s"', message.flag, this.writeStatus, this.readStatus)
|
236
315
|
|
237
316
|
if (message.flag === Message.Flag.FIN) {
|
238
|
-
//
|
239
|
-
this.
|
240
|
-
|
317
|
+
// We should expect no more data from the remote, stop reading
|
318
|
+
this.remoteCloseWrite()
|
319
|
+
|
320
|
+
this.log.trace('sending FIN_ACK')
|
321
|
+
void this._sendFlag(Message.Flag.FIN_ACK)
|
322
|
+
.catch(err => {
|
323
|
+
this.log.error('error sending FIN_ACK immediately', err)
|
324
|
+
})
|
241
325
|
}
|
242
326
|
|
243
327
|
if (message.flag === Message.Flag.RESET) {
|
244
|
-
//
|
245
|
-
this.
|
246
|
-
this.onRemoteReset()
|
328
|
+
// Stop reading and writing to the stream immediately
|
329
|
+
this.reset()
|
247
330
|
}
|
248
331
|
|
249
332
|
if (message.flag === Message.Flag.STOP_SENDING) {
|
250
|
-
//
|
251
|
-
this.
|
333
|
+
// The remote has stopped reading
|
334
|
+
this.remoteCloseRead()
|
252
335
|
}
|
253
336
|
|
254
337
|
if (message.flag === Message.Flag.FIN_ACK) {
|
255
|
-
|
256
|
-
this.
|
338
|
+
this.log.trace('received FIN_ACK')
|
339
|
+
this.receiveFinAck.resolve()
|
257
340
|
}
|
258
341
|
}
|
342
|
+
|
343
|
+
// ignore data messages if we've closed the readable end already
|
344
|
+
if (this.readStatus === 'ready') {
|
345
|
+
return message.message
|
346
|
+
}
|
259
347
|
}
|
260
348
|
|
261
|
-
private _sendFlag (flag: Message.Flag): boolean {
|
349
|
+
private async _sendFlag (flag: Message.Flag): Promise<boolean> {
|
262
350
|
if (this.channel.readyState !== 'open') {
|
263
351
|
// flags can be sent while we or the remote are closing the datachannel so
|
264
352
|
// if the channel isn't open, don't try to send it but return false to let
|
@@ -272,7 +360,7 @@ export class WebRTCStream extends AbstractStream {
|
|
272
360
|
const prefixedBuf = lengthPrefixed.encode.single(messageBuf)
|
273
361
|
|
274
362
|
try {
|
275
|
-
this._sendMessage(prefixedBuf)
|
363
|
+
await this._sendMessage(prefixedBuf, false)
|
276
364
|
|
277
365
|
return true
|
278
366
|
} catch (err: any) {
|
@@ -281,14 +369,6 @@ export class WebRTCStream extends AbstractStream {
|
|
281
369
|
|
282
370
|
return false
|
283
371
|
}
|
284
|
-
|
285
|
-
sendPause (): void {
|
286
|
-
// TODO: read backpressure?
|
287
|
-
}
|
288
|
-
|
289
|
-
sendResume (): void {
|
290
|
-
// TODO: read backpressure?
|
291
|
-
}
|
292
372
|
}
|
293
373
|
|
294
374
|
export interface WebRTCStreamOptions extends DataChannelOptions {
|
@@ -303,7 +383,12 @@ export interface WebRTCStreamOptions extends DataChannelOptions {
|
|
303
383
|
/**
|
304
384
|
* The stream direction
|
305
385
|
*/
|
306
|
-
direction:
|
386
|
+
direction: Direction
|
387
|
+
|
388
|
+
/**
|
389
|
+
* A callback invoked when the channel ends
|
390
|
+
*/
|
391
|
+
onEnd?(err?: Error | undefined): void
|
307
392
|
|
308
393
|
/**
|
309
394
|
* The logger to create a scope from
|
@@ -314,16 +399,15 @@ export interface WebRTCStreamOptions extends DataChannelOptions {
|
|
314
399
|
* If true the underlying datachannel is being used to perform the noise
|
315
400
|
* handshake during connection establishment
|
316
401
|
*/
|
317
|
-
|
402
|
+
handshake?: boolean
|
318
403
|
}
|
319
404
|
|
320
405
|
export function createStream (options: WebRTCStreamOptions): WebRTCStream {
|
321
|
-
const { channel, direction,
|
406
|
+
const { channel, direction, handshake } = options
|
322
407
|
|
323
408
|
return new WebRTCStream({
|
324
409
|
...options,
|
325
410
|
id: `${channel.id}`,
|
326
|
-
log: options.log.newScope(`${
|
327
|
-
protocol: ''
|
411
|
+
log: options.log.newScope(`${handshake === true ? 'handshake' : direction}:${channel.id}`)
|
328
412
|
})
|
329
413
|
}
|