@dxos/network-manager 0.1.9 → 0.1.11

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/src/index.d.ts +2 -2
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +2 -2
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/network-manager.d.ts +10 -14
  6. package/dist/src/network-manager.d.ts.map +1 -1
  7. package/dist/src/network-manager.js +28 -33
  8. package/dist/src/network-manager.js.map +1 -1
  9. package/dist/src/protocol-factory.d.ts.map +1 -1
  10. package/dist/src/protocol-factory.js +8 -6
  11. package/dist/src/protocol-factory.js.map +1 -1
  12. package/dist/src/signal/index.d.ts +2 -2
  13. package/dist/src/signal/index.d.ts.map +1 -1
  14. package/dist/src/signal/index.js +2 -2
  15. package/dist/src/signal/index.js.map +1 -1
  16. package/dist/src/signal/message-router.d.ts +2 -2
  17. package/dist/src/signal/message-router.js +3 -3
  18. package/dist/src/signal/message-router.js.map +1 -1
  19. package/dist/src/signal/{signal-messaging.d.ts → signal-messenger.d.ts} +2 -2
  20. package/dist/src/signal/{signal-messaging.d.ts.map → signal-messenger.d.ts.map} +1 -1
  21. package/dist/src/signal/{signal-messaging.js → signal-messenger.js} +1 -1
  22. package/dist/src/signal/signal-messenger.js.map +1 -0
  23. package/dist/src/swarm/connection.d.ts +6 -3
  24. package/dist/src/swarm/connection.d.ts.map +1 -1
  25. package/dist/src/swarm/connection.js +28 -15
  26. package/dist/src/swarm/connection.js.map +1 -1
  27. package/dist/src/swarm/peer.d.ts +45 -0
  28. package/dist/src/swarm/peer.d.ts.map +1 -0
  29. package/dist/src/swarm/peer.js +113 -0
  30. package/dist/src/swarm/peer.js.map +1 -0
  31. package/dist/src/swarm/swarm-mapper.d.ts +2 -2
  32. package/dist/src/swarm/swarm-mapper.d.ts.map +1 -1
  33. package/dist/src/swarm/swarm-mapper.js +12 -10
  34. package/dist/src/swarm/swarm-mapper.js.map +1 -1
  35. package/dist/src/swarm/swarm.d.ts +13 -18
  36. package/dist/src/swarm/swarm.d.ts.map +1 -1
  37. package/dist/src/swarm/swarm.js +126 -97
  38. package/dist/src/swarm/swarm.js.map +1 -1
  39. package/dist/src/swarm/swarm.test.js +2 -2
  40. package/dist/src/swarm/swarm.test.js.map +1 -1
  41. package/dist/src/testing/test-builder.d.ts +31 -18
  42. package/dist/src/testing/test-builder.d.ts.map +1 -1
  43. package/dist/src/testing/test-builder.js +120 -48
  44. package/dist/src/testing/test-builder.js.map +1 -1
  45. package/dist/src/testing/test-protocol.d.ts +5 -5
  46. package/dist/src/testing/test-protocol.d.ts.map +1 -1
  47. package/dist/src/testing/test-protocol.js +28 -24
  48. package/dist/src/testing/test-protocol.js.map +1 -1
  49. package/dist/src/tests/basic-test-suite.d.ts +6 -0
  50. package/dist/src/tests/basic-test-suite.d.ts.map +1 -0
  51. package/dist/src/tests/basic-test-suite.js +142 -0
  52. package/dist/src/tests/basic-test-suite.js.map +1 -0
  53. package/dist/src/tests/memory-transport.test.d.ts +2 -0
  54. package/dist/src/tests/memory-transport.test.d.ts.map +1 -0
  55. package/dist/src/tests/memory-transport.test.js +12 -0
  56. package/dist/src/tests/memory-transport.test.js.map +1 -0
  57. package/dist/src/{presence.test.d.ts → tests/presence.test.d.ts} +0 -0
  58. package/dist/src/tests/presence.test.d.ts.map +1 -0
  59. package/dist/src/{presence.test.js → tests/presence.test.js} +21 -20
  60. package/dist/src/tests/presence.test.js.map +1 -0
  61. package/dist/src/tests/property-test-suite.d.ts +5 -0
  62. package/dist/src/tests/property-test-suite.d.ts.map +1 -0
  63. package/dist/src/tests/property-test-suite.js +203 -0
  64. package/dist/src/tests/property-test-suite.js.map +1 -0
  65. package/dist/src/tests/utils.d.ts +21 -0
  66. package/dist/src/tests/utils.d.ts.map +1 -0
  67. package/dist/src/tests/utils.js +76 -0
  68. package/dist/src/tests/utils.js.map +1 -0
  69. package/dist/src/tests/webrtc-transport.test.d.ts +2 -0
  70. package/dist/src/tests/webrtc-transport.test.d.ts.map +1 -0
  71. package/dist/src/tests/webrtc-transport.test.js +16 -0
  72. package/dist/src/tests/webrtc-transport.test.js.map +1 -0
  73. package/dist/src/topology/fully-connected-topology.d.ts +1 -1
  74. package/dist/src/topology/fully-connected-topology.d.ts.map +1 -1
  75. package/dist/src/topology/fully-connected-topology.js +4 -3
  76. package/dist/src/topology/fully-connected-topology.js.map +1 -1
  77. package/dist/src/topology/mmst-topology.js +4 -4
  78. package/dist/src/topology/mmst-topology.js.map +1 -1
  79. package/dist/src/topology/star-topology.js +4 -4
  80. package/dist/src/topology/star-topology.js.map +1 -1
  81. package/dist/src/topology/topology.d.ts +0 -5
  82. package/dist/src/topology/topology.d.ts.map +1 -1
  83. package/dist/src/transport/memory-transport.d.ts +8 -7
  84. package/dist/src/transport/memory-transport.d.ts.map +1 -1
  85. package/dist/src/transport/memory-transport.js +79 -45
  86. package/dist/src/transport/memory-transport.js.map +1 -1
  87. package/dist/src/transport/memory-transport.test.js +3 -2
  88. package/dist/src/transport/memory-transport.test.js.map +1 -1
  89. package/dist/src/transport/transport.d.ts +8 -4
  90. package/dist/src/transport/transport.d.ts.map +1 -1
  91. package/dist/src/transport/webrtc-simple-peer.test.d.ts +2 -0
  92. package/dist/src/transport/webrtc-simple-peer.test.d.ts.map +1 -0
  93. package/dist/src/transport/{wrtc-simple-peer.test.js → webrtc-simple-peer.test.js} +2 -2
  94. package/dist/src/transport/webrtc-simple-peer.test.js.map +1 -0
  95. package/dist/src/transport/webrtc-transport-proxy.d.ts +4 -4
  96. package/dist/src/transport/webrtc-transport-proxy.d.ts.map +1 -1
  97. package/dist/src/transport/webrtc-transport-proxy.js +15 -10
  98. package/dist/src/transport/webrtc-transport-proxy.js.map +1 -1
  99. package/dist/src/transport/webrtc-transport-proxy.test.js +2 -2
  100. package/dist/src/transport/webrtc-transport-proxy.test.js.map +1 -1
  101. package/dist/src/transport/webrtc-transport-service.js +2 -2
  102. package/dist/src/transport/webrtc-transport-service.js.map +1 -1
  103. package/dist/src/transport/webrtc-transport.d.ts +3 -3
  104. package/dist/src/transport/webrtc-transport.d.ts.map +1 -1
  105. package/dist/src/transport/webrtc-transport.js +10 -10
  106. package/dist/src/transport/webrtc-transport.js.map +1 -1
  107. package/dist/src/transport/webrtc-transport.test.js +3 -3
  108. package/dist/src/transport/webrtc-transport.test.js.map +1 -1
  109. package/dist/src/types.d.ts +1 -1
  110. package/dist/src/types.d.ts.map +1 -1
  111. package/package.json +27 -22
  112. package/src/index.ts +2 -2
  113. package/src/network-manager.ts +31 -46
  114. package/src/protocol-factory.ts +5 -3
  115. package/src/signal/index.ts +2 -2
  116. package/src/signal/integration.test.ts +1 -1
  117. package/src/signal/message-router.test.ts +1 -1
  118. package/src/signal/message-router.ts +2 -2
  119. package/src/signal/{signal-messaging.ts → signal-messenger.ts} +2 -2
  120. package/src/swarm/connection.ts +23 -12
  121. package/src/swarm/peer.ts +156 -0
  122. package/src/swarm/swarm-mapper.ts +21 -10
  123. package/src/swarm/swarm.ts +142 -111
  124. package/src/testing/test-builder.ts +143 -63
  125. package/src/testing/test-protocol.ts +31 -23
  126. package/src/tests/basic-test-suite.ts +168 -0
  127. package/src/tests/memory-transport.test.ts +11 -0
  128. package/src/{presence.test.ts → tests/presence.test.ts} +24 -23
  129. package/src/tests/property-test-suite.ts +221 -0
  130. package/src/tests/utils.ts +85 -0
  131. package/src/tests/webrtc-transport.test.ts +16 -0
  132. package/src/topology/fully-connected-topology.ts +5 -4
  133. package/src/topology/topology.ts +0 -5
  134. package/src/transport/memory-transport.test.ts +3 -2
  135. package/src/transport/memory-transport.ts +79 -49
  136. package/src/transport/transport.ts +9 -3
  137. package/src/transport/{wrtc-simple-peer.test.ts → webrtc-simple-peer.test.ts} +1 -1
  138. package/src/transport/webrtc-transport-proxy.test.ts +2 -2
  139. package/src/transport/webrtc-transport-proxy.ts +17 -8
  140. package/src/transport/webrtc-transport-service.ts +1 -1
  141. package/src/transport/webrtc-transport.test.ts +4 -6
  142. package/src/transport/webrtc-transport.ts +4 -4
  143. package/testing.d.ts +11 -0
  144. package/testing.js +5 -0
  145. package/dist/src/network-manager.test.d.ts +0 -2
  146. package/dist/src/network-manager.test.d.ts.map +0 -1
  147. package/dist/src/network-manager.test.js +0 -596
  148. package/dist/src/network-manager.test.js.map +0 -1
  149. package/dist/src/presence.test.d.ts.map +0 -1
  150. package/dist/src/presence.test.js.map +0 -1
  151. package/dist/src/signal/signal-messaging.js.map +0 -1
  152. package/dist/src/transport/wrtc-simple-peer.test.d.ts +0 -2
  153. package/dist/src/transport/wrtc-simple-peer.test.d.ts.map +0 -1
  154. package/dist/src/transport/wrtc-simple-peer.test.js.map +0 -1
  155. package/src/network-manager.test.ts +0 -707
