@dxos/network-manager 0.6.12-staging.e11e696 → 0.6.12

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 (155) hide show
  1. package/dist/lib/browser/{chunk-YOKKEU6T.mjs → chunk-XYSYUN63.mjs} +998 -1192
  2. package/dist/lib/browser/chunk-XYSYUN63.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +19 -10
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +27 -18
  6. package/dist/lib/browser/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/{chunk-7ZWQLO5T.cjs → chunk-4YAYC7WN.cjs} +1166 -1233
  8. package/dist/lib/node/chunk-4YAYC7WN.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +37 -27
  10. package/dist/lib/node/index.cjs.map +2 -2
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +29 -20
  13. package/dist/lib/node/testing/index.cjs.map +3 -3
  14. package/dist/types/src/signal/integration.test.d.ts +2 -0
  15. package/dist/types/src/signal/integration.test.d.ts.map +1 -0
  16. package/dist/types/src/signal/swarm-messenger.test.d.ts +2 -0
  17. package/dist/types/src/signal/swarm-messenger.test.d.ts.map +1 -0
  18. package/dist/types/src/swarm/connection.d.ts.map +1 -1
  19. package/dist/types/src/swarm/swarm.d.ts +1 -1
  20. package/dist/types/src/testing/test-builder.d.ts +2 -2
  21. package/dist/types/src/testing/test-builder.d.ts.map +1 -1
  22. package/dist/types/src/tests/basic-test-suite.d.ts.map +1 -1
  23. package/dist/types/src/tests/property-test-suite.d.ts.map +1 -1
  24. package/dist/types/src/tests/tcp-transport.test.d.ts +2 -0
  25. package/dist/types/src/tests/tcp-transport.test.d.ts.map +1 -0
  26. package/dist/types/src/tests/utils.d.ts.map +1 -1
  27. package/dist/types/src/transport/index.d.ts +5 -1
  28. package/dist/types/src/transport/index.d.ts.map +1 -1
  29. package/dist/types/src/transport/libdatachannel-transport.d.ts +42 -0
  30. package/dist/types/src/transport/libdatachannel-transport.d.ts.map +1 -0
  31. package/dist/types/src/transport/libdatachannel-transport.test.d.ts +2 -0
  32. package/dist/types/src/transport/libdatachannel-transport.test.d.ts.map +1 -0
  33. package/dist/types/src/transport/memory-transport.d.ts +2 -2
  34. package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
  35. package/dist/types/src/transport/memory-transport.test.d.ts +2 -0
  36. package/dist/types/src/transport/memory-transport.test.d.ts.map +1 -0
  37. package/dist/types/src/transport/simplepeer-simple-peer.d.ts +2 -0
  38. package/dist/types/src/transport/simplepeer-simple-peer.d.ts.map +1 -0
  39. package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts +2 -0
  40. package/dist/types/src/transport/simplepeer-transport-proxy-test.d.ts.map +1 -0
  41. package/dist/types/src/transport/{webrtc/rtc-transport-proxy.d.ts → simplepeer-transport-proxy.d.ts} +12 -10
  42. package/dist/types/src/transport/simplepeer-transport-proxy.d.ts.map +1 -0
  43. package/dist/types/src/transport/{webrtc/rtc-transport-service.d.ts → simplepeer-transport-service.d.ts} +7 -9
  44. package/dist/types/src/transport/simplepeer-transport-service.d.ts.map +1 -0
  45. package/dist/types/src/transport/simplepeer-transport.d.ts +36 -0
  46. package/dist/types/src/transport/simplepeer-transport.d.ts.map +1 -0
  47. package/dist/types/src/transport/simplepeer-transport.test.d.ts +2 -0
  48. package/dist/types/src/transport/simplepeer-transport.test.d.ts.map +1 -0
  49. package/dist/types/src/transport/{tcp/tcp-transport.browser.d.ts → tcp-transport.browser.d.ts} +3 -3
  50. package/dist/types/src/transport/tcp-transport.browser.d.ts.map +1 -0
  51. package/dist/types/src/transport/{tcp/tcp-transport.d.ts → tcp-transport.d.ts} +3 -3
  52. package/dist/types/src/transport/tcp-transport.d.ts.map +1 -0
  53. package/dist/types/src/transport/transport.d.ts +6 -7
  54. package/dist/types/src/transport/transport.d.ts.map +1 -1
  55. package/dist/types/src/transport/webrtc.d.ts +6 -0
  56. package/dist/types/src/transport/webrtc.d.ts.map +1 -0
  57. package/package.json +30 -53
  58. package/src/globals.d.ts +7 -0
  59. package/src/signal/ice.test.ts +3 -1
  60. package/src/signal/{integration.node.test.ts → integration.test.ts} +15 -9
  61. package/src/signal/{swarm-messenger.node.test.ts → swarm-messenger.test.ts} +23 -13
  62. package/src/swarm/connection-limiter.test.ts +6 -3
  63. package/src/swarm/connection.test.ts +38 -63
  64. package/src/swarm/connection.ts +5 -5
  65. package/src/swarm/swarm.test.ts +11 -9
  66. package/src/swarm/swarm.ts +1 -1
  67. package/src/testing/test-builder.ts +28 -12
  68. package/src/tests/basic-test-suite.ts +33 -34
  69. package/src/tests/memory-transport.test.ts +42 -40
  70. package/src/tests/property-test-suite.ts +22 -21
  71. package/src/tests/tcp-transport.test.ts +67 -0
  72. package/src/tests/utils.ts +2 -3
  73. package/src/tests/webrtc-transport.test.ts +9 -9
  74. package/src/transport/index.ts +5 -1
  75. package/src/transport/libdatachannel-transport.test.ts +100 -0
  76. package/src/transport/libdatachannel-transport.ts +376 -0
  77. package/src/transport/memory-transport.test.ts +74 -0
  78. package/src/transport/memory-transport.ts +0 -2
  79. package/src/transport/simplepeer-simple-peer.ts +26 -0
  80. package/src/transport/simplepeer-transport-proxy-test.ts +181 -0
  81. package/src/transport/simplepeer-transport-proxy.ts +246 -0
  82. package/src/transport/simplepeer-transport-service.ts +160 -0
  83. package/src/transport/simplepeer-transport.test.ts +61 -0
  84. package/src/transport/simplepeer-transport.ts +250 -0
  85. package/src/transport/{tcp/tcp-transport.browser.ts → tcp-transport.browser.ts} +3 -7
  86. package/src/transport/{tcp/tcp-transport.ts → tcp-transport.ts} +1 -3
  87. package/src/transport/transport.ts +7 -8
  88. package/src/transport/webrtc.ts +15 -0
  89. package/src/typings.d.ts +2 -8
  90. package/dist/lib/browser/chunk-GW3YM55A.mjs +0 -14
  91. package/dist/lib/browser/chunk-GW3YM55A.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-YOKKEU6T.mjs.map +0 -7
  93. package/dist/lib/browser/transport/tcp/index.mjs +0 -39
  94. package/dist/lib/browser/transport/tcp/index.mjs.map +0 -7
  95. package/dist/lib/node/chunk-7ZWQLO5T.cjs.map +0 -7
  96. package/dist/lib/node/transport/tcp/index.cjs +0 -191
  97. package/dist/lib/node/transport/tcp/index.cjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-4VO725JT.mjs +0 -4383
  99. package/dist/lib/node-esm/chunk-4VO725JT.mjs.map +0 -7
  100. package/dist/lib/node-esm/index.mjs +0 -50
  101. package/dist/lib/node-esm/index.mjs.map +0 -7
  102. package/dist/lib/node-esm/meta.json +0 -1
  103. package/dist/lib/node-esm/testing/index.mjs +0 -280
  104. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  105. package/dist/lib/node-esm/transport/tcp/index.mjs +0 -159
  106. package/dist/lib/node-esm/transport/tcp/index.mjs.map +0 -7
  107. package/dist/types/src/signal/integration.node.test.d.ts +0 -2
  108. package/dist/types/src/signal/integration.node.test.d.ts.map +0 -1
  109. package/dist/types/src/signal/swarm-messenger.node.test.d.ts +0 -2
  110. package/dist/types/src/signal/swarm-messenger.node.test.d.ts.map +0 -1
  111. package/dist/types/src/tests/tcp-transport.node.test.d.ts +0 -2
  112. package/dist/types/src/tests/tcp-transport.node.test.d.ts.map +0 -1
  113. package/dist/types/src/transport/tcp/index.d.ts +0 -2
  114. package/dist/types/src/transport/tcp/index.d.ts.map +0 -1
  115. package/dist/types/src/transport/tcp/tcp-transport.browser.d.ts.map +0 -1
  116. package/dist/types/src/transport/tcp/tcp-transport.d.ts.map +0 -1
  117. package/dist/types/src/transport/webrtc/index.d.ts +0 -4
  118. package/dist/types/src/transport/webrtc/index.d.ts.map +0 -1
  119. package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts +0 -14
  120. package/dist/types/src/transport/webrtc/rtc-connection-factory.d.ts.map +0 -1
  121. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts +0 -68
  122. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts.map +0 -1
  123. package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts +0 -33
  124. package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts.map +0 -1
  125. package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts +0 -2
  126. package/dist/types/src/transport/webrtc/rtc-transport-channel.test.d.ts.map +0 -1
  127. package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts +0 -4
  128. package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts.map +0 -1
  129. package/dist/types/src/transport/webrtc/rtc-transport-proxy.d.ts.map +0 -1
  130. package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts +0 -2
  131. package/dist/types/src/transport/webrtc/rtc-transport-proxy.test.d.ts.map +0 -1
  132. package/dist/types/src/transport/webrtc/rtc-transport-service.d.ts.map +0 -1
  133. package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts +0 -4
  134. package/dist/types/src/transport/webrtc/rtc-transport-stats.d.ts.map +0 -1
  135. package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts +0 -2
  136. package/dist/types/src/transport/webrtc/rtc-transport.test.d.ts.map +0 -1
  137. package/dist/types/src/transport/webrtc/test-utils.d.ts +0 -5
  138. package/dist/types/src/transport/webrtc/test-utils.d.ts.map +0 -1
  139. package/dist/types/src/transport/webrtc/utils.d.ts +0 -3
  140. package/dist/types/src/transport/webrtc/utils.d.ts.map +0 -1
  141. package/src/tests/tcp-transport.node.test.ts +0 -65
  142. package/src/transport/tcp/index.ts +0 -5
  143. package/src/transport/webrtc/index.ts +0 -7
  144. package/src/transport/webrtc/rtc-connection-factory.ts +0 -82
  145. package/src/transport/webrtc/rtc-peer-connection.ts +0 -472
  146. package/src/transport/webrtc/rtc-transport-channel.test.ts +0 -176
  147. package/src/transport/webrtc/rtc-transport-channel.ts +0 -195
  148. package/src/transport/webrtc/rtc-transport-factory.ts +0 -28
  149. package/src/transport/webrtc/rtc-transport-proxy.test.ts +0 -413
  150. package/src/transport/webrtc/rtc-transport-proxy.ts +0 -264
  151. package/src/transport/webrtc/rtc-transport-service.ts +0 -192
  152. package/src/transport/webrtc/rtc-transport-stats.ts +0 -67
  153. package/src/transport/webrtc/rtc-transport.test.ts +0 -198
  154. package/src/transport/webrtc/test-utils.ts +0 -22
  155. package/src/transport/webrtc/utils.ts +0 -36
