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