@dxos/network-manager 0.1.9 → 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.
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/network-manager.d.ts +10 -14
- package/dist/src/network-manager.d.ts.map +1 -1
- package/dist/src/network-manager.js +28 -33
- package/dist/src/network-manager.js.map +1 -1
- package/dist/src/protocol-factory.d.ts.map +1 -1
- package/dist/src/protocol-factory.js +8 -6
- package/dist/src/protocol-factory.js.map +1 -1
- package/dist/src/signal/index.d.ts +2 -2
- package/dist/src/signal/index.d.ts.map +1 -1
- package/dist/src/signal/index.js +2 -2
- package/dist/src/signal/index.js.map +1 -1
- package/dist/src/signal/message-router.d.ts +2 -2
- package/dist/src/signal/message-router.js +3 -3
- package/dist/src/signal/message-router.js.map +1 -1
- package/dist/src/signal/{signal-messaging.d.ts → signal-messenger.d.ts} +2 -2
- package/dist/src/signal/{signal-messaging.d.ts.map → signal-messenger.d.ts.map} +1 -1
- package/dist/src/signal/{signal-messaging.js → signal-messenger.js} +1 -1
- package/dist/src/signal/signal-messenger.js.map +1 -0
- package/dist/src/swarm/connection.d.ts +6 -3
- package/dist/src/swarm/connection.d.ts.map +1 -1
- package/dist/src/swarm/connection.js +28 -15
- package/dist/src/swarm/connection.js.map +1 -1
- package/dist/src/swarm/peer.d.ts +45 -0
- package/dist/src/swarm/peer.d.ts.map +1 -0
- package/dist/src/swarm/peer.js +113 -0
- package/dist/src/swarm/peer.js.map +1 -0
- package/dist/src/swarm/swarm-mapper.d.ts +2 -2
- package/dist/src/swarm/swarm-mapper.d.ts.map +1 -1
- package/dist/src/swarm/swarm-mapper.js +12 -10
- package/dist/src/swarm/swarm-mapper.js.map +1 -1
- package/dist/src/swarm/swarm.d.ts +13 -18
- package/dist/src/swarm/swarm.d.ts.map +1 -1
- package/dist/src/swarm/swarm.js +126 -97
- package/dist/src/swarm/swarm.js.map +1 -1
- package/dist/src/swarm/swarm.test.js +2 -2
- package/dist/src/swarm/swarm.test.js.map +1 -1
- package/dist/src/testing/test-builder.d.ts +31 -18
- package/dist/src/testing/test-builder.d.ts.map +1 -1
- package/dist/src/testing/test-builder.js +120 -48
- package/dist/src/testing/test-builder.js.map +1 -1
- package/dist/src/testing/test-protocol.d.ts +5 -5
- package/dist/src/testing/test-protocol.d.ts.map +1 -1
- package/dist/src/testing/test-protocol.js +28 -24
- package/dist/src/testing/test-protocol.js.map +1 -1
- package/dist/src/tests/basic-test-suite.d.ts +6 -0
- package/dist/src/tests/basic-test-suite.d.ts.map +1 -0
- package/dist/src/tests/basic-test-suite.js +142 -0
- package/dist/src/tests/basic-test-suite.js.map +1 -0
- package/dist/src/tests/memory-transport.test.d.ts +2 -0
- package/dist/src/tests/memory-transport.test.d.ts.map +1 -0
- package/dist/src/tests/memory-transport.test.js +12 -0
- package/dist/src/tests/memory-transport.test.js.map +1 -0
- package/dist/src/{presence.test.d.ts → tests/presence.test.d.ts} +0 -0
- package/dist/src/tests/presence.test.d.ts.map +1 -0
- package/dist/src/{presence.test.js → tests/presence.test.js} +21 -20
- package/dist/src/tests/presence.test.js.map +1 -0
- package/dist/src/tests/property-test-suite.d.ts +5 -0
- package/dist/src/tests/property-test-suite.d.ts.map +1 -0
- package/dist/src/tests/property-test-suite.js +203 -0
- package/dist/src/tests/property-test-suite.js.map +1 -0
- package/dist/src/tests/utils.d.ts +21 -0
- package/dist/src/tests/utils.d.ts.map +1 -0
- package/dist/src/tests/utils.js +76 -0
- package/dist/src/tests/utils.js.map +1 -0
- package/dist/src/tests/webrtc-transport.test.d.ts +2 -0
- package/dist/src/tests/webrtc-transport.test.d.ts.map +1 -0
- package/dist/src/tests/webrtc-transport.test.js +16 -0
- package/dist/src/tests/webrtc-transport.test.js.map +1 -0
- package/dist/src/topology/fully-connected-topology.d.ts +1 -1
- package/dist/src/topology/fully-connected-topology.d.ts.map +1 -1
- package/dist/src/topology/fully-connected-topology.js +4 -3
- package/dist/src/topology/fully-connected-topology.js.map +1 -1
- package/dist/src/topology/mmst-topology.js +4 -4
- package/dist/src/topology/mmst-topology.js.map +1 -1
- package/dist/src/topology/star-topology.js +4 -4
- package/dist/src/topology/star-topology.js.map +1 -1
- package/dist/src/topology/topology.d.ts +0 -5
- package/dist/src/topology/topology.d.ts.map +1 -1
- package/dist/src/transport/memory-transport.d.ts +8 -7
- package/dist/src/transport/memory-transport.d.ts.map +1 -1
- package/dist/src/transport/memory-transport.js +79 -45
- package/dist/src/transport/memory-transport.js.map +1 -1
- package/dist/src/transport/memory-transport.test.js +3 -2
- package/dist/src/transport/memory-transport.test.js.map +1 -1
- package/dist/src/transport/transport.d.ts +8 -4
- package/dist/src/transport/transport.d.ts.map +1 -1
- package/dist/src/transport/webrtc-simple-peer.test.d.ts +2 -0
- package/dist/src/transport/webrtc-simple-peer.test.d.ts.map +1 -0
- package/dist/src/transport/{wrtc-simple-peer.test.js → webrtc-simple-peer.test.js} +2 -2
- package/dist/src/transport/webrtc-simple-peer.test.js.map +1 -0
- package/dist/src/transport/webrtc-transport-proxy.d.ts +4 -4
- package/dist/src/transport/webrtc-transport-proxy.d.ts.map +1 -1
- package/dist/src/transport/webrtc-transport-proxy.js +15 -10
- package/dist/src/transport/webrtc-transport-proxy.js.map +1 -1
- package/dist/src/transport/webrtc-transport-proxy.test.js +2 -2
- package/dist/src/transport/webrtc-transport-proxy.test.js.map +1 -1
- package/dist/src/transport/webrtc-transport-service.js +2 -2
- package/dist/src/transport/webrtc-transport-service.js.map +1 -1
- package/dist/src/transport/webrtc-transport.d.ts +3 -3
- package/dist/src/transport/webrtc-transport.d.ts.map +1 -1
- package/dist/src/transport/webrtc-transport.js +10 -10
- package/dist/src/transport/webrtc-transport.js.map +1 -1
- package/dist/src/transport/webrtc-transport.test.js +3 -3
- package/dist/src/transport/webrtc-transport.test.js.map +1 -1
- package/dist/src/types.d.ts +1 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +25 -18
- package/src/index.ts +2 -2
- package/src/network-manager.ts +30 -45
- package/src/protocol-factory.ts +5 -3
- package/src/signal/index.ts +2 -2
- package/src/signal/integration.test.ts +1 -1
- package/src/signal/message-router.test.ts +1 -1
- package/src/signal/message-router.ts +2 -2
- package/src/signal/{signal-messaging.ts → signal-messenger.ts} +2 -2
- package/src/swarm/connection.ts +23 -12
- package/src/swarm/peer.ts +156 -0
- package/src/swarm/swarm-mapper.ts +21 -10
- package/src/swarm/swarm.ts +142 -111
- package/src/testing/test-builder.ts +143 -63
- package/src/testing/test-protocol.ts +31 -23
- package/src/tests/basic-test-suite.ts +168 -0
- package/src/tests/memory-transport.test.ts +11 -0
- package/src/{presence.test.ts → tests/presence.test.ts} +24 -23
- package/src/tests/property-test-suite.ts +221 -0
- package/src/tests/utils.ts +85 -0
- package/src/tests/webrtc-transport.test.ts +16 -0
- package/src/topology/fully-connected-topology.ts +5 -4
- package/src/topology/topology.ts +0 -5
- package/src/transport/memory-transport.test.ts +3 -2
- package/src/transport/memory-transport.ts +79 -49
- package/src/transport/transport.ts +9 -3
- package/src/transport/{wrtc-simple-peer.test.ts → webrtc-simple-peer.test.ts} +1 -1
- package/src/transport/webrtc-transport-proxy.test.ts +2 -2
- package/src/transport/webrtc-transport-proxy.ts +17 -8
- package/src/transport/webrtc-transport-service.ts +1 -1
- package/src/transport/webrtc-transport.test.ts +4 -6
- package/src/transport/webrtc-transport.ts +4 -4
- package/dist/src/network-manager.test.d.ts +0 -2
- package/dist/src/network-manager.test.d.ts.map +0 -1
- package/dist/src/network-manager.test.js +0 -596
- package/dist/src/network-manager.test.js.map +0 -1
- package/dist/src/presence.test.d.ts.map +0 -1
- package/dist/src/presence.test.js.map +0 -1
- package/dist/src/signal/signal-messaging.js.map +0 -1
- package/dist/src/transport/wrtc-simple-peer.test.d.ts +0 -2
- package/dist/src/transport/wrtc-simple-peer.test.d.ts.map +0 -1
- package/dist/src/transport/wrtc-simple-peer.test.js.map +0 -1
- 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
|
}
|
package/src/topology/topology.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
53
|
-
log('creating', this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
92
|
+
this._remoteConnection._remoteId = this._id;
|
|
69
93
|
|
|
70
|
-
log('connected', {
|
|
71
|
-
this.
|
|
94
|
+
log('connected', { id: this._id, remote: this._remoteId });
|
|
95
|
+
this.options.stream
|
|
72
96
|
.pipe(this._outgoingDelay)
|
|
73
|
-
.pipe(this._remoteConnection.
|
|
97
|
+
.pipe(this._remoteConnection.options.stream)
|
|
74
98
|
.pipe(this._incomingDelay)
|
|
75
|
-
.pipe(this.
|
|
99
|
+
.pipe(this.options.stream);
|
|
76
100
|
|
|
77
101
|
this.connected.emit();
|
|
78
102
|
this._remoteConnection.connected.emit();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
98
|
-
log('closing', this.
|
|
114
|
+
async destroy(): Promise<void> {
|
|
115
|
+
log('closing', { id: this._id });
|
|
116
|
+
this._destroyed = true;
|
|
99
117
|
|
|
100
|
-
MemoryTransport._connections.delete(this.
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
92
|
+
await connection.destroy();
|
|
93
93
|
|
|
94
94
|
await sleep(1); // Process events.
|
|
95
95
|
|