@dxos/network-manager 0.1.8 → 0.1.10

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 (153) 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 +25 -18
  112. package/src/index.ts +2 -2
  113. package/src/network-manager.ts +30 -45
  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/dist/src/network-manager.test.d.ts +0 -2
  144. package/dist/src/network-manager.test.d.ts.map +0 -1
  145. package/dist/src/network-manager.test.js +0 -596
  146. package/dist/src/network-manager.test.js.map +0 -1
  147. package/dist/src/presence.test.d.ts.map +0 -1
  148. package/dist/src/presence.test.js.map +0 -1
  149. package/dist/src/signal/signal-messaging.js.map +0 -1
  150. package/dist/src/transport/wrtc-simple-peer.test.d.ts +0 -2
  151. package/dist/src/transport/wrtc-simple-peer.test.d.ts.map +0 -1
  152. package/dist/src/transport/wrtc-simple-peer.test.js.map +0 -1
  153. package/src/network-manager.test.ts +0 -707
@@ -0,0 +1,221 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import * as fc from 'fast-check';
6
+ import { ModelRunSetup } from 'fast-check';
7
+ import waitForExpect from 'wait-for-expect';
8
+
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { PresencePlugin } from '@dxos/protocol-plugin-presence';
11
+ import { afterTest } from '@dxos/testutils';
12
+ import { range, ComplexMap, ComplexSet } from '@dxos/util';
13
+
14
+ import { NetworkManager } from '../network-manager';
15
+ import { createProtocolFactory } from '../protocol-factory';
16
+ import { FullyConnectedTopology } from '../topology';
17
+
18
+ /**
19
+ * Performs random actions in the real system and compares its state with a simplified model.
20
+ */
21
+ // TODO(dmaretskyi): Run this with actual webrtc and signal servers.
22
+ export const propertyTestSuite = () => {
23
+ it.skip('property-based tests', async function () {
24
+ /**
25
+ * The simplified model of the system.
26
+ */
27
+ interface Model {
28
+ topic: PublicKey;
29
+ peers: ComplexSet<PublicKey>;
30
+ joinedPeers: ComplexSet<PublicKey>;
31
+ }
32
+
33
+ /**
34
+ * The real system being tested.
35
+ */
36
+ interface Real {
37
+ peers: ComplexMap<PublicKey, { networkManager: NetworkManager; presence?: PresencePlugin }>;
38
+ }
39
+
40
+ const assertState = async (model: Model, real: Real) => {
41
+ await waitForExpect(() => {
42
+ for (const peer of real.peers.values()) {
43
+ if (peer.presence) {
44
+ for (const expectedPeerId of model.joinedPeers) {
45
+ if (expectedPeerId.equals(peer.presence.peerId)) {
46
+ continue;
47
+ }
48
+
49
+ const actuallyConnectedPeers = peer.presence!.peers;
50
+ if (!actuallyConnectedPeers.some((peer) => PublicKey.equals(expectedPeerId, peer))) {
51
+ // TODO(burdon): More concise error.
52
+ const context = {
53
+ peerId: peer.presence.peerId,
54
+ expectedPeerId: expectedPeerId.truncate(),
55
+ connectedPeerIds: actuallyConnectedPeers.map((key) => key.toString('hex'))
56
+ };
57
+
58
+ throw new Error(`Expected peer to be in the list of joined peers: ${context}`);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }, 5_000);
64
+
65
+ real.peers.forEach((peer) =>
66
+ peer.networkManager.topics.forEach((topic) => {
67
+ peer.networkManager.getSwarm(topic)!.errors.assertNoUnhandledErrors();
68
+ })
69
+ );
70
+ };
71
+
72
+ class CreatePeerCommand implements fc.AsyncCommand<Model, Real> {
73
+ constructor(readonly peerId: PublicKey) {}
74
+
75
+ check = (model: Model) => !model.peers.has(this.peerId);
76
+ async run(model: Model, real: Real) {
77
+ // TODO(burdon): ???
78
+ throw new Error('Not implemented.');
79
+ // model.peers.add(this.peerId);
80
+ // const networkManager = testBuilder.createNetworkManager();
81
+ // afterTest(() => networkManager.close());
82
+
83
+ // real.peers.set(this.peerId, {
84
+ // networkManager
85
+ // });
86
+
87
+ // await assertState(model, real);
88
+ }
89
+
90
+ toString = () => `CreatePeer(${this.peerId})`;
91
+ }
92
+
93
+ class RemovePeerCommand implements fc.AsyncCommand<Model, Real> {
94
+ constructor(readonly peerId: PublicKey) {}
95
+
96
+ check = (model: Model) => model.peers.has(this.peerId);
97
+ async run(model: Model, real: Real) {
98
+ model.peers.delete(this.peerId);
99
+ model.joinedPeers.delete(this.peerId);
100
+
101
+ const peer = real.peers.get(this.peerId);
102
+ await peer!.networkManager.close();
103
+ real.peers.delete(this.peerId);
104
+
105
+ await assertState(model, real);
106
+ }
107
+
108
+ toString = () => `RemovePeer(${this.peerId})`;
109
+ }
110
+
111
+ class JoinTopicCommand implements fc.AsyncCommand<Model, Real> {
112
+ constructor(readonly peerId: PublicKey) {}
113
+
114
+ check = (model: Model) => model.peers.has(this.peerId) && !model.joinedPeers.has(this.peerId);
115
+ async run(model: Model, real: Real) {
116
+ model.joinedPeers.add(this.peerId);
117
+
118
+ const peer = real.peers.get(this.peerId)!;
119
+
120
+ const presence = new PresencePlugin(this.peerId.asBuffer());
121
+ afterTest(() => presence.stop());
122
+ const protocol = createProtocolFactory(model.topic, this.peerId, [presence]);
123
+
124
+ await peer.networkManager.joinSwarm({
125
+ peerId: this.peerId, // TODO(burdon): `this`?
126
+ topic: model.topic,
127
+ protocol,
128
+ topology: new FullyConnectedTopology(),
129
+ presence
130
+ });
131
+
132
+ peer.presence = presence;
133
+
134
+ await assertState(model, real);
135
+ }
136
+
137
+ toString = () => `JoinTopic(peerId=${this.peerId})`;
138
+ }
139
+
140
+ class LeaveTopicCommand implements fc.AsyncCommand<Model, Real> {
141
+ constructor(readonly peerId: PublicKey) {}
142
+
143
+ check = (model: Model) => model.peers.has(this.peerId) && model.joinedPeers.has(this.peerId);
144
+ async run(model: Model, real: Real) {
145
+ model.joinedPeers.delete(this.peerId);
146
+
147
+ const peer = real.peers.get(this.peerId)!;
148
+ await peer.networkManager.leaveSwarm(model.topic);
149
+ peer.presence = undefined;
150
+
151
+ await assertState(model, real);
152
+ }
153
+
154
+ toString = () => `LeaveTopic(peerId=${this.peerId})`;
155
+ }
156
+
157
+ const peerIds = range(10).map(() => PublicKey.random());
158
+ const peerId1 = fc.constantFrom(...peerIds);
159
+
160
+ const allCommands = [
161
+ peerId1.map((x) => new CreatePeerCommand(x)),
162
+ peerId1.map((x) => new RemovePeerCommand(x)),
163
+ peerId1.map((p) => new JoinTopicCommand(p)),
164
+ peerId1.map((p) => new LeaveTopicCommand(p))
165
+ ];
166
+
167
+ await fc.assert(
168
+ fc.asyncProperty(fc.commands(allCommands, { maxCommands: 30 }), async (cmds) => {
169
+ const setup: ModelRunSetup<Model, Real> = () => ({
170
+ model: {
171
+ topic: PublicKey.random(),
172
+ peers: new ComplexSet(PublicKey.hash),
173
+ joinedPeers: new ComplexSet(PublicKey.hash)
174
+ },
175
+ real: {
176
+ peers: new ComplexMap(PublicKey.hash)
177
+ }
178
+ });
179
+
180
+ await fc.asyncModelRun(setup, cmds);
181
+ }),
182
+ {
183
+ examples: [
184
+ [
185
+ [
186
+ new CreatePeerCommand(peerIds[0]),
187
+ new CreatePeerCommand(peerIds[1]),
188
+ new JoinTopicCommand(peerIds[0]),
189
+ new JoinTopicCommand(peerIds[1]),
190
+ new LeaveTopicCommand(peerIds[0]),
191
+ new LeaveTopicCommand(peerIds[1]),
192
+ new RemovePeerCommand(peerIds[0]),
193
+ new RemovePeerCommand(peerIds[1])
194
+ ]
195
+ ],
196
+ [
197
+ [
198
+ new CreatePeerCommand(peerIds[0]),
199
+ new JoinTopicCommand(peerIds[0]),
200
+ new CreatePeerCommand(peerIds[1]),
201
+ new RemovePeerCommand(peerIds[0]),
202
+ new JoinTopicCommand(peerIds[1]),
203
+ new RemovePeerCommand(peerIds[1])
204
+ ]
205
+ ],
206
+ [
207
+ [
208
+ new CreatePeerCommand(peerIds[0]),
209
+ new JoinTopicCommand(peerIds[0]),
210
+ new RemovePeerCommand(peerIds[0]),
211
+ new CreatePeerCommand(peerIds[1]),
212
+ new JoinTopicCommand(peerIds[1]),
213
+ new CreatePeerCommand(peerIds[2]),
214
+ new JoinTopicCommand(peerIds[2])
215
+ ]
216
+ ]
217
+ ]
218
+ }
219
+ );
220
+ });
221
+ };
@@ -0,0 +1,85 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { latch, Trigger } from '@dxos/async';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { afterTest } from '@dxos/testutils';
10
+ import { Provider } from '@dxos/util';
11
+
12
+ import { TestPeer, TestSwarmConnection } from '../testing';
13
+ import { Topology } from '../topology';
14
+
15
+ //
16
+ // NOTE: Don't move to testing directory since not exportable outside of package.
17
+ //
18
+
19
+ /**
20
+ * Open peers and close after tests complete.
21
+ */
22
+ export const openAndCloseAfterTest = async (peers: TestPeer[]) => {
23
+ await Promise.all(peers.map((peer) => peer.open()));
24
+ afterTest(async () => {
25
+ await Promise.all(peers.map((peer) => peer.close()));
26
+ });
27
+ };
28
+
29
+ /**
30
+ * Join and cleanly leave swarm.
31
+ */
32
+ export const joinSwarm = async (peers: TestPeer[], topic: PublicKey, topology?: Provider<Topology>) => {
33
+ const [connected, connect] = latch({ count: peers.length });
34
+ const swarms = peers.map((peer) => {
35
+ const swarm = peer.createSwarm(topic);
36
+ swarm.plugin.once('connect', connect);
37
+ return swarm;
38
+ });
39
+
40
+ await Promise.all(swarms.map((swarm) => swarm.join(topology?.())));
41
+ await connected();
42
+ return swarms;
43
+ };
44
+
45
+ /**
46
+ * Cleanly leave swarm.
47
+ */
48
+ export const leaveSwarm = async (peers: TestPeer[], topic: PublicKey) => {
49
+ const [disconnected, disconnect] = latch({ count: peers.length });
50
+ const swarms = peers.map((peer) => {
51
+ const swarm = peer.getSwarm(topic);
52
+ swarm.plugin.once('disconnect', disconnect);
53
+ return swarm;
54
+ });
55
+
56
+ await Promise.all(swarms.map((swarm) => swarm.leave()));
57
+ await disconnected();
58
+ return swarms;
59
+ };
60
+
61
+ /**
62
+ * Exchange messages between peers.
63
+ */
64
+ // TODO(burdon): Based on plugin instance.
65
+ // TODO(burdon): Configure to send more messages.
66
+ export const exchangeMessages = async (swarm1: TestSwarmConnection, swarm2: TestSwarmConnection) => {
67
+ {
68
+ const peer2Received = new Trigger<any>();
69
+ swarm2.plugin.on('receive', (peer, message) => peer2Received.wake(message));
70
+
71
+ // TODO(burdon): Message encoding?
72
+ await swarm1.plugin.send(swarm2.peer.peerId.asBuffer(), JSON.stringify({ message: 'ping' }));
73
+ const { message } = JSON.parse(await peer2Received.wait());
74
+ expect(message).to.eq('ping');
75
+ }
76
+
77
+ {
78
+ const peer1Received = new Trigger<any>();
79
+ swarm1.plugin.on('receive', (peer, message) => peer1Received.wake(message));
80
+
81
+ await swarm2.plugin.send(swarm1.peer.peerId.asBuffer(), JSON.stringify({ message: 'pong' }));
82
+ const { message } = JSON.parse(await peer1Received.wait());
83
+ expect(message).to.eq('pong');
84
+ }
85
+ };
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import { TEST_SIGNAL_URL, TestBuilder } from '../testing';
6
+ import { basicTestSuite } from './basic-test-suite';
7
+
8
+ describe('WebRTC transport', function () {
9
+ const testBuilder = new TestBuilder({ signalHosts: [TEST_SIGNAL_URL] });
10
+ basicTestSuite(testBuilder);
11
+ }).timeout(10_000);
12
+
13
+ describe('WebRTC transport proxy', function () {
14
+ const testBuilder = new TestBuilder({ signalHosts: [TEST_SIGNAL_URL], bridge: true });
15
+ basicTestSuite(testBuilder);
16
+ }).timeout(10_000);
@@ -11,6 +11,10 @@ import { SwarmController, Topology } from './topology';
11
11
  export class FullyConnectedTopology implements Topology {
12
12
  private _controller?: SwarmController;
13
13
 
14
+ toString() {
15
+ return 'FullyConnectedTopology';
16
+ }
17
+
14
18
  init(controller: SwarmController): void {
15
19
  assert(!this._controller, 'Already initialized');
16
20
  this._controller = controller;
@@ -20,6 +24,7 @@ export class FullyConnectedTopology implements Topology {
20
24
  assert(this._controller, 'Not initialized');
21
25
  const { candidates: discovered } = this._controller.getState();
22
26
  for (const peer of discovered) {
27
+ // TODO(burdon): Back-off.
23
28
  this._controller.connect(peer);
24
29
  }
25
30
  }
@@ -31,8 +36,4 @@ export class FullyConnectedTopology implements Topology {
31
36
  async destroy(): Promise<void> {
32
37
  // Nothing to do.
33
38
  }
34
-
35
- toString() {
36
- return 'FullyConnectedTopology';
37
- }
38
39
  }
@@ -41,10 +41,7 @@ export interface SwarmState {
41
41
  export interface Topology {
42
42
  /**
43
43
  * Called when swarm is created.
44
- *
45
44
  * May be used to bind the swarm controller and initialize any asynchronous actions.
46
- *
47
- * @param controller
48
45
  */
49
46
  init(controller: SwarmController): void;
50
47
 
@@ -55,14 +52,12 @@ export interface Topology {
55
52
 
56
53
  /**
57
54
  * Called when remote peer offers a connection.
58
- *
59
55
  * @returns true - to accept the connection, false - to reject.
60
56
  */
61
57
  onOffer(peer: PublicKey): Promise<boolean>;
62
58
 
63
59
  /**
64
60
  * Called when swarm is destroyed or topology is changed.
65
- *
66
61
  * Any error thrown here will be a critical error for the swarm.
67
62
  */
68
63
  destroy(): Promise<void>;
@@ -18,6 +18,7 @@ import { MemoryTransport } from './memory-transport';
18
18
  // Cannot log after tests are done. Did you forget to wait for something async in your test?
19
19
  // Attempted to log "Ignoring unsupported ICE candidate.".
20
20
 
21
+ // TODO(burdon): Move to TestBuilder.
21
22
  const createPair = () => {
22
23
  const topic = PublicKey.random();
23
24
  const peer1Id = PublicKey.random();
@@ -33,7 +34,7 @@ const createPair = () => {
33
34
  initiator: true
34
35
  });
35
36
 
36
- afterTest(() => connection1.close());
37
+ afterTest(() => connection1.destroy());
37
38
  afterTest(() => connection1.errors.assertNoUnhandledErrors());
38
39
 
39
40
  const plugin2 = new TestProtocolPlugin(peer2Id.asBuffer());
@@ -46,7 +47,7 @@ const createPair = () => {
46
47
  initiator: false
47
48
  });
48
49
 
49
- afterTest(() => connection2.close());
50
+ afterTest(() => connection2.destroy());
50
51
  afterTest(() => connection2.errors.assertNoUnhandledErrors());
51
52
 
52
53
  return {
@@ -14,91 +14,111 @@ import { ComplexMap } from '@dxos/util';
14
14
 
15
15
  import { Transport, TransportFactory, TransportOptions } from './transport';
16
16
 
17
+ // TODO(burdon): Make configurable.
17
18
  // Delay (in milliseconds) for data being sent through in-memory connections to simulate network latency.
18
19
  const MEMORY_TRANSPORT_DELAY = 1;
19
20
 
20
21
  /**
21
22
  * Creates a binary stream that delays data being sent through the stream by the specified amount of time.
22
23
  */
23
- const createStreamDelay = (delay: number): NodeJS.ReadWriteStream =>
24
- new Transform({
24
+ const createStreamDelay = (delay: number): NodeJS.ReadWriteStream => {
25
+ return new Transform({
25
26
  objectMode: true,
26
27
  transform: (chunk, _, cb) => {
27
28
  setTimeout(() => cb(null, chunk), delay); // TODO(burdon): Randomize.
28
29
  }
29
30
  });
31
+ };
32
+
33
+ export const MemoryTransportFactory: TransportFactory = {
34
+ createTransport: (params) => new MemoryTransport(params)
35
+ };
30
36
 
31
37
  /**
32
38
  * Fake transport.
33
39
  */
34
- // TODO(burdon): Remove static variables.
35
40
  export class MemoryTransport implements Transport {
36
- // TODO(burdon): Remove global properties.
41
+ // TODO(burdon): Remove static properties (inject context into constructor).
37
42
  private static readonly _connections = new ComplexMap<PublicKey, MemoryTransport>(PublicKey.hash);
38
43
 
39
44
  public readonly closed = new Event<void>();
40
45
  public readonly connected = new Event<void>();
41
46
  public readonly errors = new ErrorStream();
42
47
 
43
- private readonly _ownId = PublicKey.random();
44
- private _remoteId!: PublicKey;
48
+ private readonly _id = PublicKey.random(); // TODO(burdon): Rename peerId? (Use local/remote labels in logs).
45
49
  private readonly _remote = new Trigger<PublicKey>();
46
50
 
47
51
  private readonly _outgoingDelay = createStreamDelay(MEMORY_TRANSPORT_DELAY);
48
52
  private readonly _incomingDelay = createStreamDelay(MEMORY_TRANSPORT_DELAY);
49
53
 
54
+ private _destroyed = false;
55
+ private _remoteId!: PublicKey;
50
56
  private _remoteConnection?: MemoryTransport;
51
57
 
52
- constructor(private readonly params: TransportOptions) {
53
- log('creating', this._ownId);
54
-
55
- if (this.params.initiator) {
56
- setTimeout(async () => this.params.sendSignal({ json: JSON.stringify({ transportId: this._ownId.toHex() }) }));
57
- }
58
-
59
- assert(!MemoryTransport._connections.has(this._ownId), 'Duplicate memory connection');
60
- MemoryTransport._connections.set(this._ownId, this);
61
-
62
- this._remote.wait().then(
63
- (remoteId) => {
64
- this._remoteId = remoteId;
65
- this._remoteConnection = MemoryTransport._connections.get(this._remoteId);
66
- if (this._remoteConnection) {
58
+ constructor(private readonly options: TransportOptions) {
59
+ log('creating', { id: this._id });
60
+
61
+ assert(!MemoryTransport._connections.has(this._id), 'Duplicate memory connection');
62
+ MemoryTransport._connections.set(this._id, this);
63
+
64
+ // Initiator will send a signal, the receiver will receive the unique ID and connect the streams.
65
+ if (this.options.initiator) {
66
+ // prettier-ignore
67
+ setTimeout(async () => {
68
+ log('sending signal', { transportId: this._id });
69
+ void this.options.sendSignal({
70
+ json: JSON.stringify({ transportId: this._id.toHex() })
71
+ });
72
+ });
73
+ } else {
74
+ this._remote
75
+ .wait({ timeout: this.options.timeout ?? 1000 })
76
+ .then((remoteId) => {
77
+ if (this._destroyed) {
78
+ return;
79
+ }
80
+
81
+ this._remoteId = remoteId;
82
+ this._remoteConnection = MemoryTransport._connections.get(this._remoteId);
83
+ if (!this._remoteConnection) {
84
+ // Remote connection was destroyed before we could connect.
85
+ this._destroyed = true;
86
+ this.closed.emit();
87
+ return;
88
+ }
89
+
90
+ assert(!this._remoteConnection._remoteConnection, new Error(`Remote already connected: ${this._remoteId}`));
67
91
  this._remoteConnection._remoteConnection = this;
68
- this._remoteConnection._remoteId = this._ownId;
92
+ this._remoteConnection._remoteId = this._id;
69
93
 
70
- log('connected', { ownId: this._ownId, remoteId: this._remoteId });
71
- this.params.stream
94
+ log('connected', { id: this._id, remote: this._remoteId });
95
+ this.options.stream
72
96
  .pipe(this._outgoingDelay)
73
- .pipe(this._remoteConnection.params.stream)
97
+ .pipe(this._remoteConnection.options.stream)
74
98
  .pipe(this._incomingDelay)
75
- .pipe(this.params.stream);
99
+ .pipe(this.options.stream);
76
100
 
77
101
  this.connected.emit();
78
102
  this._remoteConnection.connected.emit();
79
- }
80
- },
81
- async (err) => {
82
- log.catch(err);
83
- }
84
- );
85
- }
86
-
87
- async signal(signal: Signal) {
88
- const { json } = signal;
89
- if (json) {
90
- const { transportId } = JSON.parse(json);
91
- if (transportId) {
92
- this._remote.wake(PublicKey.from(transportId));
93
- }
103
+ })
104
+ .catch((err) => {
105
+ if (this._destroyed) {
106
+ return;
107
+ }
108
+
109
+ this.errors.raise(err);
110
+ });
94
111
  }
95
112
  }
96
113
 
97
- async close(): Promise<void> {
98
- log('closing', this._ownId);
114
+ async destroy(): Promise<void> {
115
+ log('closing', { id: this._id });
116
+ this._destroyed = true;
99
117
 
100
- MemoryTransport._connections.delete(this._ownId);
118
+ MemoryTransport._connections.delete(this._id);
101
119
  if (this._remoteConnection) {
120
+ log('closing', { id: this._remoteId });
121
+ this._remoteConnection._destroyed = true;
102
122
  MemoryTransport._connections.delete(this._remoteId);
103
123
 
104
124
  // TODO(dmaretskyi): Hypercore streams do not seem to have the unpipe method.
@@ -115,13 +135,23 @@ export class MemoryTransport implements Transport {
115
135
  this._remoteConnection.closed.emit();
116
136
  this._remoteConnection._remoteConnection = undefined;
117
137
  this._remoteConnection = undefined;
138
+ log('closed', { id: this._remoteId });
118
139
  }
119
140
 
120
141
  this.closed.emit();
121
- log('closed');
142
+ log('closed', { id: this._id });
122
143
  }
123
- }
124
144
 
125
- export const MemoryTransportFactory: TransportFactory = {
126
- create: (params) => new MemoryTransport(params)
127
- };
145
+ signal(signal: Signal) {
146
+ const { json } = signal;
147
+ if (json) {
148
+ // TODO(burdon): Check open?
149
+ const { transportId } = JSON.parse(json);
150
+ if (transportId) {
151
+ const remoteId = PublicKey.fromHex(transportId);
152
+ log('received signal', { id: this._id, remoteId });
153
+ this._remote.wake(remoteId);
154
+ }
155
+ }
156
+ }
157
+ }
@@ -13,8 +13,9 @@ export interface Transport {
13
13
  closed: Event;
14
14
  connected: Event;
15
15
  errors: ErrorStream;
16
- signal(signal: Signal): Promise<void>; // TODO(burdon): Remove async?
17
- close(): Promise<void>;
16
+
17
+ destroy(): Promise<void>;
18
+ signal(signal: Signal): void;
18
19
  }
19
20
 
20
21
  export type TransportOptions = {
@@ -32,8 +33,13 @@ export type TransportOptions = {
32
33
  * Send a signal message to remote peer.
33
34
  */
34
35
  sendSignal: (signal: Signal) => Promise<void>; // TODO(burdon): Remove async?
36
+
37
+ timeout?: number;
35
38
  };
36
39
 
40
+ /**
41
+ *
42
+ */
37
43
  export interface TransportFactory {
38
- create(options: TransportOptions): Transport;
44
+ createTransport(options: TransportOptions): Transport;
39
45
  }
@@ -9,7 +9,7 @@ import SimplePeerConstructor from 'simple-peer';
9
9
 
10
10
  import { sleep } from '@dxos/async';
11
11
 
12
- describe('node wrtc and simple-peer', function () {
12
+ describe('Node WebRTC and simple-peer', function () {
13
13
  // Simplest test that reproduces SIGABRT (mac) and SIGSEGV (linux) in wrtc.
14
14
  it.skip('open and close', async function () {
15
15
  const peer = new SimplePeerConstructor({
@@ -73,7 +73,7 @@ describe('WebRTCTransportProxy', function () {
73
73
  sendSignal,
74
74
  bridgeService: rpcClient.rpc.BridgeService
75
75
  });
76
- afterTest(async () => await webRTCTransportProxy.close());
76
+ afterTest(async () => await webRTCTransportProxy.destroy());
77
77
 
78
78
  return { webRTCService: rpcService, webRTCTransportProxy };
79
79
  };
@@ -89,7 +89,7 @@ describe('WebRTCTransportProxy', function () {
89
89
  connection.closed.once(closedCb);
90
90
 
91
91
  await sleep(10); // Let simple-peer process events.
92
- await connection.close();
92
+ await connection.destroy();
93
93
 
94
94
  await sleep(1); // Process events.
95
95