@dxos/network-manager 0.6.14-staging.e15392e → 0.7.0

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 (45) hide show
  1. package/dist/lib/browser/{chunk-RUNQZNCV.mjs → chunk-MAR4A5JK.mjs} +144 -85
  2. package/dist/lib/browser/chunk-MAR4A5JK.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +15 -9
  6. package/dist/lib/browser/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/{chunk-D6P7ACEM.cjs → chunk-J5PWZKEH.cjs} +147 -88
  8. package/dist/lib/node/chunk-J5PWZKEH.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +23 -23
  10. package/dist/lib/node/index.cjs.map +1 -1
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +28 -22
  13. package/dist/lib/node/testing/index.cjs.map +3 -3
  14. package/dist/lib/node-esm/{chunk-22DA2US6.mjs → chunk-XHE7MQ7U.mjs} +144 -85
  15. package/dist/lib/node-esm/chunk-XHE7MQ7U.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +1 -1
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +15 -9
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/types/src/swarm/peer.d.ts.map +1 -1
  21. package/dist/types/src/swarm/swarm.d.ts +1 -0
  22. package/dist/types/src/swarm/swarm.d.ts.map +1 -1
  23. package/dist/types/src/testing/test-builder.d.ts.map +1 -1
  24. package/dist/types/src/testing/test-wire-protocol.d.ts.map +1 -1
  25. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts +4 -0
  26. package/dist/types/src/transport/webrtc/rtc-peer-connection.d.ts.map +1 -1
  27. package/dist/types/src/transport/webrtc/rtc-transport-channel.d.ts.map +1 -1
  28. package/dist/types/src/transport/webrtc/rtc-transport-factory.d.ts.map +1 -1
  29. package/dist/types/src/transport/webrtc/rtc-transport-service.d.ts.map +1 -1
  30. package/package.json +18 -18
  31. package/src/swarm/connection.ts +2 -2
  32. package/src/swarm/peer.ts +4 -1
  33. package/src/swarm/swarm.ts +15 -2
  34. package/src/testing/test-builder.ts +1 -0
  35. package/src/testing/test-wire-protocol.ts +6 -4
  36. package/src/tests/basic-test-suite.ts +13 -13
  37. package/src/tests/webrtc-transport.test.ts +1 -1
  38. package/src/transport/webrtc/rtc-peer-connection.ts +21 -7
  39. package/src/transport/webrtc/rtc-transport-channel.ts +7 -1
  40. package/src/transport/webrtc/rtc-transport-factory.ts +1 -0
  41. package/src/transport/webrtc/rtc-transport-service.ts +4 -2
  42. package/src/transport/webrtc/rtc-transport.test.ts +18 -6
  43. package/dist/lib/browser/chunk-RUNQZNCV.mjs.map +0 -7
  44. package/dist/lib/node/chunk-D6P7ACEM.cjs.map +0 -7
  45. package/dist/lib/node-esm/chunk-22DA2US6.mjs.map +0 -7
@@ -234,10 +234,10 @@ export class Connection {
234
234
  // TODO(nf): fix ErrorStream so instanceof works here
235
235
  if (err instanceof ConnectionResetError) {
236
236
  log.info('aborting due to transport ConnectionResetError');
237
- this.abort().catch((err) => this.errors.raise(err));
237
+ this.abort(err).catch((err) => this.errors.raise(err));
238
238
  } else if (err instanceof ConnectivityError) {
239
239
  log.info('aborting due to transport ConnectivityError');
240
- this.abort().catch((err) => this.errors.raise(err));
240
+ this.abort(err).catch((err) => this.errors.raise(err));
241
241
  }
242
242
 
243
243
  if (this._state !== ConnectionState.CLOSED && this._state !== ConnectionState.CLOSING) {
package/src/swarm/peer.ts CHANGED
@@ -285,7 +285,8 @@ export class Peer {
285
285
  });
286
286
  },
