@dxos/network-manager 0.3.8-next.f4e0086 → 0.3.9-main.03b62b6
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/dist/lib/browser/{chunk-UKYZHA5S.mjs → chunk-KGZ5C6JO.mjs} +392 -103
- package/dist/lib/browser/chunk-KGZ5C6JO.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/chunk-JDKM4UPJ.cjs +470 -0
- package/dist/lib/node/chunk-JDKM4UPJ.cjs.map +7 -0
- package/dist/lib/node/chunk-WUBZ4UOJ.cjs +4060 -0
- package/dist/lib/node/chunk-WUBZ4UOJ.cjs.map +7 -0
- package/dist/lib/node/datachannel-3VTTIMR5.cjs +35 -0
- package/dist/lib/node/datachannel-3VTTIMR5.cjs.map +7 -0
- package/dist/lib/node/index.cjs +35 -4250
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +111 -4139
- package/dist/lib/node/testing/index.cjs.map +4 -4
- package/dist/types/src/connection-log.d.ts.map +1 -1
- package/dist/types/src/swarm/connection.d.ts +4 -1
- package/dist/types/src/swarm/connection.d.ts.map +1 -1
- package/dist/types/src/topology/mmst-topology.d.ts +1 -0
- package/dist/types/src/topology/mmst-topology.d.ts.map +1 -1
- package/dist/types/src/transport/datachannel/rtc-peer-connection.d.ts +7 -0
- package/dist/types/src/transport/datachannel/rtc-peer-connection.d.ts.map +1 -1
- package/dist/types/src/transport/libdatachannel-transport.d.ts +3 -1
- package/dist/types/src/transport/libdatachannel-transport.d.ts.map +1 -1
- package/dist/types/src/transport/memory-transport.d.ts +3 -1
- package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
- package/dist/types/src/transport/simplepeer-transport-proxy.d.ts +3 -1
- package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +1 -1
- package/dist/types/src/transport/simplepeer-transport-service.d.ts +3 -1
- package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +1 -1
- package/dist/types/src/transport/simplepeer-transport.d.ts +7 -1
- package/dist/types/src/transport/simplepeer-transport.d.ts.map +1 -1
- package/dist/types/src/transport/tcp-transport.browser.d.ts +3 -1
- package/dist/types/src/transport/tcp-transport.browser.d.ts.map +1 -1
- package/dist/types/src/transport/tcp-transport.d.ts +3 -1
- package/dist/types/src/transport/tcp-transport.d.ts.map +1 -1
- package/dist/types/src/transport/transport.d.ts +17 -0
- package/dist/types/src/transport/transport.d.ts.map +1 -1
- package/dist/types/src/wire-protocol.d.ts +1 -1
- package/dist/types/src/wire-protocol.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/connection-log.ts +27 -1
- package/src/swarm/connection.ts +40 -6
- package/src/topology/mmst-topology.ts +34 -6
- package/src/transport/datachannel/rtc-peer-connection.ts +20 -12
- package/src/transport/libdatachannel-transport.test.ts +0 -4
- package/src/transport/libdatachannel-transport.ts +44 -4
- package/src/transport/memory-transport.ts +14 -1
- package/src/transport/simplepeer-transport-proxy.ts +9 -1
- package/src/transport/simplepeer-transport-service.ts +14 -0
- package/src/transport/simplepeer-transport.ts +70 -4
- package/src/transport/tcp-transport.browser.ts +9 -1
- package/src/transport/tcp-transport.ts +19 -1
- package/src/transport/transport.ts +19 -0
- package/src/wire-protocol.ts +3 -3
- package/dist/lib/browser/chunk-UKYZHA5S.mjs.map +0 -7
|
@@ -10,6 +10,9 @@ import { log } from '@dxos/log';
|
|
|
10
10
|
|
|
11
11
|
import { type SwarmController, type Topology } from './topology';
|
|
12
12
|
|
|
13
|
+
const MIN_UPDATE_INTERVAL = 1000 * 10;
|
|
14
|
+
const MAX_CHANGES_PER_UPDATE = 1;
|
|
15
|
+
|
|
13
16
|
export interface MMSTTopologyOptions {
|
|
14
17
|
/**
|
|
15
18
|
* Number of connections the peer will originate by itself.
|
|
@@ -36,6 +39,8 @@ export class MMSTTopology implements Topology {
|
|
|
36
39
|
|
|
37
40
|
private _sampleCollected = false;
|
|
38
41
|
|
|
42
|
+
private _lastAction = new Date(0);
|
|
43
|
+
|
|
39
44
|
constructor({ originateConnections = 2, maxPeers = 4, sampleSize = 10 }: MMSTTopologyOptions = {}) {
|
|
40
45
|
this._originateConnections = originateConnections;
|
|
41
46
|
this._maxPeers = maxPeers;
|
|
@@ -74,22 +79,45 @@ export class MMSTTopology implements Topology {
|
|
|
74
79
|
invariant(this._controller, 'Not initialized');
|
|
75
80
|
const { connected, candidates, ownPeerId } = this._controller.getState();
|
|
76
81
|
|
|
82
|
+
// TODO(nf): does this rate limiting/flap dampening logic belong here or in the SwarmController?
|
|
77
83
|
if (connected.length > this._maxPeers) {
|
|
78
84
|
// Disconnect extra peers.
|
|
85
|
+
log(`disconnect ${connected.length - this._maxPeers} peers.`);
|
|
79
86
|
const sorted = sortByXorDistance(connected, ownPeerId)
|
|
80
87
|
.reverse()
|
|
81
88
|
.slice(0, this._maxPeers - connected.length);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
invariant(sorted.length === 0);
|
|
90
|
+
|
|
91
|
+
if (sorted.length > MAX_CHANGES_PER_UPDATE) {
|
|
92
|
+
log(`want to disconnect ${sorted.length} peers but limited to ${MAX_CHANGES_PER_UPDATE}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Date.now() - this._lastAction.getTime() > MIN_UPDATE_INTERVAL) {
|
|
96
|
+
for (const peer of sorted.slice(0, MAX_CHANGES_PER_UPDATE)) {
|
|
97
|
+
log(`Disconnect ${peer}.`);
|
|
98
|
+
this._controller.disconnect(peer);
|
|
99
|
+
}
|
|
100
|
+
this._lastAction = new Date();
|
|
101
|
+
} else {
|
|
102
|
+
log('rate limited discconnect');
|
|
85
103
|
}
|
|
86
104
|
} else if (connected.length < this._originateConnections) {
|
|
87
105
|
// Connect new peers to reach desired quota.
|
|
106
|
+
log(`connect ${this._originateConnections - connected.length} peers.`);
|
|
88
107
|
const sample = candidates.sort(() => Math.random() - 0.5).slice(0, this._sampleSize);
|
|
89
108
|
const sorted = sortByXorDistance(sample, ownPeerId).slice(0, this._originateConnections - connected.length);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
|
|
110
|
+
if (sorted.length > MAX_CHANGES_PER_UPDATE) {
|
|
111
|
+
log(`want to connect ${sorted.length} peers but limited to ${MAX_CHANGES_PER_UPDATE}`);
|
|
112
|
+
}
|
|
113
|
+
if (Date.now() - this._lastAction.getTime() > MIN_UPDATE_INTERVAL) {
|
|
114
|
+
for (const peer of sorted.slice(0, MAX_CHANGES_PER_UPDATE)) {
|
|
115
|
+
log(`Connect ${peer}.`);
|
|
116
|
+
this._controller.connect(peer);
|
|
117
|
+
}
|
|
118
|
+
this._lastAction = new Date();
|
|
119
|
+
} else {
|
|
120
|
+
log('rate limited connect');
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import node from 'node-datachannel';
|
|
5
|
+
import node, { type SelectedCandidateInfo } from 'node-datachannel';
|
|
6
6
|
import defer, { type DeferredPromise } from 'p-defer';
|
|
7
7
|
|
|
8
8
|
import { DataChannel } from './rtc-data-channel';
|
|
@@ -45,21 +45,17 @@ export class PeerConnection extends EventTarget implements RTCPeerConnection {
|
|
|
45
45
|
const iceServers = init.iceServers ?? [];
|
|
46
46
|
|
|
47
47
|
this.#peerConnection = new node.PeerConnection(`peer-${Math.random()}`, {
|
|
48
|
+
// convert https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers to the format expected by https://github.com/murat-dogan/node-datachannel/blob/master/src/peer-connection-wrapper.cpp#L101
|
|
48
49
|
iceServers: iceServers
|
|
49
50
|
.map((server) => {
|
|
50
|
-
const urls =
|
|
51
|
+
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
|
|
51
52
|
|
|
52
53
|
return urls.map((url) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
password: server.credential,
|
|
59
|
-
// relayType - how to specify?
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return iceServer;
|
|
54
|
+
if (server.username && server.credential) {
|
|
55
|
+
const [protocol, rest] = url.split(/:(.*)/);
|
|
56
|
+
return `${protocol}:${server.username}:${server.credential}@${rest}`;
|
|
57
|
+
}
|
|
58
|
+
return url;
|
|
63
59
|
});
|
|
64
60
|
})
|
|
65
61
|
.flat(),
|
|
@@ -188,6 +184,10 @@ export class PeerConnection extends EventTarget implements RTCPeerConnection {
|
|
|
188
184
|
this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0');
|
|
189
185
|
}
|
|
190
186
|
|
|
187
|
+
getSelectedCandidatePair(): { local: SelectedCandidateInfo; remote: SelectedCandidateInfo } | null {
|
|
188
|
+
return this.#peerConnection.getSelectedCandidatePair();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
191
|
addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender {
|
|
192
192
|
throw new Error('addTrack Not implemented');
|
|
193
193
|
}
|
|
@@ -206,6 +206,14 @@ export class PeerConnection extends EventTarget implements RTCPeerConnection {
|
|
|
206
206
|
this.#peerConnection.destroy();
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
bytesSent(): number {
|
|
210
|
+
return this.#peerConnection.bytesSent();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
bytesReceived(): number {
|
|
214
|
+
return this.#peerConnection.bytesReceived();
|
|
215
|
+
}
|
|
216
|
+
|
|
209
217
|
createDataChannel(label: string, dataChannelDict: RTCDataChannelInit = {}): RTCDataChannel {
|
|
210
218
|
const channel = this.#peerConnection.createDataChannel(label, dataChannelDict);
|
|
211
219
|
const dataChannel = new DataChannel(channel, dataChannelDict);
|
|
@@ -10,7 +10,7 @@ import { log } from '@dxos/log';
|
|
|
10
10
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
11
11
|
|
|
12
12
|
import { type PeerConnection } from './datachannel/rtc-peer-connection';
|
|
13
|
-
import { type Transport, type TransportFactory } from './transport';
|
|
13
|
+
import { type Transport, type TransportFactory, type TransportStats } from './transport';
|
|
14
14
|
|
|
15
15
|
export type LibDataChannelTransportParams = {
|
|
16
16
|
initiator: boolean;
|
|
@@ -126,14 +126,18 @@ export class LibDataChannelTransport implements Transport {
|
|
|
126
126
|
log.debug('dataChannel.onopen');
|
|
127
127
|
const duplex = new Duplex({
|
|
128
128
|
read: () => {},
|
|
129
|
-
write: (chunk, encoding, callback) => {
|
|
129
|
+
write: async (chunk, encoding, callback) => {
|
|
130
130
|
// todo wait to open
|
|
131
131
|
|
|
132
132
|
if (chunk.length > MAX_MESSAGE_SIZE) {
|
|
133
133
|
this.errors.raise(new Error(`message too large: ${chunk.length} > ${MAX_MESSAGE_SIZE}`));
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
try {
|
|
136
|
+
dataChannel.send(chunk);
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
this.errors.raise(err);
|
|
139
|
+
await this._close();
|
|
140
|
+
}
|
|
137
141
|
if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
|
|
138
142
|
if (this._writeCallback !== null) {
|
|
139
143
|
log.error("consumer trying to write before we're ready for more data");
|
|
@@ -233,6 +237,42 @@ export class LibDataChannelTransport implements Transport {
|
|
|
233
237
|
});
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
async getDetails(): Promise<string> {
|
|
241
|
+
return this._peer
|
|
242
|
+
.then(async (peer) => {
|
|
243
|
+
const cp = await peer.getSelectedCandidatePair();
|
|
244
|
+
if (!cp) {
|
|
245
|
+
return 'unavailable';
|
|
246
|
+
}
|
|
247
|
+
return `${cp.remote.address}:${cp.remote.port}/${cp.remote.transportType} ${cp.remote.type}`;
|
|
248
|
+
})
|
|
249
|
+
.catch((err) => {
|
|
250
|
+
log.catch(err);
|
|
251
|
+
return 'unavailable';
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getStats(): Promise<TransportStats> {
|
|
256
|
+
return this._peer
|
|
257
|
+
.then((peer) => {
|
|
258
|
+
return {
|
|
259
|
+
bytesSent: peer.bytesSent(),
|
|
260
|
+
bytesReceived: peer.bytesReceived(),
|
|
261
|
+
packetsSent: 0,
|
|
262
|
+
packetsReceived: 0,
|
|
263
|
+
};
|
|
264
|
+
})
|
|
265
|
+
.catch((err) => {
|
|
266
|
+
log.catch(err);
|
|
267
|
+
return {
|
|
268
|
+
bytesSent: 0,
|
|
269
|
+
bytesReceived: 0,
|
|
270
|
+
packetsSent: 0,
|
|
271
|
+
packetsReceived: 0,
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
236
276
|
// TODO(nf): add classmethod to call node-datachannel.cleanup() when all instances have been destroyed?
|
|
237
277
|
async destroy(): Promise<void> {
|
|
238
278
|
await this._close();
|
|
@@ -12,7 +12,7 @@ import { log, logInfo } from '@dxos/log';
|
|
|
12
12
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
13
13
|
import { ComplexMap } from '@dxos/util';
|
|
14
14
|
|
|
15
|
-
import { type Transport, type TransportFactory, type TransportOptions } from './transport';
|
|
15
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from './transport';
|
|
16
16
|
|
|
17
17
|
// TODO(burdon): Make configurable.
|
|
18
18
|
// Delay (in milliseconds) for data being sent through in-memory connections to simulate network latency.
|
|
@@ -166,4 +166,17 @@ export class MemoryTransport implements Transport {
|
|
|
166
166
|
this._remote.wake(remoteId);
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
async getDetails(): Promise<string> {
|
|
171
|
+
return this._instanceId.toHex();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getStats(): Promise<TransportStats> {
|
|
175
|
+
return {
|
|
176
|
+
bytesSent: 0,
|
|
177
|
+
bytesReceived: 0,
|
|
178
|
+
packetsSent: 0,
|
|
179
|
+
packetsReceived: 0,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
169
182
|
}
|
|
@@ -22,7 +22,7 @@ import { ConnectionState, type BridgeEvent, type BridgeService } from '@dxos/pro
|
|
|
22
22
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
23
23
|
import { arrayToBuffer } from '@dxos/util';
|
|
24
24
|
|
|
25
|
-
import { type Transport, type TransportFactory, type TransportOptions } from './transport';
|
|
25
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from './transport';
|
|
26
26
|
|
|
27
27
|
const RESP_MIN_THRESHOLD = 500;
|
|
28
28
|
const TIMEOUT_THRESHOLD = 10;
|
|
@@ -145,6 +145,14 @@ export class SimplePeerTransportProxy implements Transport {
|
|
|
145
145
|
.catch((err) => this.errors.raise(decodeError(err)));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
async getDetails(): Promise<string> {
|
|
149
|
+
return (await this._params.bridgeService.getDetails({ proxyId: this._proxyId })).details;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getStats(): Promise<TransportStats> {
|
|
153
|
+
return (await this._params.bridgeService.getStats({ proxyId: this._proxyId })).stats as TransportStats;
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
// TODO(burdon): Move open from constructor.
|
|
149
157
|
async destroy(): Promise<void> {
|
|
150
158
|
await this._ctx.dispose();
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
type BridgeEvent,
|
|
17
17
|
ConnectionState,
|
|
18
18
|
type CloseRequest,
|
|
19
|
+
type DetailsRequest,
|
|
20
|
+
type DetailsResponse,
|
|
21
|
+
type StatsRequest,
|
|
22
|
+
type StatsResponse,
|
|
19
23
|
} from '@dxos/protocols/proto/dxos/mesh/bridge';
|
|
20
24
|
import { ComplexMap } from '@dxos/util';
|
|
21
25
|
|
|
@@ -113,6 +117,16 @@ export class SimplePeerTransportService implements BridgeService {
|
|
|
113
117
|
await this.transports.get(proxyId)!.transport.signal(signal);
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
async getDetails({ proxyId }: DetailsRequest): Promise<DetailsResponse> {
|
|
121
|
+
invariant(this.transports.has(proxyId));
|
|
122
|
+
return { details: await this.transports.get(proxyId)!.transport.getDetails() };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getStats({ proxyId }: StatsRequest): Promise<StatsResponse> {
|
|
126
|
+
invariant(this.transports.has(proxyId));
|
|
127
|
+
return { stats: await this.transports.get(proxyId)!.transport.getStats() };
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
async sendData({ proxyId, payload }: DataRequest): Promise<void> {
|
|
117
131
|
if (this.transports.get(proxyId)?.state !== 'OPEN') {
|
|
118
132
|
log.debug('transport is closed');
|
|
@@ -13,7 +13,7 @@ import { log } from '@dxos/log';
|
|
|
13
13
|
import { ConnectionResetError, ConnectivityError, ProtocolError, UnknownProtocolError, trace } from '@dxos/protocols';
|
|
14
14
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
15
15
|
|
|
16
|
-
import { type Transport, type TransportFactory } from './transport';
|
|
16
|
+
import { type Transport, type TransportFactory, type TransportStats } from './transport';
|
|
17
17
|
import { wrtc } from './webrtc';
|
|
18
18
|
|
|
19
19
|
export type SimplePeerTransportParams = {
|
|
@@ -37,6 +37,9 @@ export class SimplePeerTransport implements Transport {
|
|
|
37
37
|
|
|
38
38
|
private readonly _instanceId = PublicKey.random().toHex();
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @params opts.config formatted as per https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection
|
|
42
|
+
*/
|
|
40
43
|
constructor(private readonly params: SimplePeerTransportParams) {
|
|
41
44
|
log.trace('dxos.mesh.webrtc-transport.constructor', trace.begin({ id: this._instanceId }));
|
|
42
45
|
log('created connection', params);
|
|
@@ -71,6 +74,7 @@ export class SimplePeerTransport implements Transport {
|
|
|
71
74
|
if (err.errorDetail === 'sctp-failure') {
|
|
72
75
|
this.errors.raise(new ConnectionResetError('sctp-failure from RTCError', err));
|
|
73
76
|
} else {
|
|
77
|
+
log.info('unknown RTCError', { err });
|
|
74
78
|
this.errors.raise(new UnknownProtocolError('unknown RTCError', err));
|
|
75
79
|
}
|
|
76
80
|
// catch more generic simple-peer errors: https://github.com/feross/simple-peer/blob/master/README.md#error-codes
|
|
@@ -80,10 +84,12 @@ export class SimplePeerTransport implements Transport {
|
|
|
80
84
|
case 'ERR_WEBRTC_SUPPORT':
|
|
81
85
|
this.errors.raise(new ProtocolError('WebRTC not supported', err));
|
|
82
86
|
break;
|
|
87
|
+
case 'ERR_SIGNALING':
|
|
88
|
+
this.errors.raise(new ConnectivityError('signaling failure', err));
|
|
89
|
+
break;
|
|
83
90
|
case 'ERR_ICE_CONNECTION_FAILURE':
|
|
84
91
|
case 'ERR_DATA_CHANNEL':
|
|
85
92
|
case 'ERR_CONNECTION_FAILURE':
|
|
86
|
-
case 'ERR_SIGNALING':
|
|
87
93
|
this.errors.raise(new ConnectivityError('unknown communication failure', err));
|
|
88
94
|
break;
|
|
89
95
|
// errors due to library issues or improper API usage
|
|
@@ -109,7 +115,7 @@ export class SimplePeerTransport implements Transport {
|
|
|
109
115
|
(this._peer as any)._pc.getStats().then((stats: any) => {
|
|
110
116
|
log.info('report after webrtc error', {
|
|
111
117
|
config: this.params.webrtcConfig,
|
|
112
|
-
stats,
|
|
118
|
+
stats: Object.fromEntries((stats as any).entries()),
|
|
113
119
|
});
|
|
114
120
|
});
|
|
115
121
|
}
|
|
@@ -119,17 +125,77 @@ export class SimplePeerTransport implements Transport {
|
|
|
119
125
|
|
|
120
126
|
await this.destroy();
|
|
121
127
|
});
|
|
128
|
+
|
|
122
129
|
log.trace('dxos.mesh.webrtc-transport.constructor', trace.end({ id: this._instanceId }));
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
async getStats(): Promise<TransportStats> {
|
|
133
|
+
const stats = await this._getStats();
|
|
134
|
+
if (!stats) {
|
|
135
|
+
return {
|
|
136
|
+
bytesSent: 0,
|
|
137
|
+
bytesReceived: 0,
|
|
138
|
+
packetsSent: 0,
|
|
139
|
+
packetsReceived: 0,
|
|
140
|
+
rawStats: {},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// TODO(nf): transport or candidatePair?
|
|
145
|
+
return {
|
|
146
|
+
bytesSent: stats.transport.bytesSent,
|
|
147
|
+
bytesReceived: stats.transport.bytesReceived,
|
|
148
|
+
packetsSent: stats.transport.messagesSent,
|
|
149
|
+
packetsReceived: stats.transport.messagesReceived,
|
|
150
|
+
rawStats: stats.raw,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async _getStats(): Promise<any> {
|
|
155
|
+
if (typeof (this._peer as any)?._pc?.getStats !== 'function') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return await (this._peer as any)._pc.getStats().then((stats: any) => {
|
|
159
|
+
const statsEntries = Array.from(stats.entries() as any[]);
|
|
160
|
+
const transport = statsEntries.filter((s: any) => s[1].type === 'transport')[0][1];
|
|
161
|
+
const candidatePair = statsEntries.filter((s: any) => s[0] === transport.selectedCandidatePairId);
|
|
162
|
+
let selectedCandidatePair: any;
|
|
163
|
+
let remoteCandidate: any;
|
|
164
|
+
if (candidatePair.length > 0) {
|
|
165
|
+
selectedCandidatePair = candidatePair[0][1];
|
|
166
|
+
remoteCandidate = statsEntries.filter((s: any) => s[0] === selectedCandidatePair.remoteCandidateId)[0][1];
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
datachannel: statsEntries.filter((s: any) => s[1].type === 'data-channel')[0][1],
|
|
170
|
+
transport,
|
|
171
|
+
selectedCandidatePair,
|
|
172
|
+
remoteCandidate,
|
|
173
|
+
raw: Object.fromEntries(stats.entries()),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getDetails(): Promise<string> {
|
|
179
|
+
const stats = await this._getStats();
|
|
180
|
+
const rc = stats?.remoteCandidate;
|
|
181
|
+
if (!rc) {
|
|
182
|
+
return 'unavailable';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (rc.candidateType === 'relay') {
|
|
186
|
+
return `${rc.ip}:${rc.port}/${rc.protocol} relay for ${rc.relatedAddress}:${rc.relatedPort}`;
|
|
187
|
+
}
|
|
188
|
+
return `${rc.ip}:${rc.port}/${rc.protocol} ${rc.candidateType}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
125
191
|
async destroy() {
|
|
126
192
|
log('closing...');
|
|
127
193
|
if (this._closed) {
|
|
128
194
|
return;
|
|
129
195
|
}
|
|
130
|
-
this._closed = true;
|
|
131
196
|
this._disconnectStreams();
|
|
132
197
|
this._peer!.destroy();
|
|
198
|
+
this._closed = true;
|
|
133
199
|
this.closed.emit();
|
|
134
200
|
log('closed');
|
|
135
201
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Event } from '@dxos/async';
|
|
6
6
|
import { ErrorStream } from '@dxos/debug';
|
|
7
7
|
|
|
8
|
-
import { type Transport, type TransportFactory } from './transport';
|
|
8
|
+
import { type Transport, type TransportFactory, type TransportStats } from './transport';
|
|
9
9
|
|
|
10
10
|
// NOTE: Browser stub.
|
|
11
11
|
|
|
@@ -25,4 +25,12 @@ export class TcpTransport implements Transport {
|
|
|
25
25
|
signal() {
|
|
26
26
|
throw new Error('Method not implemented.');
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
async getStats(): Promise<TransportStats> {
|
|
30
|
+
throw new Error('Method not implemented.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getDetails(): Promise<string> {
|
|
34
|
+
throw new Error('Method not implemented.');
|
|
35
|
+
}
|
|
28
36
|
}
|
|
@@ -9,7 +9,7 @@ import { ErrorStream } from '@dxos/debug';
|
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
11
11
|
|
|
12
|
-
import { type Transport, type TransportFactory, type TransportOptions } from './transport';
|
|
12
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from './transport';
|
|
13
13
|
|
|
14
14
|
export const TcpTransportFactory: TransportFactory = {
|
|
15
15
|
createTransport: (params) => new TcpTransport(params),
|
|
@@ -87,6 +87,24 @@ export class TcpTransport implements Transport {
|
|
|
87
87
|
socket.connect({ port: payload.port, host: 'localhost' });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
async getDetails(): Promise<string> {
|
|
91
|
+
if (this.options.initiator) {
|
|
92
|
+
const { port, address } = this._server?.address() as AddressInfo;
|
|
93
|
+
return `LISTEN ${address}:${port}`;
|
|
94
|
+
}
|
|
95
|
+
const { port, address } = this._socket?.address() as AddressInfo;
|
|
96
|
+
return `ACCEPT ${address}:${port}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getStats(): Promise<TransportStats> {
|
|
100
|
+
return {
|
|
101
|
+
bytesSent: 0,
|
|
102
|
+
bytesReceived: 0,
|
|
103
|
+
packetsSent: 0,
|
|
104
|
+
packetsReceived: 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
private _handleSocket(socket: Socket) {
|
|
91
109
|
log('handling socket', { remotePort: socket.remotePort, localPort: socket.localPort });
|
|
92
110
|
this._socket = socket;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Event } from '@dxos/async';
|
|
6
6
|
import { type ErrorStream } from '@dxos/debug';
|
|
7
|
+
import { type PublicKey } from '@dxos/keys';
|
|
7
8
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
8
9
|
|
|
9
10
|
export enum TransportKind {
|
|
@@ -22,6 +23,14 @@ export interface Transport {
|
|
|
22
23
|
connected: Event;
|
|
23
24
|
errors: ErrorStream;
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Transport-specfic stats.
|
|
28
|
+
*/
|
|
29
|
+
getStats(): Promise<TransportStats>;
|
|
30
|
+
/**
|
|
31
|
+
* Transport-specfic connection details.
|
|
32
|
+
*/
|
|
33
|
+
getDetails(): Promise<string>;
|
|
25
34
|
destroy(): Promise<void>;
|
|
26
35
|
signal(signal: Signal): void;
|
|
27
36
|
}
|
|
@@ -43,6 +52,8 @@ export type TransportOptions = {
|
|
|
43
52
|
sendSignal: (signal: Signal) => Promise<void>; // TODO(burdon): Remove async?
|
|
44
53
|
|
|
45
54
|
timeout?: number;
|
|
55
|
+
|
|
56
|
+
sessionId?: PublicKey;
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
/**
|
|
@@ -51,3 +62,11 @@ export type TransportOptions = {
|
|
|
51
62
|
export interface TransportFactory {
|
|
52
63
|
createTransport(options: TransportOptions): Transport;
|
|
53
64
|
}
|
|
65
|
+
|
|
66
|
+
export type TransportStats = {
|
|
67
|
+
bytesSent: number;
|
|
68
|
+
bytesReceived: number;
|
|
69
|
+
packetsSent: number;
|
|
70
|
+
packetsReceived: number;
|
|
71
|
+
rawStats?: any;
|
|
72
|
+
};
|
package/src/wire-protocol.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type WireProtocolProvider = (params: WireProtocolParams) => WireProtocol;
|
|
|
23
23
|
export interface WireProtocol {
|
|
24
24
|
stream: Duplex;
|
|
25
25
|
|
|
26
|
-
open(): Promise<void>;
|
|
26
|
+
open(sessionId?: PublicKey): Promise<void>;
|
|
27
27
|
close(): Promise<void>;
|
|
28
28
|
abort(): Promise<void>;
|
|
29
29
|
}
|
|
@@ -40,8 +40,8 @@ export const createTeleportProtocolFactory = (
|
|
|
40
40
|
const teleport = new Teleport(params);
|
|
41
41
|
return {
|
|
42
42
|
stream: teleport.stream,
|
|
43
|
-
open: async () => {
|
|
44
|
-
await teleport.open();
|
|
43
|
+
open: async (sessionId?: PublicKey) => {
|
|
44
|
+
await teleport.open(sessionId);
|
|
45
45
|
await onConnection(teleport);
|
|
46
46
|
},
|
|
47
47
|
close: async () => {
|