@@ -1,472 +0,0 @@
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
- };
@@ -1,176 +0,0 @@
1
- //
2
- // Copyright 2020 DXOS.org
3
- //
4
-
5
- import { Duplex } from 'node:stream';
6
- import { describe, expect, test } from 'vitest';
7
-
8
- import { sleep } from '@dxos/async';
9
-
10
- import { type RtcPeerConnection } from './rtc-peer-connection';
11
- import { RtcTransportChannel } from './rtc-transport-channel';
12
- import { handleChannelErrors } from './test-utils';
13
- import { type TransportOptions } from '../transport';
14
-
15
- describe('RtcTransportChannel', () => {
16
- test('transport error raised if channel creation fails', async () => {
17
- const controller = createChannelController();
18
- const { transport } = createTransport(controller.connection);
19
- const transportErrors = handleChannelErrors(transport);
20
- await transport.open();
21
- controller.onChannelCreationFailed();
22
- await transportErrors.expectErrorRaised();
23
- });
24
-
25
- test('channel closed if it was open after transport was closed', async () => {
26
- const controller = createChannelController();
27
- const { transport } = createTransport(controller.connection);
28
- await transport.open();
29
- await controller.onChannelCreated();
30
- await transport.close();
31
- controller.channel.onopen();
32
- expect(controller.channel.wasClosed()).to.be.true;
33
- });
34
-
35
- test('channel open while transport is being closed', async () => {
36
- for (const syncOpen of [false, true]) {
37
- const controller = createChannelController();
38
- const { transport } = createTransport(controller.connection);
39
- await transport.open();
40
- await controller.onChannelCreated();
41
- void transport.close();
42
- if (syncOpen) {
43
- controller.channel.onopen();
44
- } else {
45
- setTimeout(() => controller.channel.onopen());
46
- }
47
- await sleep(10);
48
- expect(controller.channel.wasClosed()).to.be.true;
49
- }
50
- });
51
-
52
- test('channel close closes transport', async () => {
53
- const controller = createChannelController();
54
- const { transport } = createTransport(controller.connection);
55
- const transportClosedEvent = handleClose(transport);
56
- await transport.open();
57
- await controller.onChannelCreated();
58
- controller.channel.onopen();
59
- await sleep(10);
60
- await controller.channel.onclose();
61
- expect(transport.isOpen).to.be.false;
62
- await transportClosedEvent.expectWasEmitted();
63
- });
64
-
65
- test('channel closed if created after transport was closed', async () => {
66
- const controller = createChannelController();
67
- const { transport } = createTransport(controller.connection);
68
- await transport.open();
69
- await transport.close();
70
- await controller.onChannelCreated();
71
- controller.channel.onopen();
72
- expect(controller.channel.wasClosed()).to.be.true;
73
- });
74
-
75
- test('message not delivered on a closed transport', async () => {
76
- const controller = createChannelController();
77
- const { deliveredMessages, transport } = createTransport(controller.connection);
78
- await transport.open();
79
- await controller.onChannelCreated();
80
- controller.channel.onopen();
81
- const message = 'hello';
82
- await controller.channel.onMessage(message);
83
- expect(deliveredMessages).toStrictEqual([message]);
84
- await transport.close();
85
- await controller.channel.onMessage(message + '1');
86
- expect(deliveredMessages).toStrictEqual([message]);
87
- });
88
-
89
- test('message not sent on a closed transport', async () => {
90
- const controller = createChannelController();
91
- const { deliveredMessages, transport } = createTransport(controller.connection);
92
- await transport.open();
93
- await controller.onChannelCreated();
94
- controller.channel.onopen();
95
- const message = 'hello';
96
- await controller.channel.onMessage(message);
97
- expect(deliveredMessages).toStrictEqual([message]);
98
- await transport.close();
99
- await controller.channel.onMessage(message + '1');
100
- expect(deliveredMessages).toStrictEqual([message]);
101
- });
102
-
103
- test('error raised if send fails', async () => {
104
- const controller = createChannelController();
105
- const { transport, stream } = createTransport(controller.connection);
106
- await transport.open();
107
- await controller.onChannelCreated();
108
- const transportClosedEvent = handleChannelErrors(transport);
109
- controller.channel.onopen();
110
- controller.setFailSending(true);
111
- stream.push('hello');
112
- await transportClosedEvent.expectErrorRaised();
113
- });
114
-
115
- const createTransport = (connection: RtcPeerConnection) => {
116
- const deliveredMessages: any[] = [];
117
- const stream = new Duplex({
118
- read: () => {},
119
- write: (chunk: any, _: BufferEncoding, callback: (error?: Error | null) => void) => {
120
- deliveredMessages.push(Buffer.from(chunk).toString());
121
- callback();
122
- },
123
- });
124
- const options = { topic: 'test', stream } as any as TransportOptions;
125
- return { deliveredMessages, stream, transport: new RtcTransportChannel(connection, options) };
126
- };
127
-
128
- const handleClose = (channel: RtcTransportChannel) => {
129
- let emitted = false;
130
- channel.closed.on(() => (emitted = true));
131
- return { expectWasEmitted: async () => expect(emitted).toBeTruthy() };
132
- };
133
-
134
- const createChannelController = () => {
135
- // Lowercase methods will get overwritten internally.
136
- let closed = false;
137
- let failsSending = false;
138
- const channel = {
139
- onopen: () => {},
140
- onclose: async () => {},
141
- close: () => (closed = true),
142
- send: () => {
143
- if (failsSending) {
144
- throw new Error('Expected');
145
- }
146
- },
147
- wasClosed: () => closed,
148
- onMessage: async (message: string) => {
149
- (channel as any).onmessage({ data: message });
150
- await sleep(5);
151
- },
152
- };
153
- let onChannelCreated = async () => {};
154
- let onChannelCreationFailed = () => {};
155
- const createChannelPromise = new Promise((resolve, reject) => {
156
- onChannelCreated = async () => {
157
- resolve(channel);
158
- await sleep(5);
159
- };
160
- onChannelCreationFailed = reject;
161
- });
162
- return {
163
- onChannelCreated,
164
- onChannelCreationFailed,
165
- setFailSending: (fail: boolean) => {
166
- failsSending = fail;
167
- },
168
- channel,
169
- connection: {
170
- createDataChannel: async (topic: string) => {
171
- return createChannelPromise;
172
- },
173
- } as any as RtcPeerConnection,
174
- };
175
- };
176
- });