@dxos/network-manager 0.6.12 → 0.6.13-main.041e8aa
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-GW3YM55A.mjs +14 -0
- package/dist/lib/browser/chunk-GW3YM55A.mjs.map +7 -0
- package/dist/lib/browser/{chunk-XYSYUN63.mjs → chunk-MKIVP7G3.mjs} +1249 -1065
- package/dist/lib/browser/chunk-MKIVP7G3.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +10 -19
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +22 -32
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/transport/tcp/index.mjs +39 -0
- package/dist/lib/browser/transport/tcp/index.mjs.map +7 -0
- package/dist/lib/node/{chunk-4YAYC7WN.cjs → chunk-D6P7ACEM.cjs} +1262 -1205
- package/dist/lib/node/chunk-D6P7ACEM.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 +24 -34
- 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-22DA2US6.mjs +4373 -0
- package/dist/lib/node-esm/chunk-22DA2US6.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 +279 -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/swarm.d.ts +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 +55 -30
- 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 +5 -5
- package/src/swarm/swarm.test.ts +10 -12
- package/src/swarm/swarm.ts +1 -1
- package/src/testing/test-builder.ts +13 -29
- package/src/testing/test-wire-protocol.ts +1 -4
- package/src/tests/basic-test-suite.ts +34 -33
- 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 +9 -9
- 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 +198 -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
|
@@ -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, type TransportStats } from '
|
|
8
|
+
import { type Transport, type TransportFactory, type TransportStats } from '../transport';
|
|
9
9
|
|
|
10
10
|
export const TcpTransportFactory: TransportFactory = {
|
|
11
11
|
createTransport: () => new TcpTransport(),
|
|
@@ -23,9 +23,13 @@ export class TcpTransport implements Transport {
|
|
|
23
23
|
return true;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async open() {
|
|
26
|
+
async open() {
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
27
29
|
|
|
28
|
-
async close() {
|
|
30
|
+
async close() {
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
async onSignal() {
|
|
31
35
|
throw new Error('Method not implemented.');
|
|
@@ -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, type TransportStats } from '
|
|
12
|
+
import { type Transport, type TransportFactory, type TransportOptions, type TransportStats } from '../transport';
|
|
13
13
|
|
|
14
14
|
export const TcpTransportFactory: TransportFactory = {
|
|
15
15
|
createTransport: (options) => new TcpTransport(options),
|
|
@@ -72,6 +72,7 @@ export class TcpTransport implements Transport {
|
|
|
72
72
|
this._server.listen(0);
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
return this;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async close() {
|
|
@@ -79,6 +80,7 @@ export class TcpTransport implements Transport {
|
|
|
79
80
|
this._socket?.destroy();
|
|
80
81
|
this._server?.close();
|
|
81
82
|
this._closed = true;
|
|
83
|
+
return this;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
async onSignal({ payload }: Signal) {
|
|
@@ -8,9 +8,8 @@ import { type PublicKey } from '@dxos/keys';
|
|
|
8
8
|
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
9
9
|
|
|
10
10
|
export enum TransportKind {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
LIBDATACHANNEL = 'LIBDATACHANNEL',
|
|
11
|
+
WEB_RTC = 'WEB-RTC',
|
|
12
|
+
WEB_RTC_PROXY = 'WEB-RTC_PROXY',
|
|
14
13
|
MEMORY = 'MEMORY',
|
|
15
14
|
TCP = 'TCP',
|
|
16
15
|
}
|
|
@@ -25,10 +24,8 @@ export interface Transport {
|
|
|
25
24
|
connected: Event;
|
|
26
25
|
errors: ErrorStream;
|
|
27
26
|
|
|
28
|
-
open(): Promise<
|
|
29
|
-
close(): Promise<
|
|
30
|
-
|
|
31
|
-
get isOpen(): boolean;
|
|
27
|
+
open(): Promise<this>;
|
|
28
|
+
close(): Promise<this>;
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
31
|
* Handle message from signaling.
|
|
@@ -50,6 +47,10 @@ export interface Transport {
|
|
|
50
47
|
* Common options for all transports.
|
|
51
48
|
*/
|
|
52
49
|
export type TransportOptions = {
|
|
50
|
+
ownPeerKey: string;
|
|
51
|
+
remotePeerKey: string;
|
|
52
|
+
|
|
53
|
+
topic: string;
|
|
53
54
|
/**
|
|
54
55
|
* Did local node initiate this connection.
|
|
55
56
|
*/
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Mutex } from '@dxos/async';
|
|
6
|
+
|
|
7
|
+
export type ConnectionInfo = {
|
|
8
|
+
initiator: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface RtcConnectionFactory {
|
|
12
|
+
initialize(): Promise<void>;
|
|
13
|
+
onConnectionDestroyed(): Promise<void>;
|
|
14
|
+
createConnection(config: RTCConfiguration): Promise<RTCPeerConnection>;
|
|
15
|
+
initConnection(connection: RTCPeerConnection, info: ConnectionInfo): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Use built-in browser RTCPeerConnection.
|
|
20
|
+
*/
|
|
21
|
+
class BrowserRtcConnectionFactory implements RtcConnectionFactory {
|
|
22
|
+
async initialize() {}
|
|
23
|
+
async onConnectionDestroyed() {}
|
|
24
|
+
|
|
25
|
+
async createConnection(config: RTCConfiguration) {
|
|
26
|
+
return new RTCPeerConnection(config);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async initConnection(connection: RTCPeerConnection, info: ConnectionInfo): Promise<void> {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Use `node-datachannel` polyfill.
|
|
34
|
+
* https://github.com/paullouisageneau/libdatachannel
|
|
35
|
+
*/
|
|
36
|
+
class NodeRtcConnectionFactory implements RtcConnectionFactory {
|
|
37
|
+
private static _createdConnections = 0;
|
|
38
|
+
private static _cleanupMutex = new Mutex();
|
|
39
|
+
|
|
40
|
+
// This should be inside the function to avoid triggering `eval` in the global scope.
|
|
41
|
+
// eslint-disable-next-line no-new-func
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Do imports here?
|
|
44
|
+
async initialize() {}
|
|
45
|
+
async onConnectionDestroyed() {
|
|
46
|
+
return NodeRtcConnectionFactory._cleanupMutex.executeSynchronized(async () => {
|
|
47
|
+
if (--NodeRtcConnectionFactory._createdConnections === 0) {
|
|
48
|
+
(await import('#node-datachannel')).cleanup();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async createConnection(config: RTCConfiguration) {
|
|
54
|
+
return NodeRtcConnectionFactory._cleanupMutex.executeSynchronized(async () => {
|
|
55
|
+
const { RTCPeerConnection } = await import('#node-datachannel/polyfill');
|
|
56
|
+
NodeRtcConnectionFactory._createdConnections++;
|
|
57
|
+
return new RTCPeerConnection(config);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async initConnection(connection: RTCPeerConnection, info: ConnectionInfo): Promise<void> {
|
|
62
|
+
// Initiator peer is responsible for data-channel creation. This triggers the callback in browsers.
|
|
63
|
+
// In node-datachannel/polyfill createOffer() / setLocalDescription(offer) are no-ops, the process
|
|
64
|
+
// is handled by c++ implementation when a data-channel gets created.
|
|
65
|
+
// By calling the method here we'll start waiting for an offer promise that'll resolve on data-channel creation
|
|
66
|
+
// at which point we'll need to send an SDP to a remote peer.
|
|
67
|
+
// https://github.com/murat-dogan/node-datachannel/blob/master/polyfill/RTCPeerConnection.js#L452C1-L459C6
|
|
68
|
+
//
|
|
69
|
+
if (info.initiator) {
|
|
70
|
+
connection.onnegotiationneeded?.(null as any);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create platform-specific connection factory.
|
|
77
|
+
*/
|
|
78
|
+
export const getRtcConnectionFactory = (): RtcConnectionFactory => {
|
|
79
|
+
return typeof (globalThis as any).RTCPeerConnection === 'undefined'
|
|
80
|
+
? new NodeRtcConnectionFactory()
|
|
81
|
+
: new BrowserRtcConnectionFactory();
|
|
82
|
+
};
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { synchronized, Trigger, Mutex } from '@dxos/async';
|
|
6
|
+
import { invariant } from '@dxos/invariant';
|
|
7
|
+
import { log, logInfo } from '@dxos/log';
|
|
8
|
+
import { ConnectivityError } from '@dxos/protocols';
|
|
9
|
+
import { type Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
10
|
+
import { trace } from '@dxos/tracing';
|
|
11
|
+
|
|
12
|
+
import { type RtcConnectionFactory } from './rtc-connection-factory';
|
|
13
|
+
import { RtcTransportChannel } from './rtc-transport-channel';
|
|
14
|
+
import { areSdpEqual, chooseInitiatorPeer } from './utils';
|
|
15
|
+
import type { IceProvider } from '../../signal';
|
|
16
|
+
import { type TransportOptions } from '../transport';
|
|
17
|
+
|
|
18
|
+
export type RtcPeerChannelFactoryOptions = {
|
|
19
|
+
ownPeerKey: string;
|
|
20
|
+
remotePeerKey: string;
|
|
21
|
+
/**
|
|
22
|
+
* Sends signal message to remote peer.
|
|
23
|
+
*/
|
|
24
|
+
sendSignal: (signal: Signal) => Promise<void>;
|
|
25
|
+
|
|
26
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers
|
|
27
|
+
webrtcConfig?: RTCConfiguration;
|
|
28
|
+
iceProvider?: IceProvider;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A factory for rtc Transport implementations for a particular peer.
|
|
33
|
+
* Contains WebRTC connection establishment logic.
|
|
34
|
+
* When the first Transport is opened a connection is established and kept until all the transports are closed.
|
|
35
|
+
*/
|
|
36
|
+
@trace.resource()
|
|
37
|
+
export class RtcPeerConnection {
|
|
38
|
+
// A peer who is not the initiator waits for another party to open a channel.
|
|
39
|
+
private readonly _channelCreatedCallbacks = new Map<string, ChannelCreatedCallback>();
|
|
40
|
+
// Channels indexed by topic.
|
|
41
|
+
private readonly _transportChannels = new Map<string, RtcTransportChannel>();
|
|
42
|
+
private readonly _dataChannels = new Map<string, RTCDataChannel>();
|
|
43
|
+
// A peer is ready to receive ICE candidates when local and remote description were set.
|
|
44
|
+
private readonly _readyForCandidates = new Trigger();
|
|
45
|
+
|
|
46
|
+
private readonly _offerProcessingMutex = new Mutex();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Can't use peer.connection.initiator, because if two connections to the same peer are created in
|
|
50
|
+
* different swarms, we might be the initiator of the first one, but not of the other one.
|
|
51
|
+
* Use a stable peer keypair property (key ordering) to decide who's acting as the initiator of
|
|
52
|
+
* transport connection establishment and data channel creation.
|
|
53
|
+
*/
|
|
54
|
+
private readonly _initiator: boolean;
|
|
55
|
+
|
|
56
|
+
private _connection?: RTCPeerConnection;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly _factory: RtcConnectionFactory,
|
|
60
|
+
private readonly _options: RtcPeerChannelFactoryOptions,
|
|
61
|
+
) {
|
|
62
|
+
this._initiator = chooseInitiatorPeer(_options.ownPeerKey, _options.remotePeerKey) === _options.ownPeerKey;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public get transportChannelCount() {
|
|
66
|
+
return this._transportChannels.size;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public get currentConnection(): RTCPeerConnection | undefined {
|
|
70
|
+
return this._connection;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async createDataChannel(topic: string): Promise<RTCDataChannel> {
|
|
74
|
+
const connection = await this._openConnection();
|
|
75
|
+
if (!this._transportChannels.has(topic)) {
|
|
76
|
+
if (!this._transportChannels.size) {
|
|
77
|
+
this._lockAndCloseConnection();
|
|
78
|
+
}
|
|
79
|
+
throw new Error('Transport closed while connection was being open');
|
|
80
|
+
}
|
|
81
|
+
if (this._initiator) {
|
|
82
|
+
const channel = connection.createDataChannel(topic);
|
|
83
|
+
this._dataChannels.set(topic, channel);
|
|
84
|
+
return channel;
|
|
85
|
+
} else {
|
|
86
|
+
const existingChannel = this._dataChannels.get(topic);
|
|
87
|
+
if (existingChannel) {
|
|
88
|
+
return existingChannel;
|
|
89
|
+
}
|
|
90
|
+
log('waiting for initiator-peer to open a data channel');
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
this._channelCreatedCallbacks.set(topic, { resolve, reject });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public createTransportChannel(options: TransportOptions): RtcTransportChannel {
|
|
98
|
+
const channel = new RtcTransportChannel(this, options);
|
|
99
|
+
this._transportChannels.set(options.topic, channel);
|
|
100
|
+
channel.closed.on(() => {
|
|
101
|
+
this._transportChannels.delete(options.topic);
|
|
102
|
+
if (this._transportChannels.size === 0) {
|
|
103
|
+
this._lockAndCloseConnection();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return channel;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@synchronized
|
|
110
|
+
private async _openConnection(): Promise<RTCPeerConnection> {
|
|
111
|
+
if (this._connection) {
|
|
112
|
+
return this._connection;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log('initializing connection...', () => ({ remotePeer: this._options.remotePeerKey }));
|
|
116
|
+
|
|
117
|
+
const config = await this._loadConnectionConfig();
|
|
118
|
+
|
|
119
|
+
//
|
|
120
|
+
// Peer connection.
|
|
121
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Connectivity
|
|
122
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection
|
|
123
|
+
//
|
|
124
|
+
const connection = await this._factory.createConnection(config);
|
|
125
|
+
|
|
126
|
+
const iceCandidateErrors: IceCandidateErrorDetails[] = [];
|
|
127
|
+
|
|
128
|
+
Object.assign<RTCPeerConnection, Partial<RTCPeerConnection>>(connection, {
|
|
129
|
+
onnegotiationneeded: async () => {
|
|
130
|
+
invariant(this._initiator);
|
|
131
|
+
|
|
132
|
+
if (connection !== this._connection) {
|
|
133
|
+
this._onConnectionCallbackAfterClose('onnegotiationneeded', connection);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
log('onnegotiationneeded');
|
|
138
|
+
try {
|
|
139
|
+
const offer = await connection.createOffer();
|
|
140
|
+
await connection.setLocalDescription(offer);
|
|
141
|
+
await this._sendDescription(connection, offer);
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
this._lockAndAbort(connection, err);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// When ICE candidate identified (should be sent to remote peer) and when ICE gathering finalized.
|
|
148
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icecandidate_event
|
|
149
|
+
onicecandidate: async (event) => {
|
|
150
|
+
if (connection !== this._connection) {
|
|
151
|
+
this._onConnectionCallbackAfterClose('onicecandidate', connection);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (event.candidate) {
|
|
156
|
+
log('onicecandidate', { candidate: event.candidate.candidate });
|
|
157
|
+
await this._sendIceCandidate(event.candidate);
|
|
158
|
+
} else {
|
|
159
|
+
log('onicecandidate gathering complete');
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// When error occurs while performing ICE negotiations through a STUN or TURN server.
|
|
164
|
+
// It's ok for some candidates to fail if a working pair is eventually found.
|
|
165
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/icecandidateerror_event
|
|
166
|
+
onicecandidateerror: (event: any) => {
|
|
167
|
+
const { url, errorCode, errorText } = event as RTCPeerConnectionIceErrorEvent;
|
|
168
|
+
iceCandidateErrors.push({ url, errorCode, errorText });
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// When possible error during ICE gathering.
|
|
172
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceconnectionstatechange_event
|
|
173
|
+
oniceconnectionstatechange: () => {
|
|
174
|
+
if (connection !== this._connection) {
|
|
175
|
+
this._onConnectionCallbackAfterClose('oniceconnectionstatechange', connection);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log('oniceconnectionstatechange', { state: connection.iceConnectionState });
|
|
180
|
+
if (connection.iceConnectionState === 'failed') {
|
|
181
|
+
this._lockAndAbort(connection, createIceFailureError(iceCandidateErrors));
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// When new track (or channel) is added.
|
|
186
|
+
// State: { new, connecting, connected, disconnected, failed, closed }
|
|
187
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
|
|
188
|
+
onconnectionstatechange: () => {
|
|
189
|
+
if (connection !== this._connection) {
|
|
190
|
+
if (connection.connectionState !== 'closed' && connection.connectionState !== 'failed') {
|
|
191
|
+
this._onConnectionCallbackAfterClose('onconnectionstatechange', connection);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
log('onconnectionstatechange', { state: connection.connectionState });
|
|
197
|
+
if (connection.connectionState === 'failed') {
|
|
198
|
+
this._lockAndAbort(connection, new Error('Connection failed.'));
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
onsignalingstatechange: () => {
|
|
203
|
+
log('onsignalingstatechange', { state: connection.signalingState });
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// When channel is added to connection.
|
|
207
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/datachannel_event
|
|
208
|
+
ondatachannel: (event) => {
|
|
209
|
+
invariant(!this._initiator, 'Initiator is expected to create data channels.');
|
|
210
|
+
|
|
211
|
+
if (connection !== this._connection) {
|
|
212
|
+
this._onConnectionCallbackAfterClose('ondatachannel', connection);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log('ondatachannel', { label: event.channel.label });
|
|
217
|
+
this._dataChannels.set(event.channel.label, event.channel);
|
|
218
|
+
const pendingCallback = this._channelCreatedCallbacks.get(event.channel.label);
|
|
219
|
+
if (pendingCallback) {
|
|
220
|
+
this._channelCreatedCallbacks.delete(event.channel.label);
|
|
221
|
+
pendingCallback.resolve(event.channel);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
this._connection = connection;
|
|
227
|
+
this._readyForCandidates.reset();
|
|
228
|
+
|
|
229
|
+
await this._factory.initConnection(connection, { initiator: this._initiator });
|
|
230
|
+
|
|
231
|
+
return this._connection;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@synchronized
|
|
235
|
+
private _lockAndAbort(connection: RTCPeerConnection, error: Error) {
|
|
236
|
+
this._abortConnection(connection, error);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _abortConnection(connection: RTCPeerConnection, error: Error) {
|
|
240
|
+
if (connection !== this._connection) {
|
|
241
|
+
log.error('attempted to abort an inactive connection', { error });
|
|
242
|
+
this._safeCloseConnection(connection);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const [topic, pendingCallback] of this._channelCreatedCallbacks.entries()) {
|
|
246
|
+
pendingCallback.reject(error);
|
|
247
|
+
this._transportChannels.delete(topic);
|
|
248
|
+
}
|
|
249
|
+
this._channelCreatedCallbacks.clear();
|
|
250
|
+
for (const channel of this._transportChannels.values()) {
|
|
251
|
+
channel.onConnectionError(error);
|
|
252
|
+
}
|
|
253
|
+
this._transportChannels.clear();
|
|
254
|
+
this._safeCloseConnection();
|
|
255
|
+
log('connection aborted', { reason: error.message });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@synchronized
|
|
259
|
+
private _lockAndCloseConnection() {
|
|
260
|
+
invariant(this._transportChannels.size === 0);
|
|
261
|
+
if (this._connection) {
|
|
262
|
+
this._safeCloseConnection();
|
|
263
|
+
log('connection closed');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@synchronized
|
|
268
|
+
public async onSignal(signal: Signal) {
|
|
269
|
+
const connection = this._connection;
|
|
270
|
+
if (!connection) {
|
|
271
|
+
log.warn('a signal ignored because the connection was closed', { type: signal.payload.data.type });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const data = signal.payload.data;
|
|
276
|
+
switch (data.type) {
|
|
277
|
+
case 'offer': {
|
|
278
|
+
await this._offerProcessingMutex.executeSynchronized(async () => {
|
|
279
|
+
if (isRemoteDescriptionSet(connection, data)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (connection.connectionState !== 'new') {
|
|
283
|
+
this._abortConnection(connection, new Error(`Received an offer in ${connection.connectionState}.`));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await connection.setRemoteDescription({ type: data.type, sdp: data.sdp });
|
|
289
|
+
const answer = await connection.createAnswer();
|
|
290
|
+
await connection.setLocalDescription(answer);
|
|
291
|
+
await this._sendDescription(connection, answer);
|
|
292
|
+
this._onSessionNegotiated(connection);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this._abortConnection(connection, new Error('Error handling a remote offer.', { cause: err }));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case 'answer':
|
|
301
|
+
await this._offerProcessingMutex.executeSynchronized(async () => {
|
|
302
|
+
try {
|
|
303
|
+
if (isRemoteDescriptionSet(connection, data)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (connection.signalingState !== 'have-local-offer') {
|
|
307
|
+
this._abortConnection(
|
|
308
|
+
connection,
|
|
309
|
+
new Error(`Unexpected answer from remote peer, signalingState was ${connection.signalingState}.`),
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await connection.setRemoteDescription({ type: data.type, sdp: data.sdp });
|
|
314
|
+
this._onSessionNegotiated(connection);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
this._abortConnection(connection, new Error('Error handling a remote answer.', { cause: err }));
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'candidate':
|
|
322
|
+
void this._processIceCandidate(connection, data.candidate);
|
|
323
|
+
break;
|
|
324
|
+
|
|
325
|
+
default:
|
|
326
|
+
this._abortConnection(connection, new Error(`Unknown signal type ${data.type}.`));
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
log('signal processed');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async _processIceCandidate(connection: RTCPeerConnection, candidate: RTCIceCandidate) {
|
|
334
|
+
try {
|
|
335
|
+
// ICE candidates are associated with a session, so we need to wait for the remote description to be set.
|
|
336
|
+
await this._readyForCandidates.wait();
|
|
337
|
+
if (connection === this._connection) {
|
|
338
|
+
log('adding ice candidate', { candidate });
|
|
339
|
+
await connection.addIceCandidate(candidate);
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
log.catch(err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private _onSessionNegotiated(connection: RTCPeerConnection) {
|
|
347
|
+
if (connection === this._connection) {
|
|
348
|
+
log('ready to process ice candidates');
|
|
349
|
+
this._readyForCandidates.wake();
|
|
350
|
+
} else {
|
|
351
|
+
log.warn('session was negotiated after connection became inactive');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private _onConnectionCallbackAfterClose(callback: string, connection: RTCPeerConnection) {
|
|
356
|
+
log.warn('callback invoked after a connection was destroyed, this is probably a bug', {
|
|
357
|
+
callback,
|
|
358
|
+
state: connection.connectionState,
|
|
359
|
+
});
|
|
360
|
+
this._safeCloseConnection(connection);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private _safeCloseConnection(connection: RTCPeerConnection | undefined = this._connection) {
|
|
364
|
+
const resetFields = this._connection && connection === this._connection;
|
|
365
|
+
try {
|
|
366
|
+
connection?.close();
|
|
367
|
+
} catch (err) {
|
|
368
|
+
log.catch(err);
|
|
369
|
+
}
|
|
370
|
+
if (resetFields) {
|
|
371
|
+
this._connection = undefined;
|
|
372
|
+
this._dataChannels.clear();
|
|
373
|
+
this._readyForCandidates.wake();
|
|
374
|
+
void this._factory.onConnectionDestroyed().catch((err) => log.catch(err));
|
|
375
|
+
for (const [_, pendingCallback] of this._channelCreatedCallbacks.entries()) {
|
|
376
|
+
pendingCallback.reject('Connection closed.');
|
|
377
|
+
}
|
|
378
|
+
this._channelCreatedCallbacks.clear();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async _loadConnectionConfig() {
|
|
383
|
+
const config = { ...this._options.webrtcConfig };
|
|
384
|
+
try {
|
|
385
|
+
const providedIceServers = (await this._options.iceProvider?.getIceServers()) ?? [];
|
|
386
|
+
if (providedIceServers.length > 0) {
|
|
387
|
+
config.iceServers = [...(config.iceServers ?? []), ...providedIceServers];
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
log.catch(error);
|
|
391
|
+
}
|
|
392
|
+
return config;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async _sendIceCandidate(candidate: RTCIceCandidate) {
|
|
396
|
+
try {
|
|
397
|
+
await this._options.sendSignal({
|
|
398
|
+
payload: {
|
|
399
|
+
data: {
|
|
400
|
+
type: 'candidate',
|
|
401
|
+
candidate: {
|
|
402
|
+
candidate: candidate.candidate,
|
|
403
|
+
// These fields never seem to be not null, but connecting to Chrome doesn't work if they are.
|
|
404
|
+
sdpMLineIndex: candidate.sdpMLineIndex ?? '0',
|
|
405
|
+
sdpMid: candidate.sdpMid ?? '0',
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log.warn('signaling error', { err });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private async _sendDescription(connection: RTCPeerConnection, description: RTCSessionDescriptionInit) {
|
|
416
|
+
if (connection !== this._connection) {
|
|
417
|
+
// Connection was closed while description was being created.
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// Type is 'offer' | 'answer'.
|
|
421
|
+
const data = { type: description.type, sdp: description.sdp };
|
|
422
|
+
await this._options.sendSignal({ payload: { data } });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@trace.info()
|
|
426
|
+
protected get _connectionInfo() {
|
|
427
|
+
const connectionInfo = this._connection && {
|
|
428
|
+
connectionState: this._connection.connectionState,
|
|
429
|
+
iceConnectionState: this._connection.iceConnectionState,
|
|
430
|
+
iceGatheringState: this._connection.iceGatheringState,
|
|
431
|
+
signalingState: this._connection.signalingState,
|
|
432
|
+
remoteDescription: this._connection.remoteDescription,
|
|
433
|
+
localDescription: this._connection.localDescription,
|
|
434
|
+
};
|
|
435
|
+
return {
|
|
436
|
+
...connectionInfo,
|
|
437
|
+
ts: Date.now(),
|
|
438
|
+
remotePeerKey: this._options.remotePeerKey,
|
|
439
|
+
channels: [...this._transportChannels.keys()].map((topic) => topic),
|
|
440
|
+
config: this._connection?.getConfiguration(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@logInfo
|
|
445
|
+
private get _loggerContext() {
|
|
446
|
+
return {
|
|
447
|
+
ownPeerKey: this._options.ownPeerKey,
|
|
448
|
+
remotePeerKey: this._options.remotePeerKey,
|
|
449
|
+
initiator: this._initiator,
|
|
450
|
+
channels: this._transportChannels.size,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const isRemoteDescriptionSet = (connection: RTCPeerConnection, data: { type: string; sdp: string }) => {
|
|
456
|
+
if (!connection.remoteDescription?.type || connection.remoteDescription?.type !== data.type) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return areSdpEqual(connection.remoteDescription.sdp, data.sdp);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
type IceCandidateErrorDetails = { url: string; errorCode: number; errorText: string };
|
|
463
|
+
|
|
464
|
+
const createIceFailureError = (details: IceCandidateErrorDetails[]) => {
|
|
465
|
+
const candidateErrors = details.map(({ url, errorCode, errorText }) => `${errorCode} ${url}: ${errorText}`);
|
|
466
|
+
return new ConnectivityError(`ICE failed:\n${candidateErrors.join('\n')}`);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
type ChannelCreatedCallback = {
|
|
470
|
+
resolve: (channel: RTCDataChannel) => void;
|
|
471
|
+
reject: (reason?: any) => void;
|
|
472
|
+
};
|