@dxos/network-manager 0.6.13 → 0.6.14-main.69511f5

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.
Files changed (161) hide show
  1. package/dist/lib/browser/{chunk-XYSYUN63.mjs → chunk-RUNQZNCV.mjs} +1247 -1066
  2. package/dist/lib/browser/chunk-RUNQZNCV.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +9 -19
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +20 -33
  6. package/dist/lib/browser/testing/index.mjs.map +3 -3
  7. package/dist/lib/browser/transport/tcp/index.mjs +38 -0
  8. package/dist/lib/browser/transport/tcp/index.mjs.map +7 -0
  9. package/dist/lib/node/{chunk-4YAYC7WN.cjs → chunk-D6P7ACEM.cjs} +1262 -1205
  10. package/dist/lib/node/chunk-D6P7ACEM.cjs.map +7 -0
  11. package/dist/lib/node/index.cjs +27 -37
  12. package/dist/lib/node/index.cjs.map +2 -2
  13. package/dist/lib/node/meta.json +1 -1
  14. package/dist/lib/node/testing/index.cjs +24 -34
  15. package/dist/lib/node/testing/index.cjs.map +3 -3
  16. package/dist/lib/node/transport/tcp/index.cjs +191 -0
  17. package/dist/lib/node/transport/tcp/index.cjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-22DA2US6.mjs +4373 -0
  19. package/dist/lib/node-esm/chunk-22DA2US6.mjs.map +7 -0
  20. package/dist/lib/node-esm/index.mjs +50 -0
  21. package/dist/lib/node-esm/index.mjs.map +7 -0
  22. package/dist/lib/node-esm/meta.json +1 -0
  23. package/dist/lib/node-esm/testing/index.mjs +279 -0
  24. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  25. package/dist/lib/node-esm/transport/tcp/index.mjs +159 -0
  26. package/dist/lib/node-esm/transport/tcp/index.mjs.map +7 -0
  27. package/dist/types/src/network-manager.d.ts +2 -1
  28. package/dist/types/src/network-manager.d.ts.map +1 -1
  29. package/dist/types/src/signal/ice.d.ts.map +1 -1
  30. package/dist/types/src/signal/integration.node.test.d.ts +2 -0
  31. package/dist/types/src/signal/integration.node.test.d.ts.map +1 -0
  32. package/dist/types/src/signal/swarm-messenger.node.test.d.ts +2 -0
  33. package/dist/types/src/signal/swarm-messenger.node.test.d.ts.map +1 -0
  34. package/dist/types/src/swarm/connection.d.ts.map +1 -1
  35. package/dist/types/src/swarm/swarm.d.ts +1 -1
  36. package/dist/types/src/testing/test-builder.d.ts +2 -2
  37. package/dist/types/src/testing/test-builder.d.ts.map +1 -1
  38. package/dist/types/src/testing/test-wire-protocol.d.ts +1 -2
  39. package/dist/types/src/testing/test-wire-protocol.d.ts.map +1 -1
  40. package/dist/types/src/tests/basic-test-suite.d.ts.map +1 -1
  41. package/dist/types/src/tests/property-test-suite.d.ts.map +1 -1
  42. package/dist/types/src/tests/tcp-transport.node.test.d.ts +2 -0
  43. package/dist/types/src/tests/tcp-transport.node.test.d.ts.map +1 -0
  44. package/dist/types/src/tests/utils.d.ts.map +1 -1
  45. package/dist/types/src/transport/index.d.ts +1 -5
  46. package/dist/types/src/transport/index.d.ts.map +1 -1
  47. package/dist/types/src/transport/memory-transport.d.ts +2 -2
  48. package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
  49. package/dist/types/src/transport/tcp/index.d.ts +2 -0
  50. package/dist/types/src/transport/tcp/index.d.ts.map +1 -0
  51. package/dist/types/src/transport/{tcp-transport.browser.d.ts → tcp/tcp-transport.browser.d.ts} +3 -3
  52. package/dist/types/src/transport/tcp/tcp-transport.browser.d.ts.map +1 -0
  53. package/dist/types/src/transport/{tcp-transport.d.ts → tcp/tcp-transport.d.ts} +3 -3
  54. package/dist/types/src/transport/tcp/tcp-transport.d.ts.map +1 -0
  55. package/dist/types/src/transport/transport.d.ts +7 -6
  56. package/dist/types/src/transport/transport.d.ts.map +1 -1
  57. package/dist/types/src/transport/webrtc/index.d.ts +4 -0
  58. package/dist/types/src/transport/webrtc/index.d.ts.map +1 -0
  59. package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts +14 -0
  60. package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts.map +1 -0
  61. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts +68 -0
  62. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts.map +1 -0
  63. package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts +33 -0
  64. package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts.map +1 -0
  65. package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts +2 -0
  66. package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts.map +1 -0
  67. package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts +4 -0
  68. package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts.map +1 -0
  69. package/dist/types/src/transport/{simplepeer-transport-proxy.d.ts → webrtc/rtc-transport-proxy.d.ts} +10 -12
  70. package/dist/types/src/transport/webrtc/rtc-transport-proxy.d.ts.map +1 -0
  71. package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts +2 -0
  72. package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts.map +1 -0
  73. package/dist/types/src/transport/{simplepeer-transport-service.d.ts → webrtc/rtc-transport-service.d.ts} +9 -7
  74. package/dist/types/src/transport/webrtc/rtc-transport-service.d.ts.map +1 -0
  75. package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts +4 -0
  76. package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts.map +1 -0
  77. package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts +2 -0
  78. package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts.map +1 -0
  79. package/dist/types/src/transport/webrtc/test-utils.d.ts +5 -0
  80. package/dist/types/src/transport/webrtc/test-utils.d.ts.map +1 -0
  81. package/dist/types/src/transport/webrtc/utils.d.ts +3 -0
  82. package/dist/types/src/transport/webrtc/utils.d.ts.map +1 -0
  83. package/package.json +53 -36
  84. package/src/network-manager.ts +5 -13
  85. package/src/signal/ice.test.ts +1 -3
  86. package/src/signal/ice.ts +6 -1
  87. package/src/signal/{integration.test.ts → integration.node.test.ts} +9 -15
  88. package/src/signal/{swarm-messenger.test.ts → swarm-messenger.node.test.ts} +13 -23
  89. package/src/swarm/connection-limiter.test.ts +3 -6
  90. package/src/swarm/connection.test.ts +63 -38
  91. package/src/swarm/connection.ts +5 -5
  92. package/src/swarm/swarm.test.ts +10 -12
  93. package/src/swarm/swarm.ts +1 -1
  94. package/src/testing/test-builder.ts +13 -29
  95. package/src/testing/test-wire-protocol.ts +1 -4
  96. package/src/tests/basic-test-suite.ts +34 -33
  97. package/src/tests/memory-transport.test.ts +40 -42
  98. package/src/tests/property-test-suite.ts +21 -22
  99. package/src/tests/tcp-transport.node.test.ts +65 -0
  100. package/src/tests/utils.ts +3 -2
  101. package/src/tests/webrtc-transport.test.ts +9 -9
  102. package/src/transport/index.ts +1 -5
  103. package/src/transport/memory-transport.ts +2 -0
  104. package/src/transport/tcp/index.ts +5 -0
  105. package/src/transport/{tcp-transport.browser.ts → tcp/tcp-transport.browser.ts} +7 -3
  106. package/src/transport/{tcp-transport.ts → tcp/tcp-transport.ts} +3 -1
  107. package/src/transport/transport.ts +8 -7
  108. package/src/transport/webrtc/index.ts +7 -0
  109. package/src/transport/webrtc/rtc-connection-factory.ts +82 -0
  110. package/src/transport/webrtc/rtc-peer-connection.ts +472 -0
  111. package/src/transport/webrtc/rtc-transport-channel.test.ts +176 -0
  112. package/src/transport/webrtc/rtc-transport-channel.ts +195 -0
  113. package/src/transport/webrtc/rtc-transport-factory.ts +28 -0
  114. package/src/transport/webrtc/rtc-transport-proxy.test.ts +413 -0
  115. package/src/transport/webrtc/rtc-transport-proxy.ts +264 -0
  116. package/src/transport/webrtc/rtc-transport-service.ts +192 -0
  117. package/src/transport/webrtc/rtc-transport-stats.ts +67 -0
  118. package/src/transport/webrtc/rtc-transport.test.ts +198 -0
  119. package/src/transport/webrtc/test-utils.ts +22 -0
  120. package/src/transport/webrtc/utils.ts +36 -0
  121. package/src/typings.d.ts +8 -2
  122. package/dist/lib/browser/chunk-XYSYUN63.mjs.map +0 -7
  123. package/dist/lib/node/chunk-4YAYC7WN.cjs.map +0 -7
  124. package/dist/types/src/signal/integration.test.d.ts +0 -2
  125. package/dist/types/src/signal/integration.test.d.ts.map +0 -1
  126. package/dist/types/src/signal/swarm-messenger.test.d.ts +0 -2
  127. package/dist/types/src/signal/swarm-messenger.test.d.ts.map +0 -1
  128. package/dist/types/src/tests/tcp-transport.test.d.ts +0 -2
  129. package/dist/types/src/tests/tcp-transport.test.d.ts.map +0 -1
  130. package/dist/types/src/transport/libdatachannel-transport.d.ts +0 -42
  131. package/dist/types/src/transport/libdatachannel-transport.d.ts.map +0 -1
  132. package/dist/types/src/transport/libdatachannel-transport.test.d.ts +0 -2
  133. package/dist/types/src/transport/libdatachannel-transport.test.d.ts.map +0 -1
  134. package/dist/types/src/transport/memory-transport.test.d.ts +0 -2
  135. package/dist/types/src/transport/memory-transport.test.d.ts.map +0 -1
  136. package/dist/types/src/transport/simplepeer-simple-peer.d.ts +0 -2
  137. package/dist/types/src/transport/simplepeer-simple-peer.d.ts.map +0 -1
  138. package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts +0 -2
  139. package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts.map +0 -1
  140. package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +0 -1
  141. package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +0 -1
  142. package/dist/types/src/transport/simplepeer-transport.d.ts +0 -36
  143. package/dist/types/src/transport/simplepeer-transport.d.ts.map +0 -1
  144. package/dist/types/src/transport/simplepeer-transport.test.d.ts +0 -2
  145. package/dist/types/src/transport/simplepeer-transport.test.d.ts.map +0 -1
  146. package/dist/types/src/transport/tcp-transport.browser.d.ts.map +0 -1
  147. package/dist/types/src/transport/tcp-transport.d.ts.map +0 -1
  148. package/dist/types/src/transport/webrtc.d.ts +0 -6
  149. package/dist/types/src/transport/webrtc.d.ts.map +0 -1
  150. package/src/globals.d.ts +0 -7
  151. package/src/tests/tcp-transport.test.ts +0 -67
  152. package/src/transport/libdatachannel-transport.test.ts +0 -100
  153. package/src/transport/libdatachannel-transport.ts +0 -376
  154. package/src/transport/memory-transport.test.ts +0 -74
  155. package/src/transport/simplepeer-simple-peer.ts +0 -26
  156. package/src/transport/simplepeer-transport-proxy-test.ts +0 -181
  157. package/src/transport/simplepeer-transport-proxy.ts +0 -246
  158. package/src/transport/simplepeer-transport-service.ts +0 -160
  159. package/src/transport/simplepeer-transport.test.ts +0 -61
  160. package/src/transport/simplepeer-transport.ts +0 -250
  161. package/src/transport/webrtc.ts +0 -15
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './tcp-transport';
@@ -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 './transport';
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 './transport';
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
- SIMPLE_PEER = 'SIMPLE_PEER',
12
- SIMPLE_PEER_PROXY = 'SIMPLE_PEER_PROXY',
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<void>;
29
- close(): Promise<void>;
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,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './rtc-transport-factory';
6
+ export * from './rtc-transport-proxy';
7
+ export * from './rtc-transport-service';
@@ -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
+ };