287
287
  onClosed: (err) => {
288
- log('connection closed', { topic: this.topic, peerId: this.localInfo, remoteId: this.remoteInfo, initiator });
288
+ const logMeta = { topic: this.topic, peerId: this.localInfo, remoteId: this.remoteInfo, initiator };
289
+ log('connection closed', logMeta);
289
290
 
290
291
  // Make sure none of the connections are stuck in the limiter.
291
292
  this._connectionLimiter.doneConnecting(sessionId);
@@ -310,11 +311,13 @@ export class Peer {
310
311
  this.availableToConnect = false;
311
312
  this._availableAfter = increaseInterval(this._availableAfter);
312
313
  }
314
+
313
315
  this._callbacks.onDisconnected();
314
316
 
315
317
  scheduleTask(
316
318
  this._connectionCtx!,
317
319
  () => {
320
+ log('peer became available', logMeta);
318
321
  this.availableToConnect = true;
319
322
  this._callbacks.onPeerAvailable();
320
323
  },
@@ -189,8 +189,10 @@ export class Swarm {
189
189
  const peer = this._peers.get(swarmEvent.peerLeft.peer);
190
190
  if (peer) {
191
191
  peer.advertizing = false;
192
- if (peer.connection?.state !== ConnectionState.CONNECTED) {
193
- // Destroy peer only if there is no p2p-connection established
192
+ // Destroy peer only if there is no p2p-connection established. Otherwise, let peers go through
193
+ // the graceful shutdown protocol.
194
+ if (this._isConnectionEstablishmentInProgress(peer)) {
195
+ log(`destroying peer, state: ${peer.connection?.state}`);
194
196
  void this._destroyPeer(swarmEvent.peerLeft.peer, 'peer left').catch((err) => log.catch(err));
195
197
  }
196
198
  } else {
@@ -277,6 +279,7 @@ export class Swarm {
277
279
  },
278
280
  onDisconnected: async () => {
279
281
  if (this._isUnregistered(peer)) {
282
+ log.verbose('ignored onDisconnected for unregistered peer');
280
283
  return;
281
284
  }
282
285
  if (!peer!.advertizing) {
@@ -312,6 +315,7 @@ export class Swarm {
312
315
  }
313
316
 
314
317
  private async _destroyPeer(peerInfo: PeerInfo, reason?: string) {
318
+ log('destroy peer', { peerKey: peerInfo.peerKey, reason });
315
319
  const peer = this._peers.get(peerInfo);
316
320
  invariant(peer);
317
321
  this._peers.delete(peerInfo);
@@ -399,6 +403,15 @@ export class Swarm {
399
403
  await peer.closeConnection();
400
404
  }
401
405
 
406
+ private _isConnectionEstablishmentInProgress(peer: Peer) {
407
+ if (!peer.connection) {
408
+ return true;
409
+ }
410
+ return [ConnectionState.INITIAL, ConnectionState.CREATED, ConnectionState.CONNECTING].includes(
411
+ peer.connection.state,
412
+ );
413
+ }
414
+
402
415
  private _isUnregistered(peer?: Peer): boolean {
403
416
  return !peer || this._peers.get(peer.remoteInfo) !== peer;
404
417
  }
@@ -84,6 +84,7 @@ export class TestPeer {
84
84
  ) {
85
85
  this._signalManager = this.testBuilder.createSignalManager();
86
86
  this._networkManager = this.createNetworkManager(this.transport);
87
+ this._networkManager.setPeerInfo({ identityKey: peerId.toHex(), peerKey: peerId.toHex() });
87
88
  }
88
89
 
89
90
  // TODO(burdon): Move to TestBuilder.
@@ -33,11 +33,13 @@ export class TestWireProtocol {
33
33
 
34
34
  readonly factory = createTeleportProtocolFactory(async (teleport) => {
35
35
  log('create', { remotePeerId: teleport.remotePeerId });
36
+ const handleDisconnect = () => {
37
+ this.connections.delete(teleport.remotePeerId);
38
+ this.disconnected.emit(teleport.remotePeerId);
39
+ };
36
40
  const extension = new TestExtension({
37
- onClose: async () => {
38
- this.connections.delete(teleport.remotePeerId);
39
- this.disconnected.emit(teleport.remotePeerId);
40
- },
41
+ onClose: async () => handleDisconnect(),
42
+ onAbort: async () => handleDisconnect(),
41
43
  });
42
44
  this.connections.set(teleport.remotePeerId, extension);
43
45
  teleport.addExtension('test', extension);
@@ -23,7 +23,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
23
23
  return;
24
24
  }
25
25
 
26
- test.skip('joins swarm, sends messages, and cleanly exits', async () => {
26
+ test('joins swarm, sends messages, and cleanly exits', async () => {
27
27
  const peer1 = testBuilder.createPeer();
28
28
  const peer2 = testBuilder.createPeer();
29
29
  await openAndCloseAfterTest([peer1, peer2]);
@@ -35,7 +35,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
35
35
  });
36
36
 
37
37
  // TODO(burdon): Test with more peers (configure and test messaging).
38
- test.skip('joins swarm with star topology', async () => {
38
+ test('joins swarm with star topology', async () => {
39
39
  const peer1 = testBuilder.createPeer();
40
40
  onTestFinished(() => peer1.close());
41
41
  const peer2 = testBuilder.createPeer();
@@ -49,7 +49,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
49
49
  });
50
50
 
51
51
  // TODO(burdon): Fails when trying to reconnect to same topic.
52
- test.skip('joins swarm multiple times', async () => {
52
+ test('joins swarm multiple times', async () => {
53
53
  const peer1 = testBuilder.createPeer();
54
54
  const peer2 = testBuilder.createPeer();
55
55
  await openAndCloseAfterTest([peer1, peer2]);
@@ -75,7 +75,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
75
75
  }
76
76
  });
77
77
 
78
- test.skip('joins multiple swarms', async () => {
78
+ test('joins multiple swarms', async () => {
79
79
  // TODO(burdon): N peers.
80
80
  // TODO(burdon): Merge with test below.
81
81
  const peer1 = testBuilder.createPeer();
@@ -87,7 +87,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
87
87
  expect(topics).to.have.length(numSwarms);
88
88
  });
89
89
 
90
- test.skip('joins multiple swarms concurrently', async () => {
90
+ test('joins multiple swarms concurrently', async () => {
91
91
  const createSwarm = async () => {
92
92
  const topicA = PublicKey.random();
93
93
  const peer1a = testBuilder.createPeer();
@@ -109,7 +109,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
109
109
  ]);
110
110
  });
111
111
 
112
- test.skip('peers reconnect after and error in connection', async () => {
112
+ test('peers reconnect after and error in connection', async () => {
113
113
  const peer1 = testBuilder.createPeer();
114
114
  const peer2 = testBuilder.createPeer();
115
115
  await openAndCloseAfterTest([peer1, peer2]);
@@ -118,20 +118,20 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
118
118
  const [swarm1, swarm2] = await joinSwarm([peer1, peer2], topic, () => new FullyConnectedTopology());
119
119
  await exchangeMessages(swarm1, swarm2);
120
120
 
121
- void swarm1.protocol.connections.get(swarm2.peer.peerId)!.closeConnection(new Error('test error'));
121
+ const disconnectedKeys = new Set();
122
+ swarm1.protocol.disconnected.on((peerInfo) => disconnectedKeys.add(peerInfo.toHex()));
123
+ swarm2.protocol.disconnected.on((peerInfo) => disconnectedKeys.add(peerInfo.toHex()));
122
124
 
125
+ void swarm1.protocol.connections.get(swarm2.peer.peerId)!.closeConnection(new Error('test error'));
123
126
  // Wait until both peers are disconnected.
124
- await Promise.all([
125
- swarm1.protocol.disconnected.waitForCondition(() => swarm1.protocol.connections.size === 0),
126
- swarm2.protocol.disconnected.waitForCondition(() => swarm2.protocol.connections.size === 0),
127
- ]);
127
+ await expect.poll(() => disconnectedKeys.size).toEqual(2);
128
128
 
129
129
  await exchangeMessages(swarm1, swarm2);
130
130
 
131
131
  await leaveSwarm([peer1, peer2], topic);
132
132
  });
133
133
 
134
- test.skip('going offline and back online', { timeout: 2_000 }, async () => {
134
+ test('going offline and back online', { timeout: 2_000 }, async () => {
135
135
  const peer1 = testBuilder.createPeer();
136
136
  const peer2 = testBuilder.createPeer();
137
137
  await openAndCloseAfterTest([peer1, peer2]);
@@ -180,7 +180,7 @@ export const basicTestSuite = (testBuilder: TestBuilder, runTests = true) => {
180
180
  });
181
181
 
182
182
  // TODO(mykola): Fails with large amount of peers ~10.
183
- test.skip('many peers and connections', async () => {
183
+ test('many peers and connections', async () => {
184
184
  const numTopics = 2;
185
185
  const peersPerTopic = 3;
186
186
  const swarmsAllPeersConnected: Promise<any>[] = [];
@@ -17,7 +17,7 @@ describe('WebRTC transport proxy', { timeout: 10_000 }, () => {
17
17
  basicTestSuite(testBuilder);
18
18
  });
19
19
 
20
- describe('test with signal server', () => {
20
+ describe.skip('test with signal server', () => {
21
21
  describe('WebRTC transport', { timeout: 10_000 }, () => {
22
22
  const testBuilder = new TestBuilder({ signalHosts: TEST_SIGNAL_HOSTS });
23
23
  basicTestSuite(testBuilder);
@@ -26,8 +26,18 @@ export type RtcPeerChannelFactoryOptions = {
26
26
  // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers
27
27
  webrtcConfig?: RTCConfiguration;
28
28
  iceProvider?: IceProvider;
29
+
30
+ /**
31
+ * TODO: remove after the new rtc code rollout. Used for staging interop with older version running in prod.
32
+ */
33
+ legacyInitiator?: boolean;
29
34
  };
30
35
 
36
+ /**
37
+ * TODO: remove after the new rtc code rollout. Used for staging interop with older version running in prod.
38
+ */
39
+ const isLegacyInteropEnabled = () => Boolean((globalThis as any)?.DEVICE_INVITE_INTEROP);
40
+
31
41
  /**
32
42
  * A factory for rtc Transport implementations for a particular peer.
33
43
  * Contains WebRTC connection establishment logic.
@@ -59,7 +69,9 @@ export class RtcPeerConnection {
59
69
  private readonly _factory: RtcConnectionFactory,
60
70
  private readonly _options: RtcPeerChannelFactoryOptions,
61
71
  ) {
62
- this._initiator = chooseInitiatorPeer(_options.ownPeerKey, _options.remotePeerKey) === _options.ownPeerKey;
72
+ this._initiator = isLegacyInteropEnabled()
73
+ ? Boolean(this._options.legacyInitiator)
74
+ : chooseInitiatorPeer(_options.ownPeerKey, _options.remotePeerKey) === _options.ownPeerKey;
63
75
  }
64
76
 
65
77
  public get transportChannelCount() {
@@ -71,25 +83,27 @@ export class RtcPeerConnection {
71
83
  }
72
84
 
73
85
  public async createDataChannel(topic: string): Promise<RTCDataChannel> {
86
+ const channelKey = isLegacyInteropEnabled() ? 'dxos.mesh.transport' : topic;
87
+
74
88
  const connection = await this._openConnection();
75
- if (!this._transportChannels.has(topic)) {
89
+ if (isLegacyInteropEnabled() ? this._transportChannels.size === 0 : !this._transportChannels.has(channelKey)) {
76
90
  if (!this._transportChannels.size) {
77
91
  this._lockAndCloseConnection();
78
92
  }
79
93
  throw new Error('Transport closed while connection was being open');
80
94
  }
81
95
  if (this._initiator) {
82
- const channel = connection.createDataChannel(topic);
83
- this._dataChannels.set(topic, channel);
96
+ const channel = connection.createDataChannel(channelKey);
97
+ this._dataChannels.set(channelKey, channel);
84
98
  return channel;
85
99
  } else {
86
- const existingChannel = this._dataChannels.get(topic);
100
+ const existingChannel = this._dataChannels.get(channelKey);
87
101
  if (existingChannel) {
88
102
  return existingChannel;
89
103
  }
90
104
  log('waiting for initiator-peer to open a data channel');
91
105
  return new Promise((resolve, reject) => {
92
- this._channelCreatedCallbacks.set(topic, { resolve, reject });
106
+ this._channelCreatedCallbacks.set(channelKey, { resolve, reject });
93
107
  });
94
108
  }
95
109
  }
@@ -327,7 +341,7 @@ export class RtcPeerConnection {
327
341
  break;
328
342
  }
329
343
 
330
- log('signal processed');
344
+ log('signal processed', { type: data.type });
331
345
  }
332
346
 
333
347
  private async _processIceCandidate(connection: RTCPeerConnection, candidate: RTCIceCandidate) {
@@ -67,7 +67,13 @@ export class RtcTransportChannel extends Resource implements Transport {
67
67
  })
68
68
  .catch((err) => {
69
69
  if (this.isOpen) {
70
- this.errors.raise(new ConnectivityError(`Failed to create a channel: ${err?.message ?? 'unknown reason.'}`));
70
+ const error =
71
+ err instanceof Error
72
+ ? err
73
+ : new ConnectivityError(`Failed to create a channel: ${JSON.stringify(err?.message)}`);
74
+ this.errors.raise(error);
75
+ } else {
76
+ log.verbose('connection establishment failed after transport was closed', { err });
71
77
  }
72
78
  })
73
79
  .finally(() => {
@@ -19,6 +19,7 @@ export const createRtcTransportFactory = (
19
19
  ownPeerKey: options.ownPeerKey,
20
20
  remotePeerKey: options.remotePeerKey,
21
21
  sendSignal: options.sendSignal,
22
+ legacyInitiator: options.initiator,
22
23
  webrtcConfig,
23
24
  iceProvider,
24
25
  });
@@ -88,8 +88,6 @@ export class RtcTransportService implements BridgeService {
88
88
  writeProcessedCallbacks: [],
89
89
  };
90
90
 
91
- pushNewState(ConnectionState.CONNECTING);
92
-
93
91
  transport.connected.on(() => pushNewState(ConnectionState.CONNECTED));
94
92
 
95
93
  transport.errors.handle(async (err) => {
@@ -113,6 +111,10 @@ export class RtcTransportService implements BridgeService {
113
111
  });
114
112
 
115
113
  ready();
114
+
115
+ log('stream ready');
116
+
117
+ pushNewState(ConnectionState.CONNECTING);
116
118
  });
117
119
  }
118
120
 
@@ -126,12 +126,20 @@ describe('RtcTransport', () => {
126
126
 
127
127
  const createSignalSender = (options?: { duplicateSignals?: boolean }) => {
128
128
  const deliverSignal = (channel: RtcTransportChannel, signal: any) => {
129
+ // There's a race in libdatachannel between ice candidate gathering, connectivity checks and dtls connection
130
+ // establishment, which seems to be 100% reproducible when there are only host candidates.
131
+ // When there are no iceServers the first pair attempt starts too fast and gets interrupted by an arriving
132
+ // remote candidate. Dtls has a retry backoff which starts with 1 second delay, so all tests finish in 1s+.
133
+ if (signal.payload.data.type === 'candidate' && !isInitiator) {
134
+ return;
135
+ }
129
136
  void channel.onSignal(signal);
130
137
  if (options?.duplicateSignals) {
131
138
  void channel.onSignal(signal);
132
139
  }
133
140
  };
134
141
  let remoteChannel: RtcTransportChannel | undefined;
142
+ let isInitiator = false;
135
143
  const signalBuffer: any[] = [];
136
144
  const sendSignal = async (signal: any) => {
137
145
  await sleep(10);
@@ -141,7 +149,8 @@ describe('RtcTransport', () => {
141
149
  signalBuffer.push(signal);
142
150
  }
143
151
  };
144
- const onChannelCreated = (channel: RtcTransportChannel): void => {
152
+ const onChannelCreated = (channel: RtcTransportChannel, initiator: boolean) => {
153
+ isInitiator = initiator;
145
154
  remoteChannel = channel;
146
155
  for (const signal of signalBuffer) {
147
156
  deliverSignal(channel, signal);
@@ -152,20 +161,23 @@ describe('RtcTransport', () => {
152
161
 
153
162
  const createConnection = async (
154
163
  optionOverrides?: Partial<TransportOptions>,
155
- onChannelCreated: (channel: RtcTransportChannel) => void = () => {},
164
+ onChannelCreated: (channel: RtcTransportChannel, initiator: boolean) => void = () => {},
156
165
  ): Promise<TestSetup> => {
166
+ const remotePeerKey = optionOverrides?.remotePeerKey ?? PublicKey.random().toHex();
167
+ const ownPeerKey = optionOverrides?.ownPeerKey ?? PublicKey.random().toHex();
168
+ const initiator = chooseInitiatorPeer(remotePeerKey, ownPeerKey) === ownPeerKey;
157
169
  const stream = new TestStream();
158
170
  const options: TransportOptions = {
159
- initiator: false,
171
+ initiator,
160
172
  stream,
161
173
  sendSignal: async () => {},
162
- remotePeerKey: PublicKey.random().toHex(),
163
- ownPeerKey: PublicKey.random().toHex(),
174
+ remotePeerKey,
175
+ ownPeerKey,
164
176
  topic: PublicKey.random().toHex(),
165
177
  ...optionOverrides,
166
178
  };
167
179
  const connection = new RtcPeerConnection(connectionFactory, options);
168
- return { options, connection, stream, onChannelOpen: onChannelCreated };
180
+ return { options, connection, stream, onChannelOpen: (channel) => onChannelCreated(channel, initiator) };
169
181
  };
170
182
 
171
183
  const chooseInitiator = (peer1: TestSetup, peer2: TestSetup) => {