@dxos/network-manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/src/connection-log.d.ts +23 -0
- package/dist/src/connection-log.d.ts.map +1 -0
- package/dist/src/connection-log.js +98 -0
- package/dist/src/connection-log.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +28 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/network-manager.d.ts +80 -0
- package/dist/src/network-manager.d.ts.map +1 -0
- package/dist/src/network-manager.js +123 -0
- package/dist/src/network-manager.js.map +1 -0
- package/dist/src/network-manager.test.d.ts +2 -0
- package/dist/src/network-manager.test.d.ts.map +1 -0
- package/dist/src/network-manager.test.js +621 -0
- package/dist/src/network-manager.test.js.map +1 -0
- package/dist/src/presence.test.d.ts +2 -0
- package/dist/src/presence.test.d.ts.map +1 -0
- package/dist/src/presence.test.js +60 -0
- package/dist/src/presence.test.js.map +1 -0
- package/dist/src/protocol-factory.d.ts +30 -0
- package/dist/src/protocol-factory.d.ts.map +1 -0
- package/dist/src/protocol-factory.js +64 -0
- package/dist/src/protocol-factory.js.map +1 -0
- package/dist/src/signal/index.d.ts +4 -0
- package/dist/src/signal/index.d.ts.map +1 -0
- package/dist/src/signal/index.js +23 -0
- package/dist/src/signal/index.js.map +1 -0
- package/dist/src/signal/integration.test.d.ts +2 -0
- package/dist/src/signal/integration.test.d.ts.map +1 -0
- package/dist/src/signal/integration.test.js +105 -0
- package/dist/src/signal/integration.test.js.map +1 -0
- package/dist/src/signal/message-router.d.ts +39 -0
- package/dist/src/signal/message-router.d.ts.map +1 -0
- package/dist/src/signal/message-router.js +135 -0
- package/dist/src/signal/message-router.js.map +1 -0
- package/dist/src/signal/message-router.test.d.ts +2 -0
- package/dist/src/signal/message-router.test.d.ts.map +1 -0
- package/dist/src/signal/message-router.test.js +211 -0
- package/dist/src/signal/message-router.test.js.map +1 -0
- package/dist/src/signal/signal-connection.d.ts +21 -0
- package/dist/src/signal/signal-connection.d.ts.map +1 -0
- package/dist/src/signal/signal-connection.js +6 -0
- package/dist/src/signal/signal-connection.js.map +1 -0
- package/dist/src/signal/signal-messaging.d.ts +34 -0
- package/dist/src/signal/signal-messaging.d.ts.map +1 -0
- package/dist/src/signal/signal-messaging.js +6 -0
- package/dist/src/signal/signal-messaging.js.map +1 -0
- package/dist/src/swarm/connection.d.ts +69 -0
- package/dist/src/swarm/connection.d.ts.map +1 -0
- package/dist/src/swarm/connection.js +187 -0
- package/dist/src/swarm/connection.js.map +1 -0
- package/dist/src/swarm/index.d.ts +4 -0
- package/dist/src/swarm/index.d.ts.map +1 -0
- package/dist/src/swarm/index.js +23 -0
- package/dist/src/swarm/index.js.map +1 -0
- package/dist/src/swarm/swarm-mapper.d.ts +30 -0
- package/dist/src/swarm/swarm-mapper.d.ts.map +1 -0
- package/dist/src/swarm/swarm-mapper.js +90 -0
- package/dist/src/swarm/swarm-mapper.js.map +1 -0
- package/dist/src/swarm/swarm.d.ts +64 -0
- package/dist/src/swarm/swarm.d.ts.map +1 -0
- package/dist/src/swarm/swarm.js +276 -0
- package/dist/src/swarm/swarm.js.map +1 -0
- package/dist/src/swarm/swarm.test.d.ts +2 -0
- package/dist/src/swarm/swarm.test.d.ts.map +1 -0
- package/dist/src/swarm/swarm.test.js +145 -0
- package/dist/src/swarm/swarm.test.js.map +1 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +21 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-protocol.d.ts +59 -0
- package/dist/src/testing/test-protocol.d.ts.map +1 -0
- package/dist/src/testing/test-protocol.js +140 -0
- package/dist/src/testing/test-protocol.js.map +1 -0
- package/dist/src/topology/fully-connected-topology.d.ts +11 -0
- package/dist/src/topology/fully-connected-topology.d.ts.map +1 -0
- package/dist/src/topology/fully-connected-topology.js +34 -0
- package/dist/src/topology/fully-connected-topology.js.map +1 -0
- package/dist/src/topology/index.d.ts +5 -0
- package/dist/src/topology/index.d.ts.map +1 -0
- package/dist/src/topology/index.js +24 -0
- package/dist/src/topology/index.js.map +1 -0
- package/dist/src/topology/mmst-topology.d.ts +31 -0
- package/dist/src/topology/mmst-topology.d.ts.map +1 -0
- package/dist/src/topology/mmst-topology.js +73 -0
- package/dist/src/topology/mmst-topology.js.map +1 -0
- package/dist/src/topology/star-topology.d.ts +13 -0
- package/dist/src/topology/star-topology.d.ts.map +1 -0
- package/dist/src/topology/star-topology.js +60 -0
- package/dist/src/topology/star-topology.js.map +1 -0
- package/dist/src/topology/topology.d.ts +56 -0
- package/dist/src/topology/topology.d.ts.map +1 -0
- package/dist/src/topology/topology.js +6 -0
- package/dist/src/topology/topology.js.map +1 -0
- package/dist/src/transport/index.d.ts +6 -0
- package/dist/src/transport/index.d.ts.map +1 -0
- package/dist/src/transport/index.js +25 -0
- package/dist/src/transport/index.js.map +1 -0
- package/dist/src/transport/memory-transport.d.ts +32 -0
- package/dist/src/transport/memory-transport.d.ts.map +1 -0
- package/dist/src/transport/memory-transport.js +98 -0
- package/dist/src/transport/memory-transport.js.map +1 -0
- package/dist/src/transport/memory-transport.test.d.ts +2 -0
- package/dist/src/transport/memory-transport.test.d.ts.map +1 -0
- package/dist/src/transport/memory-transport.test.js +84 -0
- package/dist/src/transport/memory-transport.test.js.map +1 -0
- package/dist/src/transport/transport.d.ts +38 -0
- package/dist/src/transport/transport.d.ts.map +1 -0
- package/dist/src/transport/transport.js +6 -0
- package/dist/src/transport/transport.js.map +1 -0
- package/dist/src/transport/webrtc-transport-proxy.d.ts +48 -0
- package/dist/src/transport/webrtc-transport-proxy.d.ts.map +1 -0
- package/dist/src/transport/webrtc-transport-proxy.js +139 -0
- package/dist/src/transport/webrtc-transport-proxy.js.map +1 -0
- package/dist/src/transport/webrtc-transport-proxy.test.d.ts +2 -0
- package/dist/src/transport/webrtc-transport-proxy.test.d.ts.map +1 -0
- package/dist/src/transport/webrtc-transport-proxy.test.js +245 -0
- package/dist/src/transport/webrtc-transport-proxy.test.js.map +1 -0
- package/dist/src/transport/webrtc-transport-service.d.ts +15 -0
- package/dist/src/transport/webrtc-transport-service.d.ts.map +1 -0
- package/dist/src/transport/webrtc-transport-service.js +97 -0
- package/dist/src/transport/webrtc-transport-service.js.map +1 -0
- package/dist/src/transport/webrtc-transport.d.ts +34 -0
- package/dist/src/transport/webrtc-transport.d.ts.map +1 -0
- package/dist/src/transport/webrtc-transport.js +100 -0
- package/dist/src/transport/webrtc-transport.js.map +1 -0
- package/dist/src/transport/webrtc-transport.test.d.ts +2 -0
- package/dist/src/transport/webrtc-transport.test.d.ts.map +1 -0
- package/dist/src/transport/webrtc-transport.test.js +80 -0
- package/dist/src/transport/webrtc-transport.test.js.map +1 -0
- package/dist/src/transport/webrtc.d.ts +6 -0
- package/dist/src/transport/webrtc.d.ts.map +1 -0
- package/dist/src/transport/webrtc.js +17 -0
- package/dist/src/transport/webrtc.js.map +1 -0
- package/dist/src/transport/wrtc-simple-peer.test.d.ts +2 -0
- package/dist/src/transport/wrtc-simple-peer.test.d.ts.map +1 -0
- package/dist/src/transport/wrtc-simple-peer.test.js +24 -0
- package/dist/src/transport/wrtc-simple-peer.test.js.map +1 -0
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +58 -0
- package/src/connection-log.ts +102 -0
- package/src/globals.d.ts +7 -0
- package/src/index.ts +12 -0
- package/src/network-manager.test.ts +764 -0
- package/src/network-manager.ts +204 -0
- package/src/presence.test.ts +78 -0
- package/src/protocol-factory.ts +83 -0
- package/src/signal/index.ts +7 -0
- package/src/signal/integration.test.ts +129 -0
- package/src/signal/message-router.test.ts +274 -0
- package/src/signal/message-router.ts +200 -0
- package/src/signal/signal-connection.ts +20 -0
- package/src/signal/signal-messaging.ts +38 -0
- package/src/swarm/connection.ts +206 -0
- package/src/swarm/index.ts +7 -0
- package/src/swarm/swarm-mapper.ts +120 -0
- package/src/swarm/swarm.test.ts +192 -0
- package/src/swarm/swarm.ts +328 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/test-protocol.ts +160 -0
- package/src/topology/fully-connected-topology.ts +38 -0
- package/src/topology/index.ts +8 -0
- package/src/topology/mmst-topology.ts +105 -0
- package/src/topology/star-topology.ts +67 -0
- package/src/topology/topology.ts +69 -0
- package/src/transport/index.ts +9 -0
- package/src/transport/memory-transport.test.ts +110 -0
- package/src/transport/memory-transport.ts +128 -0
- package/src/transport/transport.ts +47 -0
- package/src/transport/webrtc-transport-proxy.test.ts +290 -0
- package/src/transport/webrtc-transport-proxy.ts +167 -0
- package/src/transport/webrtc-transport-service.ts +115 -0
- package/src/transport/webrtc-transport.test.ts +112 -0
- package/src/transport/webrtc-transport.ts +122 -0
- package/src/transport/webrtc.ts +15 -0
- package/src/transport/wrtc-simple-peer.test.ts +24 -0
- package/src/types.ts +10 -0
- package/src/typings.d.ts +7 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Event, sleep } from '@dxos/async';
|
|
8
|
+
import { discoveryKey } from '@dxos/crypto';
|
|
9
|
+
import { ErrorStream } from '@dxos/debug';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { Messenger } from '@dxos/messaging';
|
|
13
|
+
import { SwarmEvent } from '@dxos/protocols/proto/dxos/mesh/signal';
|
|
14
|
+
import { Answer } from '@dxos/protocols/proto/dxos/mesh/swarm';
|
|
15
|
+
import { ComplexMap, ComplexSet } from '@dxos/util';
|
|
16
|
+
|
|
17
|
+
import { ProtocolProvider } from '../network-manager';
|
|
18
|
+
import { MessageRouter } from '../signal';
|
|
19
|
+
import { OfferMessage, SignalMessage } from '../signal/signal-messaging';
|
|
20
|
+
import { SwarmController, Topology } from '../topology';
|
|
21
|
+
import { TransportFactory } from '../transport';
|
|
22
|
+
import { Topic } from '../types';
|
|
23
|
+
import { Connection, ConnectionState } from './connection';
|
|
24
|
+
|
|
25
|
+
const INITIATION_DELAY = 100;
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Factor out.
|
|
28
|
+
const getClassName = (obj: any) => Object.getPrototypeOf(obj).constructor.name;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A single peer's view of the swarm.
|
|
32
|
+
* Manages a set of connections implemented by simple-peer instances.
|
|
33
|
+
* Routes signal events and maintains swarm topology.
|
|
34
|
+
*/
|
|
35
|
+
export class Swarm {
|
|
36
|
+
private readonly _connections = new ComplexMap<PublicKey, Connection>(PublicKey.hash);
|
|
37
|
+
private readonly _discoveredPeers = new ComplexSet<PublicKey>(PublicKey.hash);
|
|
38
|
+
private readonly _swarmMessenger: MessageRouter;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unique id of the swarm, local to the current peer, generated when swarm is joined.
|
|
42
|
+
*/
|
|
43
|
+
readonly id = PublicKey.random();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* New connection to a peer is started.
|
|
47
|
+
*/
|
|
48
|
+
readonly connectionAdded = new Event<Connection>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Connection to a peer is dropped.
|
|
52
|
+
*/
|
|
53
|
+
readonly connectionRemoved = new Event<Connection>();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Connection is established to a new peer.
|
|
57
|
+
*/
|
|
58
|
+
readonly connected = new Event<PublicKey>();
|
|
59
|
+
|
|
60
|
+
readonly errors = new ErrorStream();
|
|
61
|
+
|
|
62
|
+
// TODO(burdon): Split up properties.
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly _topic: PublicKey,
|
|
65
|
+
private readonly _ownPeerId: PublicKey,
|
|
66
|
+
private _topology: Topology,
|
|
67
|
+
private readonly _protocolProvider: ProtocolProvider,
|
|
68
|
+
private readonly _messenger: Messenger,
|
|
69
|
+
private readonly _transportFactory: TransportFactory,
|
|
70
|
+
private readonly _label: string | undefined
|
|
71
|
+
) {
|
|
72
|
+
log('creating swarm', { topic: this._topic, peerId: _ownPeerId });
|
|
73
|
+
_topology.init(this._getSwarmController());
|
|
74
|
+
|
|
75
|
+
this._swarmMessenger = new MessageRouter({
|
|
76
|
+
sendMessage: async (msg) => await this._messenger.sendMessage(msg),
|
|
77
|
+
onSignal: async (msg) => await this.onSignal(msg),
|
|
78
|
+
onOffer: async (msg) => await this.onOffer(msg),
|
|
79
|
+
topic: this._topic
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this._messenger
|
|
83
|
+
.listen({
|
|
84
|
+
peerId: this._ownPeerId,
|
|
85
|
+
payloadType: 'dxos.mesh.swarm.SwarmMessage',
|
|
86
|
+
onMessage: async (message) => await this._swarmMessenger.receiveMessage(message)
|
|
87
|
+
})
|
|
88
|
+
.catch((error) => log.catch(error));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get connections() {
|
|
92
|
+
return Array.from(this._connections.values());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get ownPeerId() {
|
|
96
|
+
return this._ownPeerId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Custom label assigned to this swarm. Used in devtools to display human-readable names for swarms.
|
|
101
|
+
*/
|
|
102
|
+
get label(): string | undefined {
|
|
103
|
+
return this._label;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get topic(): Topic {
|
|
107
|
+
return this._topic;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onSwarmEvent(swarmEvent: SwarmEvent) {
|
|
111
|
+
log('swarm event', { topic: this._topic, swarmEvent });
|
|
112
|
+
if (swarmEvent.peerAvailable) {
|
|
113
|
+
const peerId = PublicKey.from(swarmEvent.peerAvailable.peer);
|
|
114
|
+
log('new peer', { topic: this._topic, peerId });
|
|
115
|
+
if (!peerId.equals(this._ownPeerId)) {
|
|
116
|
+
this._discoveredPeers.add(peerId);
|
|
117
|
+
}
|
|
118
|
+
} else if (swarmEvent.peerLeft) {
|
|
119
|
+
this._discoveredPeers.delete(PublicKey.from(swarmEvent.peerLeft.peer));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this._topology.update();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async onOffer(message: OfferMessage): Promise<Answer> {
|
|
126
|
+
log('offer', { topic: this._topic, message });
|
|
127
|
+
// Id of the peer offering us the connection.
|
|
128
|
+
assert(message.author);
|
|
129
|
+
const remoteId = message.author;
|
|
130
|
+
if (!message.recipient?.equals(this._ownPeerId)) {
|
|
131
|
+
log('rejecting offer with incorrect peerId', { topic: this._topic, message });
|
|
132
|
+
return { accept: false };
|
|
133
|
+
}
|
|
134
|
+
if (!message.topic?.equals(this._topic)) {
|
|
135
|
+
log('rejecting offer with incorrect topic', { topic: this._topic, message });
|
|
136
|
+
return { accept: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 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.
|
|
142
|
+
if (remoteId.toHex() < this._ownPeerId.toHex()) {
|
|
143
|
+
log("closing local connection and accepting remote peer's offer", {
|
|
144
|
+
topic: this._topic,
|
|
145
|
+
peerId: this._ownPeerId
|
|
146
|
+
});
|
|
147
|
+
// Close our connection and accept remote peer's connection.
|
|
148
|
+
await this._closeConnection(remoteId).catch((err) => {
|
|
149
|
+
this.errors.raise(err);
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
// Continue with our origination attempt, the remote peer will close it's connection and accept ours.
|
|
153
|
+
return { accept: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let accept = false;
|
|
158
|
+
if (await this._topology.onOffer(remoteId)) {
|
|
159
|
+
if (!this._connections.has(remoteId)) {
|
|
160
|
+
// Connection might have been already established.
|
|
161
|
+
assert(message.sessionId);
|
|
162
|
+
const connection = this._createConnection(false, message.author, message.sessionId);
|
|
163
|
+
try {
|
|
164
|
+
connection.open();
|
|
165
|
+
} catch (err: any) {
|
|
166
|
+
this.errors.raise(err);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
accept = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this._topology.update();
|
|
174
|
+
return { accept };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async onSignal(message: SignalMessage): Promise<void> {
|
|
178
|
+
log('signal', { topic: this._topic, message });
|
|
179
|
+
assert(
|
|
180
|
+
message.recipient?.equals(this._ownPeerId),
|
|
181
|
+
`Invalid signal peer id expected=${this.ownPeerId}, actual=${message.recipient}`
|
|
182
|
+
);
|
|
183
|
+
assert(message.topic?.equals(this._topic));
|
|
184
|
+
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
|
+
|
|
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)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private _getSwarmController(): SwarmController {
|
|
216
|
+
return {
|
|
217
|
+
getState: () => ({
|
|
218
|
+
ownPeerId: this._ownPeerId,
|
|
219
|
+
connected: Array.from(this._connections.keys()),
|
|
220
|
+
candidates: Array.from(this._discoveredPeers.keys()).filter((key) => !this._connections.has(key))
|
|
221
|
+
}),
|
|
222
|
+
connect: (peer) => this._initiateConnection(peer),
|
|
223
|
+
disconnect: async (peer) => {
|
|
224
|
+
try {
|
|
225
|
+
await this._closeConnection(peer);
|
|
226
|
+
} catch (err: any) {
|
|
227
|
+
this.errors.raise(err);
|
|
228
|
+
}
|
|
229
|
+
this._topology.update();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// TODO(burdon): Make async.
|
|
235
|
+
private async _initiateConnection(remoteId: PublicKey) {
|
|
236
|
+
// It is likely that the other peer will also try to connect to us at the same time.
|
|
237
|
+
// If our peerId is higher, we will wait for a bit so that other peer has a chance to connect first.
|
|
238
|
+
if (remoteId.toHex() < this._ownPeerId.toHex()) {
|
|
239
|
+
await sleep(INITIATION_DELAY);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (this._connections.has(remoteId)) {
|
|
243
|
+
// Do nothing if peer is already connected.
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const sessionId = PublicKey.random();
|
|
248
|
+
|
|
249
|
+
log('initiating...', { topic: this._topic, peerId: remoteId, sessionId });
|
|
250
|
+
const connection = this._createConnection(true, remoteId, sessionId);
|
|
251
|
+
connection.initiate();
|
|
252
|
+
this._topology.update();
|
|
253
|
+
log('initiated', { topic: this._topic });
|
|
254
|
+
}
|
|
255
|
+
|
|
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.');
|
|
260
|
+
|
|
261
|
+
const connection = new Connection(
|
|
262
|
+
this._topic,
|
|
263
|
+
this._ownPeerId,
|
|
264
|
+
remoteId,
|
|
265
|
+
sessionId,
|
|
266
|
+
initiator,
|
|
267
|
+
this._swarmMessenger,
|
|
268
|
+
this._protocolProvider({ channel: discoveryKey(this._topic), initiator }),
|
|
269
|
+
this._transportFactory
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
this._connections.set(remoteId, connection);
|
|
273
|
+
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
|
+
return connection;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// TODO(burdon): Make async.
|
|
317
|
+
private async _closeConnection(peerId: PublicKey) {
|
|
318
|
+
log('closing...', { topic: this._topic, peerId });
|
|
319
|
+
const connection = this._connections.get(peerId);
|
|
320
|
+
if (!connection) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this._connections.delete(peerId);
|
|
325
|
+
await connection.close();
|
|
326
|
+
log('closed', { topic: this._topic });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Test/mock Protocol implementation used in network-manager tests.
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { Extension, Protocol } from '@dxos/mesh-protocol';
|
|
13
|
+
|
|
14
|
+
import { protocolFactory } from '../protocol-factory';
|
|
15
|
+
|
|
16
|
+
const EXTENSION_NAME = 'test';
|
|
17
|
+
|
|
18
|
+
// TODO(dboreham): This method should be added to Protocol (and one for "my ID"?).
|
|
19
|
+
export const getPeerId = (protocol: Protocol) => {
|
|
20
|
+
const { peerId } = protocol.getSession() ?? {};
|
|
21
|
+
if (!peerId) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return PublicKey.bufferize(peerId);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Object Implementing a p2p protocol (interaction across a set of peers using a common protocol)
|
|
29
|
+
* and an associated transport connection protocol (interaction with each individual peer over a single network
|
|
30
|
+
* transport connection).
|
|
31
|
+
*/
|
|
32
|
+
export class TestProtocolPlugin extends EventEmitter {
|
|
33
|
+
/** @type {Map<string, {Protocol}>} */
|
|
34
|
+
_peers;
|
|
35
|
+
|
|
36
|
+
/** @type {Buffer} */
|
|
37
|
+
_peerId;
|
|
38
|
+
|
|
39
|
+
/** @type {Boolean} */
|
|
40
|
+
_uppercase;
|
|
41
|
+
|
|
42
|
+
initCalled = false;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Test Protocol. Passes through messages unchanged, or folds to upper case.
|
|
46
|
+
* Provides a way to test that two protocols with different behavior are
|
|
47
|
+
* indeed being used with two different swarm keys.
|
|
48
|
+
* @param {Buffer} peerId
|
|
49
|
+
* @param {Boolean} uppercase Fold payload case to upper if set.
|
|
50
|
+
*/
|
|
51
|
+
constructor(peerId: Buffer, uppercase = false) {
|
|
52
|
+
super();
|
|
53
|
+
assert(Buffer.isBuffer(peerId));
|
|
54
|
+
|
|
55
|
+
// TODO(dboreham): Mis-named because this is OUR node ID, not the id of any peer.
|
|
56
|
+
this._peerId = peerId;
|
|
57
|
+
this._uppercase = uppercase;
|
|
58
|
+
this._peers = new Map();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This node's id.
|
|
63
|
+
* @return {Buffer}
|
|
64
|
+
*/
|
|
65
|
+
get peerId() {
|
|
66
|
+
return this._peerId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Array of the currently connected peers' node ids (not including our id).
|
|
71
|
+
* @return {{Protocol}[]}
|
|
72
|
+
*/
|
|
73
|
+
get peers() {
|
|
74
|
+
return Array.from(this._peers.values());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Factory function for Extensions (per-connection object).
|
|
79
|
+
* Called when a new peer transport connection is established.
|
|
80
|
+
* @return {Extension}
|
|
81
|
+
*/
|
|
82
|
+
createExtension() {
|
|
83
|
+
return new Extension(EXTENSION_NAME, { binary: true })
|
|
84
|
+
.setInitHandler(this._init.bind(this))
|
|
85
|
+
.setMessageHandler(this._receive.bind(this))
|
|
86
|
+
.setHandshakeHandler(this._onPeerConnect.bind(this))
|
|
87
|
+
.setCloseHandler(this._onPeerDisconnect.bind(this));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Methods below are per-peer-connection.
|
|
91
|
+
// TODO(dboreham): Why are these here and not on Protocol?
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send/Receive messages with peer when initiating a request/response interaction.
|
|
95
|
+
* @param peerId Must be the value passed to the constructor on the responding node.
|
|
96
|
+
* @param payload Message to send
|
|
97
|
+
* @return Message received from peer in response to our request.
|
|
98
|
+
*/
|
|
99
|
+
async send(peerId: Buffer, payload: string): Promise<string> {
|
|
100
|
+
assert(Buffer.isBuffer(peerId));
|
|
101
|
+
const peerIdStr = PublicKey.stringify(peerId);
|
|
102
|
+
const peer = this._peers.get(peerIdStr);
|
|
103
|
+
// TODO(dboreham): Throw fatal error if peer not found.
|
|
104
|
+
const extension = peer.getExtension(EXTENSION_NAME);
|
|
105
|
+
const encoded = Buffer.from(payload);
|
|
106
|
+
log('Sent', { peerIdStr, payload });
|
|
107
|
+
return await extension.send(encoded, { oneway: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async _init(protocol: Protocol) {
|
|
111
|
+
this.initCalled = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async _receive(protocol: Protocol, data: any) {
|
|
115
|
+
const peerId = getPeerId(protocol);
|
|
116
|
+
assert(peerId !== undefined);
|
|
117
|
+
const peerIdStr = PublicKey.stringify(peerId);
|
|
118
|
+
let payload = data.data.toString();
|
|
119
|
+
if (this._uppercase) {
|
|
120
|
+
payload = payload.toUpperCase();
|
|
121
|
+
}
|
|
122
|
+
log('Received', { peerIdStr, payload });
|
|
123
|
+
this.emit('receive', protocol, payload);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async _onPeerConnect(protocol: Protocol) {
|
|
127
|
+
const peerId = getPeerId(protocol);
|
|
128
|
+
if (peerId === undefined) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const peerIdStr = PublicKey.stringify(peerId);
|
|
132
|
+
if (this._peers.has(peerIdStr)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._peers.set(peerIdStr, protocol);
|
|
137
|
+
this.emit('connect', protocol);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async _onPeerDisconnect(protocol: Protocol) {
|
|
141
|
+
const peerId = getPeerId(protocol);
|
|
142
|
+
if (peerId === undefined) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this._peers.delete(PublicKey.stringify(peerId));
|
|
147
|
+
this.emit('disconnect', peerId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @return {ProtocolProvider}
|
|
153
|
+
*/
|
|
154
|
+
// TODO(dboreham): Try to encapsulate swarm_key, nodeId.
|
|
155
|
+
export const testProtocolProvider = (swarmKey: Buffer, nodeId: Buffer, protocolPlugin: any) =>
|
|
156
|
+
protocolFactory({
|
|
157
|
+
getTopics: () => [swarmKey],
|
|
158
|
+
session: { peerId: PublicKey.stringify(nodeId) },
|
|
159
|
+
plugins: [protocolPlugin]
|
|
160
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { PublicKey } from '@dxos/keys';
|
|
8
|
+
|
|
9
|
+
import { SwarmController, Topology } from './topology';
|
|
10
|
+
|
|
11
|
+
export class FullyConnectedTopology implements Topology {
|
|
12
|
+
private _controller?: SwarmController;
|
|
13
|
+
|
|
14
|
+
init(controller: SwarmController): void {
|
|
15
|
+
assert(!this._controller, 'Already initialized');
|
|
16
|
+
this._controller = controller;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
update(): void {
|
|
20
|
+
assert(this._controller, 'Not initialized');
|
|
21
|
+
const { candidates: discovered } = this._controller.getState();
|
|
22
|
+
for (const peer of discovered) {
|
|
23
|
+
this._controller.connect(peer);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async onOffer(peer: PublicKey): Promise<boolean> {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async destroy(): Promise<void> {
|
|
32
|
+
// Nothing to do.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toString() {
|
|
36
|
+
return 'FullyConnectedTopology';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import distance from 'xor-distance';
|
|
7
|
+
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
|
|
11
|
+
import { SwarmController, Topology } from './topology';
|
|
12
|
+
|
|
13
|
+
export interface MMSTTopologyOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Number of connections the peer will originate by itself.
|
|
16
|
+
*/
|
|
17
|
+
originateConnections?: number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of connections allowed, all other connections will be dropped.
|
|
21
|
+
*/
|
|
22
|
+
maxPeers?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Size of random sample from which peer candidates are selected.
|
|
26
|
+
*/
|
|
27
|
+
sampleSize?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class MMSTTopology implements Topology {
|
|
31
|
+
private readonly _originateConnections: number;
|
|
32
|
+
private readonly _maxPeers: number;
|
|
33
|
+
private readonly _sampleSize: number;
|
|
34
|
+
|
|
35
|
+
private _controller?: SwarmController;
|
|
36
|
+
|
|
37
|
+
private _sampleCollected = false;
|
|
38
|
+
|
|
39
|
+
constructor({ originateConnections = 2, maxPeers = 4, sampleSize = 10 }: MMSTTopologyOptions = {}) {
|
|
40
|
+
this._originateConnections = originateConnections;
|
|
41
|
+
this._maxPeers = maxPeers;
|
|
42
|
+
this._sampleSize = sampleSize;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
init(controller: SwarmController): void {
|
|
46
|
+
assert(!this._controller, 'Already initialized');
|
|
47
|
+
this._controller = controller;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
update(): void {
|
|
51
|
+
assert(this._controller, 'Not initialized');
|
|
52
|
+
const { connected, candidates } = this._controller.getState();
|
|
53
|
+
// Run the algorithms if we have first candidates, ran it before, or have more connections than needed.
|
|
54
|
+
if (this._sampleCollected || connected.length > this._maxPeers || candidates.length > 0) {
|
|
55
|
+
log('Running the algorithm.');
|
|
56
|
+
this._sampleCollected = true;
|
|
57
|
+
this._runAlgorithm();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async onOffer(peer: PublicKey): Promise<boolean> {
|
|
62
|
+
assert(this._controller, 'Not initialized');
|
|
63
|
+
const { connected } = this._controller.getState();
|
|
64
|
+
const accept = connected.length < this._maxPeers;
|
|
65
|
+
log(`Offer ${peer} accept=${accept}`);
|
|
66
|
+
return accept;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async destroy(): Promise<void> {
|
|
70
|
+
// Nothing to do.
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _runAlgorithm() {
|
|
74
|
+
assert(this._controller, 'Not initialized');
|
|
75
|
+
const { connected, candidates, ownPeerId } = this._controller.getState();
|
|
76
|
+
|
|
77
|
+
if (connected.length > this._maxPeers) {
|
|
78
|
+
// Disconnect extra peers.
|
|
79
|
+
const sorted = sortByXorDistance(connected, ownPeerId)
|
|
80
|
+
.reverse()
|
|
81
|
+
.slice(0, this._maxPeers - connected.length);
|
|
82
|
+
for (const peer of sorted) {
|
|
83
|
+
log(`Disconnect ${peer}.`);
|
|
84
|
+
this._controller.disconnect(peer);
|
|
85
|
+
}
|
|
86
|
+
} else if (connected.length < this._originateConnections) {
|
|
87
|
+
// Connect new peers to reach desired quota.
|
|
88
|
+
const sample = candidates.sort(() => Math.random() - 0.5).slice(0, this._sampleSize);
|
|
89
|
+
const sorted = sortByXorDistance(sample, ownPeerId).slice(0, this._originateConnections - connected.length);
|
|
90
|
+
for (const peer of sorted) {
|
|
91
|
+
log(`Connect ${peer}.`);
|
|
92
|
+
this._controller.connect(peer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toString() {
|
|
98
|
+
return 'MMSTTopology';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sortByXorDistance = (keys: PublicKey[], reference: PublicKey): PublicKey[] =>
|
|
103
|
+
keys.sort((a, b) =>
|
|
104
|
+
distance.gt(distance(a.asBuffer(), reference.asBuffer()), distance(b.asBuffer(), reference.asBuffer()))
|
|
105
|
+
);
|