@libp2p/webrtc 0.0.0
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/LICENSE +4 -0
- package/README.md +210 -0
- package/dist/index.min.js +54 -0
- package/dist/index.min.js.map +7 -0
- package/dist/proto_ts/message.d.ts +56 -0
- package/dist/proto_ts/message.d.ts.map +1 -0
- package/dist/proto_ts/message.js +86 -0
- package/dist/proto_ts/message.js.map +1 -0
- package/dist/src/error.d.ts +54 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +105 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/maconn.d.ts +43 -0
- package/dist/src/maconn.d.ts.map +1 -0
- package/dist/src/maconn.js +29 -0
- package/dist/src/maconn.js.map +1 -0
- package/dist/src/muxer.d.ts +55 -0
- package/dist/src/muxer.d.ts.map +1 -0
- package/dist/src/muxer.js +92 -0
- package/dist/src/muxer.js.map +1 -0
- package/dist/src/options.d.ts +6 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +2 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/sdp.d.ts +33 -0
- package/dist/src/sdp.d.ts.map +1 -0
- package/dist/src/sdp.js +118 -0
- package/dist/src/sdp.js.map +1 -0
- package/dist/src/stream.d.ts +141 -0
- package/dist/src/stream.d.ts.map +1 -0
- package/dist/src/stream.js +290 -0
- package/dist/src/stream.js.map +1 -0
- package/dist/src/transport.d.ts +60 -0
- package/dist/src/transport.d.ts.map +1 -0
- package/dist/src/transport.js +193 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/util.d.ts +5 -0
- package/dist/src/util.d.ts.map +1 -0
- package/dist/src/util.js +5 -0
- package/dist/src/util.js.map +1 -0
- package/dist/stats.json +1 -0
- package/package.json +170 -0
- package/proto_ts/message.ts +105 -0
- package/src/error.ts +123 -0
- package/src/index.ts +6 -0
- package/src/maconn.ts +67 -0
- package/src/message.proto +22 -0
- package/src/muxer.ts +120 -0
- package/src/options.ts +4 -0
- package/src/sdp.ts +136 -0
- package/src/stream.ts +422 -0
- package/src/transport.ts +243 -0
- package/src/util.ts +5 -0
package/src/sdp.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { logger } from '@libp2p/logger'
|
|
2
|
+
import { Multiaddr } from '@multiformats/multiaddr'
|
|
3
|
+
import { bases } from 'multiformats/basics'
|
|
4
|
+
import * as multihashes from 'multihashes'
|
|
5
|
+
|
|
6
|
+
import { inappropriateMultiaddr, invalidArgument, invalidFingerprint, unsupportedHashAlgorithm } from './error.js'
|
|
7
|
+
import { CERTHASH_CODE } from './transport.js'
|
|
8
|
+
|
|
9
|
+
const log = logger('libp2p:webrtc:sdp')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get base2 | identity decoders
|
|
13
|
+
*/
|
|
14
|
+
export const mbdecoder: any = (function () {
|
|
15
|
+
const decoders = Object.values(bases).map((b) => b.decoder)
|
|
16
|
+
let acc = decoders[0].or(decoders[1])
|
|
17
|
+
decoders.slice(2).forEach((d) => (acc = acc.or(d)))
|
|
18
|
+
return acc
|
|
19
|
+
})()
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get base2 | identity decoders
|
|
23
|
+
*/
|
|
24
|
+
function ipv (ma: Multiaddr): string {
|
|
25
|
+
for (const proto of ma.protoNames()) {
|
|
26
|
+
if (proto.startsWith('ip')) {
|
|
27
|
+
return proto.toUpperCase()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
log('Warning: multiaddr does not appear to contain IP4 or IP6, defaulting to IP6', ma)
|
|
32
|
+
|
|
33
|
+
return 'IP6'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Extract the certhash from a multiaddr
|
|
37
|
+
export function certhash (ma: Multiaddr): string {
|
|
38
|
+
const tups = ma.stringTuples()
|
|
39
|
+
const certhash = tups.filter((tup) => tup[0] === CERTHASH_CODE).map((tup) => tup[1])[0]
|
|
40
|
+
|
|
41
|
+
if (certhash === undefined || certhash === '') {
|
|
42
|
+
throw inappropriateMultiaddr(`Couldn't find a certhash component of multiaddr: ${ma.toString()}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return certhash
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a certhash into a multihash
|
|
50
|
+
*/
|
|
51
|
+
export function decodeCerthash (certhash: string) {
|
|
52
|
+
const mbdecoded = mbdecoder.decode(certhash)
|
|
53
|
+
return multihashes.decode(mbdecoded)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract the fingerprint from a multiaddr
|
|
58
|
+
*/
|
|
59
|
+
export function ma2Fingerprint (ma: Multiaddr): string[] {
|
|
60
|
+
// certhash_value is a multibase encoded multihash encoded string
|
|
61
|
+
const mhdecoded = decodeCerthash(certhash(ma))
|
|
62
|
+
const prefix = toSupportedHashFunction(mhdecoded.name)
|
|
63
|
+
const fingerprint = mhdecoded.digest.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
|
|
64
|
+
const sdp = fingerprint.match(/.{1,2}/g)
|
|
65
|
+
|
|
66
|
+
if (sdp == null) {
|
|
67
|
+
throw invalidFingerprint(fingerprint, ma.toString())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [`${prefix.toUpperCase()} ${sdp.join(':').toUpperCase()}`, fingerprint]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalize the hash name from a given multihash has name
|
|
75
|
+
*/
|
|
76
|
+
export function toSupportedHashFunction (name: multihashes.HashName): string {
|
|
77
|
+
switch (name) {
|
|
78
|
+
case 'sha1':
|
|
79
|
+
return 'sha-1'
|
|
80
|
+
case 'sha2-256':
|
|
81
|
+
return 'sha-256'
|
|
82
|
+
case 'sha2-512':
|
|
83
|
+
return 'sha-512'
|
|
84
|
+
default:
|
|
85
|
+
throw unsupportedHashAlgorithm(name)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert a multiaddr into a SDP
|
|
91
|
+
*/
|
|
92
|
+
function ma2sdp (ma: Multiaddr, ufrag: string): string {
|
|
93
|
+
const { host, port } = ma.toOptions()
|
|
94
|
+
const ipVersion = ipv(ma)
|
|
95
|
+
const [CERTFP] = ma2Fingerprint(ma)
|
|
96
|
+
|
|
97
|
+
return `v=0
|
|
98
|
+
o=- 0 0 IN ${ipVersion} ${host}
|
|
99
|
+
s=-
|
|
100
|
+
c=IN ${ipVersion} ${host}
|
|
101
|
+
t=0 0
|
|
102
|
+
a=ice-lite
|
|
103
|
+
m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
|
|
104
|
+
a=mid:0
|
|
105
|
+
a=setup:passive
|
|
106
|
+
a=ice-ufrag:${ufrag}
|
|
107
|
+
a=ice-pwd:${ufrag}
|
|
108
|
+
a=fingerprint:${CERTFP}
|
|
109
|
+
a=sctp-port:5000
|
|
110
|
+
a=max-message-size:100000
|
|
111
|
+
a=candidate:1467250027 1 UDP 1467250027 ${host} ${port} typ host\r\n`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create an answer SDP from a multiaddr
|
|
116
|
+
*/
|
|
117
|
+
export function fromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
|
|
118
|
+
return {
|
|
119
|
+
type: 'answer',
|
|
120
|
+
sdp: ma2sdp(ma, ufrag)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Replace (munge) the ufrag and password values in a SDP
|
|
126
|
+
*/
|
|
127
|
+
export function munge (desc: RTCSessionDescriptionInit, ufrag: string): RTCSessionDescriptionInit {
|
|
128
|
+
if (desc.sdp === undefined) {
|
|
129
|
+
throw invalidArgument("Can't munge a missing SDP")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
desc.sdp = desc.sdp
|
|
133
|
+
.replace(/\na=ice-ufrag:[^\n]*\n/, '\na=ice-ufrag:' + ufrag + '\n')
|
|
134
|
+
.replace(/\na=ice-pwd:[^\n]*\n/, '\na=ice-pwd:' + ufrag + '\n')
|
|
135
|
+
return desc
|
|
136
|
+
}
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Stream, StreamStat, Direction } from '@libp2p/interface-connection'
|
|
2
|
+
import { logger } from '@libp2p/logger'
|
|
3
|
+
import * as lengthPrefixed from 'it-length-prefixed'
|
|
4
|
+
import merge from 'it-merge'
|
|
5
|
+
import { pipe } from 'it-pipe'
|
|
6
|
+
import { pushable } from 'it-pushable'
|
|
7
|
+
import defer, { DeferredPromise } from 'p-defer'
|
|
8
|
+
import { Source, Sink } from 'it-stream-types'
|
|
9
|
+
import { Uint8ArrayList } from 'uint8arraylist'
|
|
10
|
+
|
|
11
|
+
import * as pb from '../proto_ts/message.js'
|
|
12
|
+
|
|
13
|
+
const log = logger('libp2p:webrtc:stream')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Constructs a default StreamStat
|
|
17
|
+
*/
|
|
18
|
+
export function defaultStat (dir: Direction): StreamStat {
|
|
19
|
+
return {
|
|
20
|
+
direction: dir,
|
|
21
|
+
timeline: {
|
|
22
|
+
open: 0,
|
|
23
|
+
close: undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface StreamInitOpts {
|
|
29
|
+
/**
|
|
30
|
+
* The network channel used for bidirectional peer-to-peer transfers of
|
|
31
|
+
* arbitrary data
|
|
32
|
+
*
|
|
33
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel}
|
|
34
|
+
*/
|
|
35
|
+
channel: RTCDataChannel
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* User defined stream metadata
|
|
39
|
+
*/
|
|
40
|
+
metadata?: Record<string, any>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stats about this stream
|
|
44
|
+
*/
|
|
45
|
+
stat: StreamStat
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Callback to invoke when the stream is closed.
|
|
49
|
+
*/
|
|
50
|
+
closeCb?: (stream: WebRTCStream) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* State transitions for a stream
|
|
55
|
+
*/
|
|
56
|
+
interface StreamStateInput {
|
|
57
|
+
/**
|
|
58
|
+
* Outbound conections are opened by the local node, inbound streams are
|
|
59
|
+
* opened by the remote
|
|
60
|
+
*/
|
|
61
|
+
direction: 'inbound' | 'outbound'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Message flag from the protobuffs
|
|
65
|
+
*
|
|
66
|
+
* 0 = FIN
|
|
67
|
+
* 1 = STOP_SENDING
|
|
68
|
+
* 2 = RESET
|
|
69
|
+
*/
|
|
70
|
+
flag: pb.Message_Flag
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export enum StreamStates {
|
|
74
|
+
OPEN,
|
|
75
|
+
READ_CLOSED,
|
|
76
|
+
WRITE_CLOSED,
|
|
77
|
+
CLOSED,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class StreamState {
|
|
81
|
+
state: StreamStates = StreamStates.OPEN
|
|
82
|
+
|
|
83
|
+
transition ({ direction, flag }: StreamStateInput): [StreamStates, StreamStates] {
|
|
84
|
+
const prev = this.state
|
|
85
|
+
|
|
86
|
+
// return early if the stream is closed
|
|
87
|
+
if (this.state === StreamStates.CLOSED) {
|
|
88
|
+
return [prev, StreamStates.CLOSED]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (direction === 'inbound') {
|
|
92
|
+
switch (flag) {
|
|
93
|
+
case pb.Message_Flag.FIN:
|
|
94
|
+
if (this.state === StreamStates.OPEN) {
|
|
95
|
+
this.state = StreamStates.READ_CLOSED
|
|
96
|
+
} else if (this.state === StreamStates.WRITE_CLOSED) {
|
|
97
|
+
this.state = StreamStates.CLOSED
|
|
98
|
+
}
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
case pb.Message_Flag.STOP_SENDING:
|
|
102
|
+
if (this.state === StreamStates.OPEN) {
|
|
103
|
+
this.state = StreamStates.WRITE_CLOSED
|
|
104
|
+
} else if (this.state === StreamStates.READ_CLOSED) {
|
|
105
|
+
this.state = StreamStates.CLOSED
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
case pb.Message_Flag.RESET:
|
|
110
|
+
this.state = StreamStates.CLOSED
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
// no default
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
switch (flag) {
|
|
117
|
+
case pb.Message_Flag.FIN:
|
|
118
|
+
if (this.state === StreamStates.OPEN) {
|
|
119
|
+
this.state = StreamStates.WRITE_CLOSED
|
|
120
|
+
} else if (this.state === StreamStates.READ_CLOSED) {
|
|
121
|
+
this.state = StreamStates.CLOSED
|
|
122
|
+
}
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
case pb.Message_Flag.STOP_SENDING:
|
|
126
|
+
if (this.state === StreamStates.OPEN) {
|
|
127
|
+
this.state = StreamStates.READ_CLOSED
|
|
128
|
+
} else if (this.state === StreamStates.WRITE_CLOSED) {
|
|
129
|
+
this.state = StreamStates.CLOSED
|
|
130
|
+
}
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
case pb.Message_Flag.RESET:
|
|
134
|
+
this.state = StreamStates.CLOSED
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
// no default
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [prev, this.state]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class WebRTCStream implements Stream {
|
|
145
|
+
/**
|
|
146
|
+
* Unique identifier for a stream
|
|
147
|
+
*/
|
|
148
|
+
id: string;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Stats about this stream
|
|
152
|
+
*/
|
|
153
|
+
stat: StreamStat;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* User defined stream metadata
|
|
157
|
+
*/
|
|
158
|
+
metadata: Record<string, any>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The data channel used to send and receive data
|
|
162
|
+
*/
|
|
163
|
+
private readonly channel: RTCDataChannel;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The current state of the stream
|
|
167
|
+
*/
|
|
168
|
+
streamState = new StreamState();
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read unwrapped protobuf data from the underlying datachannel.
|
|
172
|
+
* _src is exposed to the user via the `source` getter to .
|
|
173
|
+
*/
|
|
174
|
+
private readonly _src: Source<Uint8ArrayList>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* push data from the underlying datachannel to the length prefix decoder
|
|
178
|
+
* and then the protobuf decoder.
|
|
179
|
+
*/
|
|
180
|
+
private readonly _innersrc = pushable();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Write data to the remote peer.
|
|
184
|
+
* It takes care of wrapping data in a protobuf and adding the length prefix.
|
|
185
|
+
*/
|
|
186
|
+
sink: Sink<Uint8ArrayList | Uint8Array, Promise<void>>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Deferred promise that resolves when the underlying datachannel is in the
|
|
190
|
+
* open state.
|
|
191
|
+
*/
|
|
192
|
+
opened: DeferredPromise<void> = defer();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Triggers a generator which can be used to close the sink.
|
|
196
|
+
*/
|
|
197
|
+
closeWritePromise: DeferredPromise<void> = defer();
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Callback to invoke when the stream is closed.
|
|
201
|
+
*/
|
|
202
|
+
closeCb?: (stream: WebRTCStream) => void
|
|
203
|
+
|
|
204
|
+
constructor (opts: StreamInitOpts) {
|
|
205
|
+
this.channel = opts.channel
|
|
206
|
+
this.id = this.channel.label
|
|
207
|
+
|
|
208
|
+
this.stat = opts.stat
|
|
209
|
+
switch (this.channel.readyState) {
|
|
210
|
+
case 'open':
|
|
211
|
+
this.opened.resolve()
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
case 'closed':
|
|
215
|
+
case 'closing':
|
|
216
|
+
this.streamState.state = StreamStates.CLOSED
|
|
217
|
+
if (this.stat.timeline.close === undefined || this.stat.timeline.close === 0) {
|
|
218
|
+
this.stat.timeline.close = new Date().getTime()
|
|
219
|
+
}
|
|
220
|
+
this.opened.resolve()
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
// no default
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.metadata = opts.metadata ?? {}
|
|
227
|
+
|
|
228
|
+
// closable sink
|
|
229
|
+
this.sink = this._sinkFn
|
|
230
|
+
|
|
231
|
+
// handle RTCDataChannel events
|
|
232
|
+
this.channel.onopen = (_evt) => {
|
|
233
|
+
this.stat.timeline.open = new Date().getTime()
|
|
234
|
+
this.opened.resolve()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.channel.onclose = (_evt) => {
|
|
238
|
+
this.close()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.channel.onerror = (evt) => {
|
|
242
|
+
const err = (evt as RTCErrorEvent).error
|
|
243
|
+
this.abort(err)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const self = this
|
|
247
|
+
|
|
248
|
+
// reader pipe
|
|
249
|
+
this.channel.onmessage = async ({ data }) => {
|
|
250
|
+
if (data === null || data.length === 0) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
this._innersrc.push(new Uint8Array(data as ArrayBufferLike))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// pipe framed protobuf messages through a length prefixed decoder, and
|
|
257
|
+
// surface data from the `Message.message` field through a source.
|
|
258
|
+
this._src = pipe(
|
|
259
|
+
this._innersrc,
|
|
260
|
+
lengthPrefixed.decode(),
|
|
261
|
+
(source) => (async function * () {
|
|
262
|
+
for await (const buf of source) {
|
|
263
|
+
const message = self.processIncomingProtobuf(buf.subarray())
|
|
264
|
+
if (message != null) {
|
|
265
|
+
yield new Uint8ArrayList(message)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
})()
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If user attempts to set a new source this should be a noop
|
|
273
|
+
set source (_src: Source<Uint8ArrayList>) { }
|
|
274
|
+
|
|
275
|
+
get source (): Source<Uint8ArrayList> {
|
|
276
|
+
return this._src
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Closable sink
|
|
281
|
+
*/
|
|
282
|
+
private async _sinkFn (src: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
|
|
283
|
+
await this.opened.promise
|
|
284
|
+
|
|
285
|
+
const isClosed = (state: StreamStates) => state === StreamStates.CLOSED || state === StreamStates.WRITE_CLOSED
|
|
286
|
+
|
|
287
|
+
if (isClosed(this.streamState.state)) {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const self = this
|
|
292
|
+
const closeWriteIterable = {
|
|
293
|
+
async * [Symbol.asyncIterator] () {
|
|
294
|
+
await self.closeWritePromise.promise
|
|
295
|
+
yield new Uint8Array(0)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for await (const buf of merge(closeWriteIterable, src)) {
|
|
300
|
+
if (isClosed(self.streamState.state)) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const msgbuf = pb.Message.toBinary({ message: buf.subarray() })
|
|
305
|
+
const sendbuf = lengthPrefixed.encode.single(msgbuf)
|
|
306
|
+
|
|
307
|
+
this.channel.send(sendbuf.subarray())
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Handle incoming
|
|
313
|
+
*/
|
|
314
|
+
processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined {
|
|
315
|
+
const message = pb.Message.fromBinary(buffer)
|
|
316
|
+
|
|
317
|
+
if (message.flag !== undefined) {
|
|
318
|
+
const [currentState, nextState] = this.streamState.transition({ direction: 'inbound', flag: message.flag })
|
|
319
|
+
|
|
320
|
+
if (currentState !== nextState) {
|
|
321
|
+
// @TODO(ddimaria): determine if we need to check for StreamStates.OPEN
|
|
322
|
+
switch (nextState) {
|
|
323
|
+
case StreamStates.READ_CLOSED:
|
|
324
|
+
this._innersrc.end()
|
|
325
|
+
break
|
|
326
|
+
case StreamStates.WRITE_CLOSED:
|
|
327
|
+
this.closeWritePromise.resolve()
|
|
328
|
+
break
|
|
329
|
+
case StreamStates.CLOSED:
|
|
330
|
+
this.close()
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
// no default
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return message.message
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Close a stream for reading and writing
|
|
343
|
+
*/
|
|
344
|
+
close (): void {
|
|
345
|
+
this.stat.timeline.close = new Date().getTime()
|
|
346
|
+
this.streamState.state = StreamStates.CLOSED
|
|
347
|
+
this._innersrc.end()
|
|
348
|
+
this.closeWritePromise.resolve()
|
|
349
|
+
this.channel.close()
|
|
350
|
+
|
|
351
|
+
if (this.closeCb !== undefined) {
|
|
352
|
+
this.closeCb(this)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Close a stream for reading only
|
|
358
|
+
*/
|
|
359
|
+
closeRead (): void {
|
|
360
|
+
const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.STOP_SENDING })
|
|
361
|
+
|
|
362
|
+
if (currentState === StreamStates.OPEN || currentState === StreamStates.WRITE_CLOSED) {
|
|
363
|
+
this._sendFlag(pb.Message_Flag.STOP_SENDING);
|
|
364
|
+
(this._innersrc).end()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (currentState !== nextState && nextState === StreamStates.CLOSED) {
|
|
368
|
+
this.close()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Close a stream for writing only
|
|
374
|
+
*/
|
|
375
|
+
closeWrite (): void {
|
|
376
|
+
const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.FIN })
|
|
377
|
+
|
|
378
|
+
if (currentState === StreamStates.OPEN || currentState === StreamStates.READ_CLOSED) {
|
|
379
|
+
this._sendFlag(pb.Message_Flag.FIN)
|
|
380
|
+
this.closeWritePromise.resolve()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (currentState !== nextState && nextState === StreamStates.CLOSED) {
|
|
384
|
+
this.close()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Call when a local error occurs, should close the stream for reading and writing
|
|
390
|
+
*/
|
|
391
|
+
abort (err: Error): void {
|
|
392
|
+
log.error(`An error occurred, clost the stream for reading and writing: ${err.message}`)
|
|
393
|
+
this.close()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Close the stream for writing, and indicate to the remote side this is being done 'abruptly'
|
|
398
|
+
*
|
|
399
|
+
* @see closeWrite
|
|
400
|
+
*/
|
|
401
|
+
reset (): void {
|
|
402
|
+
this.stat = defaultStat(this.stat.direction)
|
|
403
|
+
const [currentState, nextState] = this.streamState.transition({ direction: 'outbound', flag: pb.Message_Flag.RESET })
|
|
404
|
+
|
|
405
|
+
if (currentState !== nextState) {
|
|
406
|
+
this._sendFlag(pb.Message_Flag.RESET)
|
|
407
|
+
this.close()
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private _sendFlag (flag: pb.Message_Flag): void {
|
|
412
|
+
try {
|
|
413
|
+
log.trace('Sending flag: %s', flag.toString())
|
|
414
|
+
const msgbuf = pb.Message.toBinary({ flag: flag })
|
|
415
|
+
this.channel.send(lengthPrefixed.encode.single(msgbuf).subarray())
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err instanceof Error) {
|
|
418
|
+
log.error(`Exception while sending flag ${flag}: ${err.message}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|