@dxos/network-manager 0.6.13 → 0.6.14-main.1366248
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-XYSYUN63.mjs → chunk-UEVA7BFW.mjs} +1323 -1102
- package/dist/lib/browser/chunk-UEVA7BFW.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +9 -19
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +30 -37
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/transport/tcp/index.mjs +38 -0
- package/dist/lib/browser/transport/tcp/index.mjs.map +7 -0
- package/dist/lib/node/{chunk-4YAYC7WN.cjs → chunk-LK5D44SA.cjs} +1336 -1239
- package/dist/lib/node/chunk-LK5D44SA.cjs.map +7 -0
- package/dist/lib/node/index.cjs +27 -37
- package/dist/lib/node/index.cjs.map +2 -2
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +34 -38
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node/transport/tcp/index.cjs +191 -0
- package/dist/lib/node/transport/tcp/index.cjs.map +7 -0
- package/dist/lib/node-esm/chunk-Y5TD36KR.mjs +4413 -0
- package/dist/lib/node-esm/chunk-Y5TD36KR.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +50 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +285 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/transport/tcp/index.mjs +159 -0
- package/dist/lib/node-esm/transport/tcp/index.mjs.map +7 -0
- package/dist/types/src/network-manager.d.ts +2 -1
- package/dist/types/src/network-manager.d.ts.map +1 -1
- package/dist/types/src/signal/ice.d.ts.map +1 -1
- package/dist/types/src/signal/integration.node.test.d.ts +2 -0
- package/dist/types/src/signal/integration.node.test.d.ts.map +1 -0
- package/dist/types/src/signal/swarm-messenger.node.test.d.ts +2 -0
- package/dist/types/src/signal/swarm-messenger.node.test.d.ts.map +1 -0
- package/dist/types/src/swarm/connection.d.ts.map +1 -1
- package/dist/types/src/swarm/peer.d.ts.map +1 -1
- package/dist/types/src/swarm/swarm.d.ts +2 -1
- package/dist/types/src/swarm/swarm.d.ts.map +1 -1
- package/dist/types/src/testing/test-builder.d.ts +2 -2
- package/dist/types/src/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-wire-protocol.d.ts +1 -2
- package/dist/types/src/testing/test-wire-protocol.d.ts.map +1 -1
- package/dist/types/src/tests/basic-test-suite.d.ts.map +1 -1
- package/dist/types/src/tests/property-test-suite.d.ts.map +1 -1
- package/dist/types/src/tests/tcp-transport.node.test.d.ts +2 -0
- package/dist/types/src/tests/tcp-transport.node.test.d.ts.map +1 -0
- package/dist/types/src/tests/utils.d.ts.map +1 -1
- package/dist/types/src/transport/index.d.ts +1 -5
- package/dist/types/src/transport/index.d.ts.map +1 -1
- package/dist/types/src/transport/memory-transport.d.ts +2 -2
- package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
- package/dist/types/src/transport/tcp/index.d.ts +2 -0
- package/dist/types/src/transport/tcp/index.d.ts.map +1 -0
- package/dist/types/src/transport/{tcp-transport.browser.d.ts → tcp/tcp-transport.browser.d.ts} +3 -3
- package/dist/types/src/transport/tcp/tcp-transport.browser.d.ts.map +1 -0
- package/dist/types/src/transport/{tcp-transport.d.ts → tcp/tcp-transport.d.ts} +3 -3
- package/dist/types/src/transport/tcp/tcp-transport.d.ts.map +1 -0
- package/dist/types/src/transport/transport.d.ts +7 -6
- package/dist/types/src/transport/transport.d.ts.map +1 -1
- package/dist/types/src/transport/webrtc/index.d.ts +4 -0
- package/dist/types/src/transport/webrtc/index.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts +14 -0
- package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts +68 -0
- package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts +33 -0
- package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts +2 -0
- package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts +4 -0
- package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts.map +1 -0
- package/dist/types/src/transport/{simplepeer-transport-proxy.d.ts → webrtc/rtc-transport-proxy.d.ts} +10 -12
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts +2 -0
- package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts.map +1 -0
- package/dist/types/src/transport/{simplepeer-transport-service.d.ts → webrtc/rtc-transport-service.d.ts} +9 -7
- package/dist/types/src/transport/webrtc/rtc-transport-service.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts +4 -0
- package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts +2 -0
- package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/test-utils.d.ts +5 -0
- package/dist/types/src/transport/webrtc/test-utils.d.ts.map +1 -0
- package/dist/types/src/transport/webrtc/utils.d.ts +3 -0
- package/dist/types/src/transport/webrtc/utils.d.ts.map +1 -0
- package/package.json +53 -36
- package/src/network-manager.ts +5 -13
- package/src/signal/ice.test.ts +1 -3
- package/src/signal/ice.ts +6 -1
- package/src/signal/{integration.test.ts → integration.node.test.ts} +9 -15
- package/src/signal/{swarm-messenger.test.ts → swarm-messenger.node.test.ts} +13 -23
- package/src/swarm/connection-limiter.test.ts +3 -6
- package/src/swarm/connection.test.ts +63 -38
- package/src/swarm/connection.ts +7 -7
- package/src/swarm/peer.ts +4 -1
- package/src/swarm/swarm.test.ts +10 -12
- package/src/swarm/swarm.ts +16 -3
- package/src/testing/test-builder.ts +14 -29
- package/src/testing/test-wire-protocol.ts +7 -8
- package/src/tests/basic-test-suite.ts +32 -31
- package/src/tests/memory-transport.test.ts +40 -42
- package/src/tests/property-test-suite.ts +21 -22
- package/src/tests/tcp-transport.node.test.ts +65 -0
- package/src/tests/utils.ts +3 -2
- package/src/tests/webrtc-transport.test.ts +10 -10
- package/src/transport/index.ts +1 -5
- package/src/transport/memory-transport.ts +2 -0
- package/src/transport/tcp/index.ts +5 -0
- package/src/transport/{tcp-transport.browser.ts → tcp/tcp-transport.browser.ts} +7 -3
- package/src/transport/{tcp-transport.ts → tcp/tcp-transport.ts} +3 -1
- package/src/transport/transport.ts +8 -7
- package/src/transport/webrtc/index.ts +7 -0
- package/src/transport/webrtc/rtc-connection-factory.ts +82 -0
- package/src/transport/webrtc/rtc-peer-connection.ts +472 -0
- package/src/transport/webrtc/rtc-transport-channel.test.ts +176 -0
- package/src/transport/webrtc/rtc-transport-channel.ts +195 -0
- package/src/transport/webrtc/rtc-transport-factory.ts +28 -0
- package/src/transport/webrtc/rtc-transport-proxy.test.ts +413 -0
- package/src/transport/webrtc/rtc-transport-proxy.ts +264 -0
- package/src/transport/webrtc/rtc-transport-service.ts +192 -0
- package/src/transport/webrtc/rtc-transport-stats.ts +67 -0
- package/src/transport/webrtc/rtc-transport.test.ts +210 -0
- package/src/transport/webrtc/test-utils.ts +22 -0
- package/src/transport/webrtc/utils.ts +36 -0
- package/src/typings.d.ts +8 -2
- package/dist/lib/browser/chunk-XYSYUN63.mjs.map +0 -7
- package/dist/lib/node/chunk-4YAYC7WN.cjs.map +0 -7
- package/dist/types/src/signal/integration.test.d.ts +0 -2
- package/dist/types/src/signal/integration.test.d.ts.map +0 -1
- package/dist/types/src/signal/swarm-messenger.test.d.ts +0 -2
- package/dist/types/src/signal/swarm-messenger.test.d.ts.map +0 -1
- package/dist/types/src/tests/tcp-transport.test.d.ts +0 -2
- package/dist/types/src/tests/tcp-transport.test.d.ts.map +0 -1
- package/dist/types/src/transport/libdatachannel-transport.d.ts +0 -42
- package/dist/types/src/transport/libdatachannel-transport.d.ts.map +0 -1
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts +0 -2
- package/dist/types/src/transport/libdatachannel-transport.test.d.ts.map +0 -1
- package/dist/types/src/transport/memory-transport.test.d.ts +0 -2
- package/dist/types/src/transport/memory-transport.test.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts +0 -2
- package/dist/types/src/transport/simplepeer-simple-peer.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts +0 -2
- package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-transport.d.ts +0 -36
- package/dist/types/src/transport/simplepeer-transport.d.ts.map +0 -1
- package/dist/types/src/transport/simplepeer-transport.test.d.ts +0 -2
- package/dist/types/src/transport/simplepeer-transport.test.d.ts.map +0 -1
- package/dist/types/src/transport/tcp-transport.browser.d.ts.map +0 -1
- package/dist/types/src/transport/tcp-transport.d.ts.map +0 -1
- package/dist/types/src/transport/webrtc.d.ts +0 -6
- package/dist/types/src/transport/webrtc.d.ts.map +0 -1
- package/src/globals.d.ts +0 -7
- package/src/tests/tcp-transport.test.ts +0 -67
- package/src/transport/libdatachannel-transport.test.ts +0 -100
- package/src/transport/libdatachannel-transport.ts +0 -376
- package/src/transport/memory-transport.test.ts +0 -74
- package/src/transport/simplepeer-simple-peer.ts +0 -26
- package/src/transport/simplepeer-transport-proxy-test.ts +0 -181
- package/src/transport/simplepeer-transport-proxy.ts +0 -246
- package/src/transport/simplepeer-transport-service.ts +0 -160
- package/src/transport/simplepeer-transport.test.ts +0 -61
- package/src/transport/simplepeer-transport.ts +0 -250
- package/src/transport/webrtc.ts +0 -15
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Writable } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
import { Event, scheduleTask } from '@dxos/async';
|
|
8
|
+
import { type Stream } from '@dxos/codec-protobuf';
|
|
9
|
+
import { Resource } from '@dxos/context';
|
|
10
|
+
import { ErrorStream } from '@dxos/debug';
|
|
11
|
+
import { invariant } from '@dxos/invariant';
|
|
12
|
+
import { PublicKey } from '@dxos/keys';
|
|
13
|
+
import { log } from '@dxos/log';
|
|
14
|
+
import { ConnectionResetError, ConnectivityError, TimeoutError } from '@dxos/protocols';
|
|
15
|
+
import { type BridgeEvent, type BridgeService, ConnectionState } from '@dxos/protocols/proto/dxos/mesh/bridge';
|
|
16
|
+
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
17
|
+
import { arrayToBuffer } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from '../transport';
|
|
20
|
+
|
|
21
|
+
const RPC_TIMEOUT = 10_000;
|
|
22
|
+
const CLOSE_RPC_TIMEOUT = 3000;
|
|
23
|
+
const RESP_MIN_THRESHOLD = 500;
|
|
24
|
+
|
|
25
|
+
export type RtcTransportProxyOptions = TransportOptions & {
|
|
26
|
+
bridgeService: BridgeService;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class RtcTransportProxy extends Resource implements Transport {
|
|
30
|
+
private readonly _proxyId = PublicKey.random();
|
|
31
|
+
|
|
32
|
+
readonly closed = new Event();
|
|
33
|
+
readonly connected = new Event();
|
|
34
|
+
readonly errors = new ErrorStream();
|
|
35
|
+
|
|
36
|
+
private _serviceStream: Stream<BridgeEvent> | undefined;
|
|
37
|
+
|
|
38
|
+
constructor(private readonly _options: RtcTransportProxyOptions) {
|
|
39
|
+
super();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected override async _open() {
|
|
43
|
+
let stream: Stream<BridgeEvent>;
|
|
44
|
+
try {
|
|
45
|
+
stream = this._options.bridgeService.open(
|
|
46
|
+
{
|
|
47
|
+
proxyId: this._proxyId,
|
|
48
|
+
remotePeerKey: this._options.remotePeerKey,
|
|
49
|
+
ownPeerKey: this._options.ownPeerKey,
|
|
50
|
+
topic: this._options.topic,
|
|
51
|
+
initiator: this._options.initiator ?? false,
|
|
52
|
+
},
|
|
53
|
+
{ timeout: RPC_TIMEOUT },
|
|
54
|
+
);
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
this.errors.raise(error);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this._serviceStream = stream;
|
|
61
|
+
|
|
62
|
+
stream.waitUntilReady().then(
|
|
63
|
+
() => {
|
|
64
|
+
stream.subscribe(
|
|
65
|
+
async (event: BridgeEvent) => {
|
|
66
|
+
log('rtc transport proxy event', event);
|
|
67
|
+
if (event.connection) {
|
|
68
|
+
await this._handleConnection(event.connection);
|
|
69
|
+
} else if (event.data) {
|
|
70
|
+
this._handleData(event.data);
|
|
71
|
+
} else if (event.signal) {
|
|
72
|
+
await this._handleSignal(event.signal);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
(err) => {
|
|
76
|
+
log('rtc bridge stream closed', { err });
|
|
77
|
+
if (err) {
|
|
78
|
+
this._raiseIfOpen(err);
|
|
79
|
+
} else {
|
|
80
|
+
void this.close();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const connectorStream = new Writable({
|
|
86
|
+
write: (chunk, _, callback) => {
|
|
87
|
+
const sendStartMs = Date.now();
|
|
88
|
+
this._options.bridgeService
|
|
89
|
+
.sendData({ proxyId: this._proxyId, payload: chunk }, { timeout: RPC_TIMEOUT })
|
|
90
|
+
.then(
|
|
91
|
+
() => {
|
|
92
|
+
if (Date.now() - sendStartMs > RESP_MIN_THRESHOLD) {
|
|
93
|
+
log('slow response, delaying callback');
|
|
94
|
+
scheduleTask(this._ctx, () => callback(), RESP_MIN_THRESHOLD);
|
|
95
|
+
} else {
|
|
96
|
+
callback();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
(err: any) => {
|
|
100
|
+
callback();
|
|
101
|
+
this._raiseIfOpen(err);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
connectorStream.on('error', (err) => {
|
|
108
|
+
this._raiseIfOpen(err);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this._options.stream.pipe(connectorStream);
|
|
112
|
+
},
|
|
113
|
+
(error) => {
|
|
114
|
+
if (error) {
|
|
115
|
+
this._raiseIfOpen(error);
|
|
116
|
+
} else {
|
|
117
|
+
void this.close();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected override async _close() {
|
|
124
|
+
try {
|
|
125
|
+
await this._serviceStream?.close();
|
|
126
|
+
this._serviceStream = undefined;
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
log.catch(err);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await this._options.bridgeService.close({ proxyId: this._proxyId }, { timeout: CLOSE_RPC_TIMEOUT });
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
log.catch(err);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.closed.emit();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async onSignal(signal: Signal) {
|
|
141
|
+
this._options.bridgeService
|
|
142
|
+
.sendSignal({ proxyId: this._proxyId, signal }, { timeout: RPC_TIMEOUT })
|
|
143
|
+
.catch((err) => this._raiseIfOpen(decodeError(err)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async _handleConnection(connectionEvent: BridgeEvent.ConnectionEvent): Promise<void> {
|
|
147
|
+
if (connectionEvent.error) {
|
|
148
|
+
this.errors.raise(decodeError(connectionEvent.error));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
switch (connectionEvent.state) {
|
|
153
|
+
case ConnectionState.CONNECTED: {
|
|
154
|
+
this.connected.emit();
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case ConnectionState.CLOSED: {
|
|
158
|
+
await this.close();
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _handleData(dataEvent: BridgeEvent.DataEvent) {
|
|
165
|
+
try {
|
|
166
|
+
// NOTE: This must be a Buffer otherwise hypercore-protocol breaks.
|
|
167
|
+
this._options.stream.write(arrayToBuffer(dataEvent.payload));
|
|
168
|
+
} catch (error: any) {
|
|
169
|
+
this._raiseIfOpen(error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async _handleSignal(signalEvent: BridgeEvent.SignalEvent) {
|
|
174
|
+
try {
|
|
175
|
+
await this._options.sendSignal(signalEvent.payload);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const type = signalEvent.payload.payload.data?.type;
|
|
178
|
+
if (type === 'offer' || type === 'answer') {
|
|
179
|
+
this._raiseIfOpen(new ConnectivityError(`Session establishment failed: ${type} couldn't be sent.`));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getDetails(): Promise<string> {
|
|
185
|
+
try {
|
|
186
|
+
const response = await this._options.bridgeService.getDetails(
|
|
187
|
+
{ proxyId: this._proxyId },
|
|
188
|
+
{ timeout: RPC_TIMEOUT },
|
|
189
|
+
);
|
|
190
|
+
return response.details;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return 'bridge-svc unreachable';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getStats(): Promise<TransportStats> {
|
|
197
|
+
try {
|
|
198
|
+
const response = await this._options.bridgeService.getStats({ proxyId: this._proxyId }, { timeout: RPC_TIMEOUT });
|
|
199
|
+
return response.stats as TransportStats;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return {
|
|
202
|
+
bytesSent: 0,
|
|
203
|
+
bytesReceived: 0,
|
|
204
|
+
packetsSent: 0,
|
|
205
|
+
packetsReceived: 0,
|
|
206
|
+
rawStats: 'bridge-svc unreachable',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _raiseIfOpen(error: any) {
|
|
212
|
+
if (this.isOpen) {
|
|
213
|
+
this.errors.raise(error);
|
|
214
|
+
} else {
|
|
215
|
+
log.info('error swallowed because transport was closed', { message: error.message });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Called when underlying proxy service becomes unavailable.
|
|
221
|
+
*/
|
|
222
|
+
forceClose() {
|
|
223
|
+
void this._serviceStream?.close();
|
|
224
|
+
this.closed.emit();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export class RtcTransportProxyFactory implements TransportFactory {
|
|
229
|
+
private _bridgeService: BridgeService | undefined;
|
|
230
|
+
private _connections = new Set<RtcTransportProxy>();
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Sets the current BridgeService to be used to open connections.
|
|
234
|
+
* Calling this method will close any existing connections.
|
|
235
|
+
*/
|
|
236
|
+
setBridgeService(bridgeService: BridgeService | undefined): this {
|
|
237
|
+
this._bridgeService = bridgeService;
|
|
238
|
+
for (const connection of this._connections) {
|
|
239
|
+
connection.forceClose();
|
|
240
|
+
}
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
createTransport(options: TransportOptions): Transport {
|
|
245
|
+
invariant(this._bridgeService, 'RtcTransportProxyFactory is not ready to open connections');
|
|
246
|
+
const transport = new RtcTransportProxy({ ...options, bridgeService: this._bridgeService });
|
|
247
|
+
this._connections.add(transport);
|
|
248
|
+
transport.closed.on(() => this._connections.delete(transport));
|
|
249
|
+
return transport;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const decodeError = (err: Error | string) => {
|
|
254
|
+
const message = typeof err === 'string' ? err : err.message;
|
|
255
|
+
if (message.includes('CONNECTION_RESET')) {
|
|
256
|
+
return new ConnectionResetError(message);
|
|
257
|
+
} else if (message.includes('TIMEOUT')) {
|
|
258
|
+
return new TimeoutError(message);
|
|
259
|
+
} else if (message.includes('CONNECTIVITY_ERROR')) {
|
|
260
|
+
return new ConnectivityError(message);
|
|
261
|
+
} else {
|
|
262
|
+
return typeof err === 'string' ? new Error(err) : err;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Duplex } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
import { Stream } from '@dxos/codec-protobuf';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import {
|
|
12
|
+
type BridgeService,
|
|
13
|
+
type ConnectionRequest,
|
|
14
|
+
type SignalRequest,
|
|
15
|
+
type DataRequest,
|
|
16
|
+
type BridgeEvent,
|
|
17
|
+
ConnectionState,
|
|
18
|
+
type CloseRequest,
|
|
19
|
+
type DetailsRequest,
|
|
20
|
+
type DetailsResponse,
|
|
21
|
+
type StatsRequest,
|
|
22
|
+
type StatsResponse,
|
|
23
|
+
} from '@dxos/protocols/proto/dxos/mesh/bridge';
|
|
24
|
+
import { ComplexMap } from '@dxos/util';
|
|
25
|
+
|
|
26
|
+
import { createRtcTransportFactory } from './rtc-transport-factory';
|
|
27
|
+
import { type IceProvider } from '../../signal';
|
|
28
|
+
import { type Transport, type TransportFactory } from '../transport';
|
|
29
|
+
|
|
30
|
+
type TransportState = {
|
|
31
|
+
proxyId: PublicKey;
|
|
32
|
+
transport: Transport;
|
|
33
|
+
connectorStream: Duplex;
|
|
34
|
+
writeProcessedCallbacks: (() => void)[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class RtcTransportService implements BridgeService {
|
|
38
|
+
private readonly _openTransports = new ComplexMap<PublicKey, TransportState>(PublicKey.hash);
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
webrtcConfig?: RTCConfiguration,
|
|
42
|
+
iceProvider?: IceProvider,
|
|
43
|
+
private readonly _transportFactory: TransportFactory = createRtcTransportFactory(webrtcConfig, iceProvider),
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
public hasOpenTransports() {
|
|
47
|
+
return this._openTransports.size > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
open(request: ConnectionRequest): Stream<BridgeEvent> {
|
|
51
|
+
const existingTransport = this._openTransports.get(request.proxyId);
|
|
52
|
+
if (existingTransport) {
|
|
53
|
+
log.error('requesting a new transport bridge for an existing proxy');
|
|
54
|
+
void this._safeCloseTransport(existingTransport);
|
|
55
|
+
this._openTransports.delete(request.proxyId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Stream<BridgeEvent>(({ ready, next, close }) => {
|
|
59
|
+
const pushNewState = createStateUpdater(next);
|
|
60
|
+
|
|
61
|
+
const transportStream: Duplex = new Duplex({
|
|
62
|
+
read: () => {
|
|
63
|
+
const callbacks = [...transportState.writeProcessedCallbacks];
|
|
64
|
+
transportState.writeProcessedCallbacks.length = 0;
|
|
65
|
+
callbacks.forEach((cb) => cb());
|
|
66
|
+
},
|
|
67
|
+
write: function (chunk, _, callback) {
|
|
68
|
+
next({ data: { payload: chunk } });
|
|
69
|
+
callback();
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const transport = this._transportFactory.createTransport({
|
|
74
|
+
initiator: request.initiator,
|
|
75
|
+
topic: request.topic,
|
|
76
|
+
ownPeerKey: request.ownPeerKey,
|
|
77
|
+
remotePeerKey: request.remotePeerKey,
|
|
78
|
+
stream: transportStream,
|
|
79
|
+
sendSignal: async (signal) => {
|
|
80
|
+
next({ signal: { payload: signal } });
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const transportState: TransportState = {
|
|
85
|
+
proxyId: request.proxyId,
|
|
86
|
+
transport,
|
|
87
|
+
connectorStream: transportStream,
|
|
88
|
+
writeProcessedCallbacks: [],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
pushNewState(ConnectionState.CONNECTING);
|
|
92
|
+
|
|
93
|
+
transport.connected.on(() => pushNewState(ConnectionState.CONNECTED));
|
|
94
|
+
|
|
95
|
+
transport.errors.handle(async (err) => {
|
|
96
|
+
pushNewState(ConnectionState.CLOSED, err);
|
|
97
|
+
void this._safeCloseTransport(transportState);
|
|
98
|
+
close(err);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
transport.closed.on(async () => {
|
|
102
|
+
pushNewState(ConnectionState.CLOSED);
|
|
103
|
+
void this._safeCloseTransport(transportState);
|
|
104
|
+
close();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this._openTransports.set(request.proxyId, transportState);
|
|
108
|
+
|
|
109
|
+
transport.open().catch(async (err) => {
|
|
110
|
+
pushNewState(ConnectionState.CLOSED, err);
|
|
111
|
+
void this._safeCloseTransport(transportState);
|
|
112
|
+
close(err);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ready();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async sendSignal({ proxyId, signal }: SignalRequest): Promise<void> {
|
|
120
|
+
const transport = this._openTransports.get(proxyId);
|
|
121
|
+
invariant(transport);
|
|
122
|
+
|
|
123
|
+
await transport.transport.onSignal(signal);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async getDetails({ proxyId }: DetailsRequest): Promise<DetailsResponse> {
|
|
127
|
+
const transport = this._openTransports.get(proxyId);
|
|
128
|
+
invariant(transport);
|
|
129
|
+
|
|
130
|
+
return { details: await transport.transport.getDetails() };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getStats({ proxyId }: StatsRequest): Promise<StatsResponse> {
|
|
134
|
+
const transport = this._openTransports.get(proxyId);
|
|
135
|
+
invariant(transport);
|
|
136
|
+
|
|
137
|
+
return { stats: await transport.transport.getStats() };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async sendData({ proxyId, payload }: DataRequest): Promise<void> {
|
|
141
|
+
const transport = this._openTransports.get(proxyId);
|
|
142
|
+
invariant(transport);
|
|
143
|
+
|
|
144
|
+
const bufferHasSpace = transport.connectorStream.push(payload);
|
|
145
|
+
if (!bufferHasSpace) {
|
|
146
|
+
await new Promise<void>((resolve) => {
|
|
147
|
+
transport.writeProcessedCallbacks.push(resolve);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async close({ proxyId }: CloseRequest) {
|
|
153
|
+
const transport = this._openTransports.get(proxyId);
|
|
154
|
+
if (!transport) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._openTransports.delete(proxyId);
|
|
159
|
+
await this._safeCloseTransport(transport);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async _safeCloseTransport(transport: TransportState) {
|
|
163
|
+
if (this._openTransports.get(transport.proxyId) === transport) {
|
|
164
|
+
this._openTransports.delete(transport.proxyId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
transport.writeProcessedCallbacks.forEach((cb) => cb());
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await transport.transport.close();
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
log.warn('transport close error', { message: error?.message });
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
transport.connectorStream.end();
|
|
176
|
+
} catch (error: any) {
|
|
177
|
+
log.warn('connectorStream close error', { message: error?.message });
|
|
178
|
+
}
|
|
179
|
+
log('closed');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const createStateUpdater = (next: (event: BridgeEvent) => void) => {
|
|
184
|
+
return (state: ConnectionState, err?: Error) => {
|
|
185
|
+
next({
|
|
186
|
+
connection: {
|
|
187
|
+
state,
|
|
188
|
+
...(err ? { error: err.message } : undefined),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { TransportStats } from '../transport';
|
|
6
|
+
|
|
7
|
+
export const describeSelectedRemoteCandidate = async (connection?: RTCPeerConnection): Promise<string> => {
|
|
8
|
+
const stats = connection && (await getRtcConnectionStats(connection));
|
|
9
|
+
const rc = stats?.remoteCandidate;
|
|
10
|
+
if (!rc) {
|
|
11
|
+
return 'unavailable';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (rc.candidateType === 'relay') {
|
|
15
|
+
return `${rc.ip}:${rc.port} relay for ${rc.relatedAddress}:${rc.relatedPort}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return `${rc.ip}:${rc.port} ${rc.candidateType}`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const createRtcTransportStats = async (
|
|
22
|
+
connection: RTCPeerConnection | undefined,
|
|
23
|
+
topic: string,
|
|
24
|
+
): Promise<TransportStats> => {
|
|
25
|
+
const stats = connection && (await getRtcConnectionStats(connection, topic));
|
|
26
|
+
if (!stats) {
|
|
27
|
+
return {
|
|
28
|
+
bytesSent: 0,
|
|
29
|
+
bytesReceived: 0,
|
|
30
|
+
packetsSent: 0,
|
|
31
|
+
packetsReceived: 0,
|
|
32
|
+
rawStats: {},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
bytesSent: stats.dataChannel?.bytesSent,
|
|
38
|
+
bytesReceived: stats.dataChannel?.bytesReceived,
|
|
39
|
+
packetsSent: 0,
|
|
40
|
+
packetsReceived: 0,
|
|
41
|
+
rawStats: stats.raw,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getRtcConnectionStats = async (connection: RTCPeerConnection, channelTopic?: string): Promise<any> => {
|
|
46
|
+
const stats = await connection.getStats();
|
|
47
|
+
|
|
48
|
+
const statsEntries: [string, any][] = Array.from((stats as any).entries());
|
|
49
|
+
const transport = statsEntries.find(([_, entry]) => entry.type === 'transport')?.[1];
|
|
50
|
+
|
|
51
|
+
const selectedCandidatePair =
|
|
52
|
+
transport && statsEntries.find(([entryId]) => entryId === transport.selectedCandidatePairId)?.[1];
|
|
53
|
+
const remoteCandidate =
|
|
54
|
+
selectedCandidatePair && statsEntries.find(([entryId]) => entryId === selectedCandidatePair.remoteCandidateId)?.[1];
|
|
55
|
+
|
|
56
|
+
const dataChannel =
|
|
57
|
+
channelTopic &&
|
|
58
|
+
statsEntries.find(([_, entry]) => entry.type === 'data-channel' && entry.label === channelTopic)?.[1];
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
transport,
|
|
62
|
+
selectedCandidatePair,
|
|
63
|
+
dataChannel,
|
|
64
|
+
remoteCandidate,
|
|
65
|
+
raw: Object.fromEntries(stats as any),
|
|
66
|
+
};
|
|
67
|
+
};
|