@@ -0,0 +1,156 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import assert from 'assert';
6
+
7
+ import { synchronized } from '@dxos/async';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+ import { Protocol } from '@dxos/mesh-protocol';
11
+
12
+ import { SignalMessage, SignalMessenger } from '../signal';
13
+ import { TransportFactory } from '../transport';
14
+ import { Connection, ConnectionState } from './connection';
15
+
16
+ interface PeerCallbacks {
17
+ /**
18
+ * Connection opened.
19
+ */
20
+ onConnected: () => void;
21
+
22
+ /**
23
+ * Connection closed.
24
+ */
25
+ onDisconnected: () => void;
26
+
27
+ /**
28
+ * Peer accepted our offer to connect.
29
+ */
30
+ onAccepted: () => void;
31
+
32
+ /**
33
+ * Peer rejected our offer to connect.
34
+ */
35
+ onRejected: () => void;
36
+ }
37
+
38
+ export class Peer {
39
+ public connection?: Connection;
40
+
41
+ /**
42
+ * Whether the peer is currently advertizing itself on the signal-network.
43
+ */
44
+ public advertizing = false;
45
+
46
+ constructor(
47
+ public readonly id: PublicKey,
48
+ public readonly topic: PublicKey,
49
+ public readonly localPeerId: PublicKey,
50
+ private readonly _callbacks: PeerCallbacks
51
+ ) {}
52
+
53
+ /**
54
+ * Create new connection.
55
+ * Either we're initiating a connection or creating one in response to an offer from the other peer.
56
+ */
57
+ createConnection(
58
+ // TODO(dmaretskyi): Move into constructor?
59
+ signalMessaging: SignalMessenger,
60
+ initiator: boolean,
61
+ sessionId: PublicKey,
62
+ protocol: Protocol,
63
+ transportFactory: TransportFactory
64
+ ) {
65
+ log('creating connection', {
66
+ topic: this.topic,
67
+ peerId: this.localPeerId,
68
+ remoteId: this.id,
69
+ initiator,
70
+ sessionId
71
+ });
72
+ assert(!this.connection, 'Already connected.');
73
+ const connection = new Connection(
74
+ this.topic,
75
+ this.localPeerId,
76
+ this.id,
77
+ sessionId,
78
+ initiator,
79
+ signalMessaging,
80
+ protocol,
81
+ transportFactory
82
+ );
83
+ connection.stateChanged.on((state) => {
84
+ switch (state) {
85
+ case ConnectionState.CONNECTED: {
86
+ this._callbacks.onConnected();
87
+ break;
88
+ }
89
+
90
+ case ConnectionState.REJECTED: {
91
+ this._callbacks.onRejected();
92
+ break;
93
+ }
94
+
95
+ case ConnectionState.ACCEPTED: {
96
+ this._callbacks.onAccepted();
97
+ break;
98
+ }
99
+
100
+ case ConnectionState.CLOSED: {
101
+ log('connection closed', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, initiator });
102
+ assert(this.connection === connection, 'Connection mismatch (race condition).');
103
+
104
+ this.connection = undefined;
105
+ this._callbacks.onDisconnected();
106
+ break;
107
+ }
108
+ }
109
+ });
110
+ connection.errors.handle((err) => {
111
+ log.warn('connection error', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, initiator, err });
112
+
113
+ // Calls `onStateChange` with CLOSED state.
114
+ void this.closeConnection().catch(() => {
115
+ log.catch(err);
116
+ });
117
+ });
118
+
119
+ this.connection = connection;
120
+
121
+ return connection;
122
+ }
123
+
124
+ async closeConnection() {
125
+ if (!this.connection) {
126
+ return;
127
+ }
128
+ const connection = this.connection;
129
+ log('closing...', { peerId: this.id, sessionId: connection.sessionId });
130
+
131
+ try {
132
+ // Triggers `onStateChange` callback which will clean up the connection.
133
+ await connection.close();
134
+ } catch (err) {
135
+ log.catch(err);
136
+ }
137
+
138
+ log('closed', { peerId: this.id, sessionId: connection.sessionId });
139
+ }
140
+
141
+ async onSignal(message: SignalMessage) {
142
+ if (!this.connection) {
143
+ log('dropping signal message for non-existent connection', { message });
144
+ return;
145
+ }
146
+ await this.connection.signal(message);
147
+ }
148
+
149
+ @synchronized
150
+ async destroy() {
151
+ log('Destroying peer', { peerId: this.id, topic: this.topic });
152
+
153
+ // Won't throw.
154
+ await this?.connection?.close();
155
+ }
156
+ }
@@ -30,15 +30,14 @@ type Unsubscribe = () => void;
30
30
  export class SwarmMapper {
31
31
  private readonly _subscriptions = new EventSubscriptions();
32
32
  private readonly _connectionSubscriptions = new ComplexMap<PublicKey, Unsubscribe>(PublicKey.hash);
33
-
34
33
  private readonly _peers = new ComplexMap<PublicKey, PeerInfo>(PublicKey.hash);
35
34
 
35
+ readonly mapUpdated = new Event<PeerInfo[]>();
36
+
36
37
  get peers(): PeerInfo[] {
37
38
  return Array.from(this._peers.values());
38
39
  }
39
40
 
40
- readonly mapUpdated = new Event<PeerInfo[]>();
41
-
42
41
  // prettier-ignore
43
42
  constructor(
44
43
  private readonly _swarm: Swarm,
@@ -55,29 +54,35 @@ export class SwarmMapper {
55
54
  );
56
55
  })
57
56
  );
57
+
58
58
  this._subscriptions.add(
59
- _swarm.connectionRemoved.on((connection) => {
60
- this._connectionSubscriptions.get(connection.remoteId)?.();
61
- this._connectionSubscriptions.delete(connection.remoteId);
59
+ _swarm.disconnected.on((peerId) => {
60
+ this._connectionSubscriptions.get(peerId)?.();
61
+ this._connectionSubscriptions.delete(peerId);
62
62
  this._update();
63
63
  })
64
64
  );
65
+
65
66
  if (_presence) {
66
- const cb = () => {
67
+ this._subscriptions.add(_presence.graphUpdated.on(() => {
67
68
  this._update();
68
- };
69
- this._subscriptions.add(_presence.graphUpdated.on(cb));
69
+ }));
70
70
  }
71
+
72
+ // TODO(burdon): Do not call from constructor.
71
73
  this._update();
72
74
  }
73
75
 
74
76
  private _update() {
77
+ log('updating swarm');
78
+
75
79
  this._peers.clear();
76
80
  this._peers.set(this._swarm.ownPeerId, {
77
81
  id: this._swarm.ownPeerId,
78
- state: 'ME',
82
+ state: 'ME', // TODO(burdon): Enum (rename "local").
79
83
  connections: []
80
84
  });
85
+
81
86
  for (const connection of this._swarm.connections) {
82
87
  this._peers.set(connection.remoteId, {
83
88
  id: connection.remoteId,
@@ -85,18 +90,21 @@ export class SwarmMapper {
85
90
  connections: [this._swarm.ownPeerId]
86
91
  });
87
92
  }
93
+
88
94
  if (this._presence) {
89
95
  this._presence.graph.forEachNode((node: any) => {
90
96
  const id = PublicKey.fromHex(node.id);
91
97
  if (this._peers.has(id)) {
92
98
  return;
93
99
  }
100
+
94
101
  this._peers.set(id, {
95
102
  id,
96
103
  state: 'INDIRECTLY_CONNECTED',
97
104
  connections: []
98
105
  });
99
106
  });
107
+
100
108
  this._presence.graph.forEachLink((link: any) => {
101
109
  const from = PublicKey.from(link.fromId);
102
110
  const to = PublicKey.from(link.toId);
@@ -106,13 +114,16 @@ export class SwarmMapper {
106
114
  }
107
115
  });
108
116
  }
117
+
109
118
  log('graph changed', {
110
119
  directConnections: this._swarm.connections.length,
111
120
  totalPeersInSwarm: this._peers.size
112
121
  });
122
+
113
123
  this.mapUpdated.emit(Array.from(this._peers.values()));
114
124
  }
115
125
 
126
+ // TODO(burdon): Async open/close.
116
127
  destroy() {
117
128
  Array.from(this._connectionSubscriptions.values()).forEach((cb) => cb());
118
129
  this._subscriptions.clear();
@@ -12,15 +12,15 @@ import { log } from '@dxos/log';
12
12
  import { Messenger } from '@dxos/messaging';
13
13
  import { SwarmEvent } from '@dxos/protocols/proto/dxos/mesh/signal';
14
14
  import { Answer } from '@dxos/protocols/proto/dxos/mesh/swarm';
15
- import { ComplexMap, ComplexSet } from '@dxos/util';
15
+ import { ComplexMap, isNotNullOrUndefined } from '@dxos/util';
16
16
 
17
17
  import { ProtocolProvider } from '../network-manager';
18
- import { MessageRouter } from '../signal';
19
- import { OfferMessage, SignalMessage } from '../signal/signal-messaging';
18
+ import { MessageRouter, OfferMessage, SignalMessage } from '../signal';
20
19
  import { SwarmController, Topology } from '../topology';
21
20
  import { TransportFactory } from '../transport';
22
21
  import { Topic } from '../types';
23
- import { Connection, ConnectionState } from './connection';
22
+ import { Connection } from './connection';
23
+ import { Peer } from './peer';
24
24
 
25
25
  const INITIATION_DELAY = 100;
26
26
 
@@ -33,10 +33,12 @@ const getClassName = (obj: any) => Object.getPrototypeOf(obj).constructor.name;
33
33
  * Routes signal events and maintains swarm topology.
34
34
  */
35
35
  export class Swarm {
36
- private readonly _connections = new ComplexMap<PublicKey, Connection>(PublicKey.hash);
37
- private readonly _discoveredPeers = new ComplexSet<PublicKey>(PublicKey.hash);
36
+ private readonly _peers = new ComplexMap<PublicKey, Peer>(PublicKey.hash);
37
+
38
38
  private readonly _swarmMessenger: MessageRouter;
39
39
 
40
+ private _destroyed = false;
41
+
40
42
  /**
41
43
  * Unique id of the swarm, local to the current peer, generated when swarm is joined.
42
44
  */
@@ -44,21 +46,26 @@ export class Swarm {
44
46
 
45
47
  /**
46
48
  * New connection to a peer is started.
49
+ * @internal
47
50
  */
48
51
  readonly connectionAdded = new Event<Connection>();
49
52
 
50
53
  /**
51
54
  * Connection to a peer is dropped.
55
+ * @internal
52
56
  */
53
- readonly connectionRemoved = new Event<Connection>();
57
+ readonly disconnected = new Event<PublicKey>();
54
58
 
55
59
  /**
56
60
  * Connection is established to a new peer.
61
+ * @internal
57
62
  */
58
63
  readonly connected = new Event<PublicKey>();
59
64
 
60
65
  readonly errors = new ErrorStream();
61
66
 
67
+ // TODO(burdon): Swarm => Peer.create/destroy =< Connection.open/close
68
+
62
69
  // TODO(burdon): Split up properties.
63
70
  constructor(
64
71
  private readonly _topic: PublicKey,
@@ -69,7 +76,7 @@ export class Swarm {
69
76
  private readonly _transportFactory: TransportFactory,
70
77
  private readonly _label: string | undefined
71
78
  ) {
72
- log('creating swarm', { topic: this._topic, peerId: _ownPeerId });
79
+ log('creating swarm', { id: this.id, topic: this._topic, peerId: _ownPeerId });
73
80
  _topology.init(this._getSwarmController());
74
81
 
75
82
  this._swarmMessenger = new MessageRouter({
@@ -89,7 +96,9 @@ export class Swarm {
89
96
  }
90
97
 
91
98
  get connections() {
92
- return Array.from(this._connections.values());
99
+ return Array.from(this._peers.values())
100
+ .map((peer) => peer.connection)
101
+ .filter(isNotNullOrUndefined);
93
102
  }
94
103
 
95
104
  get ownPeerId() {
@@ -107,40 +116,123 @@ export class Swarm {
107
116
  return this._topic;
108
117
  }
109
118
 
119
+ // TODO(burdon): async open?
120
+ async destroy() {
121
+ log('destroying', { id: this.id, topic: this._topic });
122
+ this._destroyed = true;
123
+ await this._topology.destroy();
124
+ await Promise.all(Array.from(this._peers.keys()).map((key) => this._closeConnection(key)));
125
+ }
126
+
127
+ async setTopology(topology: Topology) {
128
+ if (topology === this._topology) {
129
+ return;
130
+ }
131
+ log('setting topology', {
132
+ id: this.id,
133
+ topic: this._topic,
134
+ previous: getClassName(this._topology),
135
+ topology: getClassName(topology)
136
+ });
137
+
138
+ await this._topology.destroy();
139
+ this._topology = topology;
140
+ this._topology.init(this._getSwarmController());
141
+ this._topology.update();
142
+ }
143
+
144
+ private _getOrCreatePeer(peerId: PublicKey): Peer {
145
+ let peer = this._peers.get(peerId);
146
+ if (!peer) {
147
+ peer = new Peer(peerId, this._topic, this._ownPeerId, {
148
+ onConnected: () => {
149
+ this.connected.emit(peerId);
150
+ },
151
+ onDisconnected: async () => {
152
+ if (!peer!.advertizing) {
153
+ await this._destroyPeer(peer!.id);
154
+ }
155
+
156
+ this.disconnected.emit(peerId);
157
+ this._topology.update();
158
+ },
159
+ onRejected: () => {
160
+ // If the peer rejected our connection remove it from the set of candidates.
161
+ // TODO(dmaretskyi): Set flag instead.
162
+ if (this._peers.has(peerId)) {
163
+ void this._destroyPeer(peerId);
164
+ }
165
+ },
166
+ onAccepted: () => {
167
+ this._topology.update();
168
+ }
169
+ });
170
+ this._peers.set(peerId, peer);
171
+ }
172
+
173
+ return peer;
174
+ }
175
+
176
+ private async _destroyPeer(peerId: PublicKey) {
177
+ assert(this._peers.has(peerId));
178
+ await this._peers.get(peerId)!.destroy();
179
+ this._peers.delete(peerId);
180
+ }
181
+
110
182
  onSwarmEvent(swarmEvent: SwarmEvent) {
111
- log('swarm event', { topic: this._topic, swarmEvent });
183
+ log('swarm event', { id: this.id, topic: this._topic, swarmEvent }); // TODO(burdon): Stringify.
184
+ if (this._destroyed) {
185
+ log.warn('ignored for destroyed swarm', { peerId: this.id, topic: this._topic });
186
+ return;
187
+ }
188
+
112
189
  if (swarmEvent.peerAvailable) {
113
190
  const peerId = PublicKey.from(swarmEvent.peerAvailable.peer);
114
- log('new peer', { topic: this._topic, peerId });
191
+ log('new peer', { id: this.id, topic: this._topic, peerId });
115
192
  if (!peerId.equals(this._ownPeerId)) {
116
- this._discoveredPeers.add(peerId);
193
+ const peer = this._getOrCreatePeer(peerId);
194
+ peer.advertizing = true;
117
195
  }
118
196
  } else if (swarmEvent.peerLeft) {
119
- this._discoveredPeers.delete(PublicKey.from(swarmEvent.peerLeft.peer));
197
+ const peer = this._peers.get(PublicKey.from(swarmEvent.peerLeft.peer));
198
+ if (peer) {
199
+ peer.advertizing = false;
200
+ if (!peer.connection) {
201
+ void this._destroyPeer(peer.id);
202
+ }
203
+ }
120
204
  }
121
205
 
122
206
  this._topology.update();
123
207
  }
124
208
 
125
209
  async onOffer(message: OfferMessage): Promise<Answer> {
126
- log('offer', { topic: this._topic, message });
210
+ log('offer', { id: this.id, topic: this._topic, message });
211
+ if (this._destroyed) {
212
+ log.warn('ignored for destroyed swarm', { peerId: this.id, topic: this._topic });
213
+ return { accept: false };
214
+ }
215
+
127
216
  // Id of the peer offering us the connection.
128
217
  assert(message.author);
129
218
  const remoteId = message.author;
130
219
  if (!message.recipient?.equals(this._ownPeerId)) {
131
- log('rejecting offer with incorrect peerId', { topic: this._topic, message });
220
+ log('rejecting offer with incorrect peerId', { id: this.id, topic: this._topic, message });
132
221
  return { accept: false };
133
222
  }
134
223
  if (!message.topic?.equals(this._topic)) {
135
- log('rejecting offer with incorrect topic', { topic: this._topic, message });
224
+ log('rejecting offer with incorrect topic', { id: this.id, topic: this._topic, message });
136
225
  return { accept: false };
137
226
  }
138
227
 
228
+ const peer = this._getOrCreatePeer(remoteId);
229
+
139
230
  // Check if we are already trying to connect to that peer.
140
- if (this._connections.has(remoteId)) {
141
- // Peer with the highest Id closes it's connection, and accepts remote peer's offer.
231
+ if (peer.connection) {
232
+ // Peer with the highest Id closes its connection, and accepts remote peer's offer.
142
233
  if (remoteId.toHex() < this._ownPeerId.toHex()) {
143
234
  log("closing local connection and accepting remote peer's offer", {
235
+ id: this.id,
144
236
  topic: this._topic,
145
237
  peerId: this._ownPeerId
146
238
  });
@@ -156,12 +248,12 @@ export class Swarm {
156
248
 
157
249
  let accept = false;
158
250
  if (await this._topology.onOffer(remoteId)) {
159
- if (!this._connections.has(remoteId)) {
251
+ if (!peer.connection) {
160
252
  // Connection might have been already established.
161
253
  assert(message.sessionId);
162
254
  const connection = this._createConnection(false, message.author, message.sessionId);
163
255
  try {
164
- connection.open();
256
+ connection.openConnection();
165
257
  } catch (err: any) {
166
258
  this.errors.raise(err);
167
259
  }
@@ -175,49 +267,32 @@ export class Swarm {
175
267
  }
176
268
 
177
269
  async onSignal(message: SignalMessage): Promise<void> {
178
- log('signal', { topic: this._topic, message });
270
+ log('signal', { id: this.id, topic: this._topic, message });
271
+ if (this._destroyed) {
272
+ log.warn('ignored for destroyed swarm', { peerId: this.id, topic: this._topic });
273
+ return;
274
+ }
179
275
  assert(
180
276
  message.recipient?.equals(this._ownPeerId),
181
277
  `Invalid signal peer id expected=${this.ownPeerId}, actual=${message.recipient}`
182
278
  );
183
279
  assert(message.topic?.equals(this._topic));
184
280
  assert(message.author);
185
- const connection = this._connections.get(message.author);
186
- if (!connection) {
187
- log('dropping signal message for non-existent connection', { topic: this._topic, message });
188
- return;
189
- }
190
281
 
191
- await connection.signal(message);
192
- }
193
-
194
- async setTopology(topology: Topology) {
195
- if (topology === this._topology) {
196
- return;
197
- }
198
- log('setting topology', {
199
- topic: this._topic,
200
- previous: getClassName(this._topology),
201
- topology: getClassName(topology)
202
- });
203
- await this._topology.destroy();
204
- this._topology = topology;
205
- this._topology.init(this._getSwarmController());
206
- this._topology.update();
207
- }
208
-
209
- async destroy() {
210
- log('destroying', { topic: this._topic });
211
- await this._topology.destroy();
212
- await Promise.all(Array.from(this._connections.keys()).map((key) => this._closeConnection(key)));
282
+ const peer = this._getOrCreatePeer(message.author);
283
+ await peer.onSignal(message);
213
284
  }
214
285
 
215
286
  private _getSwarmController(): SwarmController {
216
287
  return {
217
288
  getState: () => ({
218
289
  ownPeerId: this._ownPeerId,
219
- connected: Array.from(this._connections.keys()),
220
- candidates: Array.from(this._discoveredPeers.keys()).filter((key) => !this._connections.has(key))
290
+ connected: Array.from(this._peers.values())
291
+ .filter((peer) => peer.connection)
292
+ .map((peer) => peer.id),
293
+ candidates: Array.from(this._peers.values())
294
+ .filter((peer) => !peer.connection && peer.advertizing)
295
+ .map((peer) => peer.id)
221
296
  }),
222
297
  connect: (peer) => this._initiateConnection(peer),
223
298
  disconnect: async (peer) => {
@@ -231,7 +306,9 @@ export class Swarm {
231
306
  };
232
307
  }
233
308
 
234
- // TODO(burdon): Make async.
309
+ /**
310
+ * Creates a connection then sends message over signal network.
311
+ */
235
312
  private async _initiateConnection(remoteId: PublicKey) {
236
313
  // It is likely that the other peer will also try to connect to us at the same time.
237
314
  // If our peerId is higher, we will wait for a bit so that other peer has a chance to connect first.
@@ -239,90 +316,44 @@ export class Swarm {
239
316
  await sleep(INITIATION_DELAY);
240
317
  }
241
318
 
242
- if (this._connections.has(remoteId)) {
319
+ if (this._peers.get(remoteId)?.connection) {
243
320
  // Do nothing if peer is already connected.
244
321
  return;
245
322
  }
246
323
 
247
324
  const sessionId = PublicKey.random();
248
325
 
249
- log('initiating...', { topic: this._topic, peerId: remoteId, sessionId });
326
+ log('initiating...', { id: this.id, topic: this._topic, peerId: remoteId, sessionId });
250
327
  const connection = this._createConnection(true, remoteId, sessionId);
251
328
  connection.initiate();
252
329
  this._topology.update();
253
- log('initiated', { topic: this._topic });
330
+ log('initiated', { id: this.id, topic: this._topic });
254
331
  }
255
332
 
256
- // TODO(burdon): Make async.
257
- private _createConnection(initiator: boolean, remoteId: PublicKey, sessionId: PublicKey) {
258
- log('creating connection', { topic: this._topic, peerId: this._ownPeerId, remoteId, initiator });
259
- assert(!this._connections.has(remoteId), 'Peer already connected.');
333
+ /**
334
+ * Synchronously create a connection, which must be initialized.
335
+ */
336
+ private _createConnection(initiator: boolean, remoteId: PublicKey, sessionId: PublicKey): Connection {
337
+ const peer = this._getOrCreatePeer(remoteId);
260
338
 
261
- const connection = new Connection(
262
- this._topic,
263
- this._ownPeerId,
264
- remoteId,
265
- sessionId,
266
- initiator,
339
+ const connection = peer.createConnection(
267
340
  this._swarmMessenger,
341
+ initiator,
342
+ sessionId,
268
343
  this._protocolProvider({ channel: discoveryKey(this._topic), initiator }),
269
344
  this._transportFactory
270
345
  );
271
346
 
272
- this._connections.set(remoteId, connection);
273
347
  this.connectionAdded.emit(connection);
274
-
275
- connection.errors.handle((err) => {
276
- // TODO(burdon): Change to warn? Why does this fail during tests?
277
- log('connection failed', { topic: this._topic, peerId: this._ownPeerId, remoteId, initiator, err });
278
- this._closeConnection(remoteId).catch((err) => this.errors.raise(err));
279
- });
280
-
281
- connection.stateChanged.on((state) => {
282
- switch (state) {
283
- case ConnectionState.CONNECTED: {
284
- this.connected.emit(remoteId);
285
- break;
286
- }
287
-
288
- case ConnectionState.REJECTED: {
289
- // If the peer rejected our connection remove it from the set of candidates.
290
- this._discoveredPeers.delete(remoteId);
291
- break;
292
- }
293
-
294
- case ConnectionState.ACCEPTED: {
295
- this._topology.update();
296
- break;
297
- }
298
-
299
- case ConnectionState.CLOSED: {
300
- log('connection closed', { topic: this._topic, peerId: this._ownPeerId, remoteId, initiator });
301
- // Connection might have been already closed or replace by a different one.
302
- // Only remove the connection if it has the same session id.
303
- if (this._connections.get(remoteId)?.sessionId.equals(sessionId)) {
304
- this._connections.delete(remoteId);
305
- this.connectionRemoved.emit(connection);
306
- this._topology.update();
307
- }
308
- break;
309
- }
310
- }
311
- });
312
-
313
348
  return connection;
314
349
  }
315
350
 
316
- // TODO(burdon): Make async.
317
351
  private async _closeConnection(peerId: PublicKey) {
318
- log('closing...', { topic: this._topic, peerId });
319
- const connection = this._connections.get(peerId);
320
- if (!connection) {
352
+ const peer = this._peers.get(peerId);
353
+ if (!peer) {
321
354
  return;
322
355
  }
323
356
 
324
- this._connections.delete(peerId);
325
- await connection.close();
326
- log('closed', { topic: this._topic });
357
+ await peer.closeConnection();
327
358
  }
328
359
  }