@dxos/network-manager 2.33.9-dev.9246a07b → 2.33.9-dev.9bbef4e2
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/network-manager.blueprint-test.d.ts +3 -1
- package/dist/src/network-manager.blueprint-test.d.ts.map +1 -1
- package/dist/src/network-manager.blueprint-test.js +46 -17
- package/dist/src/network-manager.blueprint-test.js.map +1 -1
- package/dist/src/network-manager.browser-test.js +1 -1
- package/dist/src/network-manager.browser-test.js.map +1 -1
- package/dist/src/network-manager.d.ts.map +1 -1
- package/dist/src/network-manager.js +6 -6
- package/dist/src/network-manager.js.map +1 -1
- package/dist/src/network-manager.test.js +5 -4
- package/dist/src/network-manager.test.js.map +1 -1
- package/dist/src/proto/gen/dxos/credentials.d.ts.map +1 -1
- package/dist/src/proto/gen/dxos/halo/keys.d.ts.map +1 -1
- package/dist/src/proto/gen/dxos/halo/keys.js.map +1 -1
- package/dist/src/proto/gen/dxos/mesh/signal.d.ts +52 -45
- package/dist/src/proto/gen/dxos/mesh/signal.d.ts.map +1 -1
- package/dist/src/proto/gen/dxos/mesh/signalMessage.d.ts +79 -0
- package/dist/src/proto/gen/dxos/mesh/signalMessage.d.ts.map +1 -0
- package/dist/src/proto/gen/dxos/mesh/signalMessage.js +3 -0
- package/dist/src/proto/gen/dxos/mesh/signalMessage.js.map +1 -0
- package/dist/src/proto/gen/google/protobuf.d.ts +6 -0
- package/dist/src/proto/gen/google/protobuf.d.ts.map +1 -1
- package/dist/src/proto/gen/index.d.ts +17 -5
- package/dist/src/proto/gen/index.d.ts.map +1 -1
- package/dist/src/proto/gen/index.js +1 -1
- package/dist/src/proto/gen/index.js.map +1 -1
- package/dist/src/proto/substitutions.d.ts +4 -0
- package/dist/src/proto/substitutions.d.ts.map +1 -1
- package/dist/src/proto/substitutions.js +3 -1
- package/dist/src/proto/substitutions.js.map +1 -1
- package/dist/src/signal/in-memory-signal-manager.d.ts +7 -7
- package/dist/src/signal/in-memory-signal-manager.d.ts.map +1 -1
- package/dist/src/signal/in-memory-signal-manager.js +29 -8
- package/dist/src/signal/in-memory-signal-manager.js.map +1 -1
- package/dist/src/signal/index.d.ts +1 -2
- package/dist/src/signal/index.d.ts.map +1 -1
- package/dist/src/signal/index.js +1 -2
- package/dist/src/signal/index.js.map +1 -1
- 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 +102 -0
- package/dist/src/signal/integration.test.js.map +1 -0
- package/dist/src/signal/message-router.d.ts +7 -7
- package/dist/src/signal/message-router.d.ts.map +1 -1
- package/dist/src/signal/message-router.js +6 -1
- package/dist/src/signal/message-router.js.map +1 -1
- package/dist/src/signal/message-router.test.js +15 -19
- package/dist/src/signal/message-router.test.js.map +1 -1
- package/dist/src/signal/signal-client.d.ts +33 -18
- package/dist/src/signal/signal-client.d.ts.map +1 -1
- package/dist/src/signal/signal-client.js +102 -92
- package/dist/src/signal/signal-client.js.map +1 -1
- package/dist/src/signal/signal-client.test.js +60 -77
- package/dist/src/signal/signal-client.test.js.map +1 -1
- package/dist/src/signal/{websocket-signal-manager.d.ts → signal-manager-impl.d.ts} +13 -11
- package/dist/src/signal/signal-manager-impl.d.ts.map +1 -0
- package/dist/src/signal/signal-manager-impl.js +151 -0
- package/dist/src/signal/signal-manager-impl.js.map +1 -0
- package/dist/src/signal/signal-manager.d.ts +12 -11
- package/dist/src/signal/signal-manager.d.ts.map +1 -1
- package/dist/src/signal/signal-rpc-client.d.ts +19 -0
- package/dist/src/signal/signal-rpc-client.d.ts.map +1 -0
- package/dist/src/signal/signal-rpc-client.js +108 -0
- package/dist/src/signal/signal-rpc-client.js.map +1 -0
- package/dist/src/signal/signal-rpc-client.test.d.ts +2 -0
- package/dist/src/signal/signal-rpc-client.test.d.ts.map +1 -0
- package/dist/src/signal/signal-rpc-client.test.js +74 -0
- package/dist/src/signal/signal-rpc-client.test.js.map +1 -0
- package/dist/src/swarm/connection.d.ts +3 -3
- package/dist/src/swarm/connection.d.ts.map +1 -1
- package/dist/src/swarm/connection.js +1 -4
- package/dist/src/swarm/connection.js.map +1 -1
- package/dist/src/swarm/swarm.d.ts +6 -7
- package/dist/src/swarm/swarm.d.ts.map +1 -1
- package/dist/src/swarm/swarm.js +21 -17
- package/dist/src/swarm/swarm.js.map +1 -1
- package/dist/src/swarm/swarm.test.js +156 -117
- package/dist/src/swarm/swarm.test.js.map +1 -1
- package/dist/src/topology/fully-connected-topology.d.ts +0 -1
- package/dist/src/topology/fully-connected-topology.d.ts.map +1 -1
- package/dist/src/topology/fully-connected-topology.js +1 -6
- package/dist/src/topology/fully-connected-topology.js.map +1 -1
- package/dist/src/topology/mmst-topology.d.ts +0 -1
- package/dist/src/topology/mmst-topology.d.ts.map +1 -1
- package/dist/src/topology/mmst-topology.js +1 -6
- package/dist/src/topology/mmst-topology.js.map +1 -1
- package/dist/src/topology/star-topology.d.ts +0 -1
- package/dist/src/topology/star-topology.d.ts.map +1 -1
- package/dist/src/topology/star-topology.js +1 -6
- package/dist/src/topology/star-topology.js.map +1 -1
- package/dist/src/topology/topology.d.ts +0 -6
- package/dist/src/topology/topology.d.ts.map +1 -1
- package/dist/src/transport/in-memory-transport.d.ts +2 -2
- package/dist/src/transport/in-memory-transport.d.ts.map +1 -1
- package/dist/src/transport/in-memory-transport.js.map +1 -1
- package/dist/src/transport/transport.d.ts +3 -3
- package/dist/src/transport/transport.d.ts.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.map +1 -1
- package/dist/tests-setup.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -12
- package/src/network-manager.blueprint-test.ts +57 -22
- package/src/network-manager.browser-test.ts +1 -1
- package/src/network-manager.test.ts +8 -7
- package/src/network-manager.ts +8 -9
- package/src/proto/defs/dxos/mesh/signal.proto +53 -35
- package/src/proto/defs/dxos/mesh/signalMessage.proto +51 -0
- package/src/proto/gen/dxos/credentials.ts +1 -0
- package/src/proto/gen/dxos/halo/keys.ts +1 -0
- package/src/proto/gen/dxos/mesh/signal.ts +51 -45
- package/src/proto/gen/dxos/mesh/signalMessage.ts +83 -0
- package/src/proto/gen/google/protobuf.ts +7 -0
- package/src/proto/gen/index.ts +18 -6
- package/src/proto/substitutions.ts +3 -1
- package/src/signal/in-memory-signal-manager.ts +37 -12
- package/src/signal/index.ts +1 -2
- package/src/signal/integration.test.ts +117 -0
- package/src/signal/message-router.test.ts +36 -41
- package/src/signal/message-router.ts +22 -18
- package/src/signal/signal-client.test.ts +70 -92
- package/src/signal/signal-client.ts +119 -113
- package/src/signal/signal-manager-impl.ts +166 -0
- package/src/signal/signal-manager.ts +12 -12
- package/src/signal/signal-rpc-client.test.ts +86 -0
- package/src/signal/signal-rpc-client.ts +121 -0
- package/src/swarm/connection.ts +5 -8
- package/src/swarm/swarm.test.ts +208 -169
- package/src/swarm/swarm.ts +24 -20
- package/src/topology/fully-connected-topology.ts +1 -9
- package/src/topology/mmst-topology.ts +1 -9
- package/src/topology/star-topology.ts +1 -7
- package/src/topology/topology.ts +0 -7
- package/src/transport/in-memory-transport.ts +2 -2
- package/src/transport/transport.ts +3 -3
- package/src/transport/webrtc-transport.ts +3 -3
- package/dist/browser-mocha/bundle.js +0 -119346
- package/dist/browser-mocha/main.js +0 -27
- package/dist/src/signal/websocket-rpc.d.ts +0 -30
- package/dist/src/signal/websocket-rpc.d.ts.map +0 -1
- package/dist/src/signal/websocket-rpc.js +0 -203
- package/dist/src/signal/websocket-rpc.js.map +0 -1
- package/dist/src/signal/websocket-signal-manager.d.ts.map +0 -1
- package/dist/src/signal/websocket-signal-manager.js +0 -134
- package/dist/src/signal/websocket-signal-manager.js.map +0 -1
- package/src/signal/websocket-rpc.ts +0 -208
- package/src/signal/websocket-signal-manager.ts +0 -158
|
@@ -2,26 +2,55 @@
|
|
|
2
2
|
// Copyright 2020 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import assert from 'assert';
|
|
5
6
|
import debug from 'debug';
|
|
6
7
|
|
|
7
|
-
import { Event } from '@dxos/async';
|
|
8
|
-
import {
|
|
8
|
+
import { Event, synchronized } from '@dxos/async';
|
|
9
|
+
import { Any, Stream } from '@dxos/codec-protobuf';
|
|
9
10
|
import { PublicKey } from '@dxos/protocols';
|
|
11
|
+
import { ComplexMap, SubscriptionGroup } from '@dxos/util';
|
|
10
12
|
|
|
11
13
|
import { schema } from '../proto/gen';
|
|
12
|
-
import {
|
|
14
|
+
import { Message, SwarmEvent } from '../proto/gen/dxos/mesh/signal';
|
|
15
|
+
import { SignalMessage } from '../proto/gen/dxos/mesh/signalMessage';
|
|
13
16
|
import { SignalApi } from './signal-api';
|
|
14
|
-
import {
|
|
17
|
+
import { SignalRPCClient } from './signal-rpc-client';
|
|
15
18
|
|
|
16
19
|
const log = debug('dxos:network-manager:signal-client');
|
|
17
20
|
|
|
18
21
|
const DEFAULT_RECONNECT_TIMEOUT = 1000;
|
|
19
22
|
|
|
23
|
+
enum State {
|
|
24
|
+
/** Connection is being established. */
|
|
25
|
+
CONNECTING = 'CONNECTING',
|
|
26
|
+
|
|
27
|
+
/** Connection is being re-established. */
|
|
28
|
+
RE_CONNECTING = 'RE_CONNECTING',
|
|
29
|
+
|
|
30
|
+
/** Connected. */
|
|
31
|
+
CONNECTED = 'CONNECTED',
|
|
32
|
+
|
|
33
|
+
/** Server terminated the connection. Socket will be reconnected. */
|
|
34
|
+
DISCONNECTED = 'DISCONNECTED',
|
|
35
|
+
|
|
36
|
+
/** Socket was closed. */
|
|
37
|
+
CLOSED = 'CLOSED'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Status = {
|
|
41
|
+
host: string
|
|
42
|
+
state: State
|
|
43
|
+
error?: string
|
|
44
|
+
reconnectIn: number
|
|
45
|
+
connectionStarted: number
|
|
46
|
+
lastStateChange: number
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Establishes a websocket connection to signal server and provides RPC methods.
|
|
22
51
|
*/
|
|
23
52
|
export class SignalClient {
|
|
24
|
-
private _state =
|
|
53
|
+
private _state = State.CONNECTING;
|
|
25
54
|
|
|
26
55
|
private _lastError?: Error;
|
|
27
56
|
|
|
@@ -42,28 +71,29 @@ export class SignalClient {
|
|
|
42
71
|
|
|
43
72
|
private _reconnectIntervalId?: NodeJS.Timeout;
|
|
44
73
|
|
|
45
|
-
private _client!:
|
|
74
|
+
private _client!: SignalRPCClient;
|
|
46
75
|
|
|
47
|
-
private
|
|
76
|
+
private _cleanupSubscriptions = new SubscriptionGroup();
|
|
77
|
+
readonly statusChanged = new Event<Status>();
|
|
48
78
|
|
|
49
|
-
readonly statusChanged = new Event<SignalApi.Status>();
|
|
50
79
|
readonly commandTrace = new Event<SignalApi.CommandTrace>();
|
|
80
|
+
readonly swarmEvent = new Event<[topic: PublicKey, swarmEvent: SwarmEvent]>();
|
|
51
81
|
|
|
82
|
+
private readonly _swarmStreams = new ComplexMap<PublicKey, Stream<SwarmEvent>>(key => key.toHex());
|
|
83
|
+
private readonly _messageStreams = new ComplexMap<PublicKey, Stream<Message>>(key => key.toHex());
|
|
52
84
|
/**
|
|
53
85
|
* @param _host Signal server websocket URL.
|
|
54
|
-
* @param _onOffer See `SignalApi.offer`.
|
|
55
86
|
* @param _onSignal See `SignalApi.signal`.
|
|
56
87
|
*/
|
|
57
88
|
constructor (
|
|
58
89
|
private readonly _host: string,
|
|
59
|
-
private readonly
|
|
60
|
-
private readonly _onSignal: (message: Message) => Promise<void>
|
|
90
|
+
private readonly _onSignal: (message: SignalMessage) => Promise<void>
|
|
61
91
|
) {
|
|
62
|
-
this._setState(
|
|
92
|
+
this._setState(State.CONNECTING);
|
|
63
93
|
this._createClient();
|
|
64
94
|
}
|
|
65
95
|
|
|
66
|
-
private _setState (newState:
|
|
96
|
+
private _setState (newState: State) {
|
|
67
97
|
this._state = newState;
|
|
68
98
|
this._lastStateChange = Date.now();
|
|
69
99
|
log(`Signal state changed ${JSON.stringify(this.getStatus())}`);
|
|
@@ -73,111 +103,88 @@ export class SignalClient {
|
|
|
73
103
|
private _createClient () {
|
|
74
104
|
this._connectionStarted = Date.now();
|
|
75
105
|
try {
|
|
76
|
-
this._client = new
|
|
106
|
+
this._client = new SignalRPCClient(this._host);
|
|
77
107
|
} catch (error: any) {
|
|
78
|
-
if (this._state ===
|
|
108
|
+
if (this._state === State.RE_CONNECTING) {
|
|
79
109
|
this._reconnectAfter *= 2;
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
this._lastError = error;
|
|
83
|
-
this._setState(
|
|
113
|
+
this._setState(State.DISCONNECTED);
|
|
84
114
|
this._reconnect();
|
|
85
115
|
}
|
|
86
116
|
|
|
87
|
-
this._client.
|
|
88
|
-
id: PublicKey.from(message.id),
|
|
89
|
-
remoteId: PublicKey.from(message.remoteId),
|
|
90
|
-
topic: PublicKey.from(message.topic),
|
|
91
|
-
sessionId: PublicKey.from(message.sessionId),
|
|
92
|
-
data: message.data
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
this._client.subscribe('signal', (msg: SignalMessage) => {
|
|
96
|
-
return this._onSignal({
|
|
97
|
-
id: PublicKey.from(msg.id!),
|
|
98
|
-
remoteId: PublicKey.from(msg.remoteId!),
|
|
99
|
-
topic: PublicKey.from(msg.topic!),
|
|
100
|
-
sessionId: PublicKey.from(msg.sessionId!),
|
|
101
|
-
data: schema.getCodecForType('dxos.mesh.signal.MessageData').decode(msg.data ?? failUndefined()),
|
|
102
|
-
// Field that MessageRouter adds, so on lower level it not always defined.
|
|
103
|
-
messageId: msg.messageId ? PublicKey.from(msg.messageId) : undefined
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this._clientCleanup.push(this._client.connected.on(() => {
|
|
108
|
-
log('Socket connected');
|
|
117
|
+
this._cleanupSubscriptions.push(this._client.connected.on(() => {
|
|
109
118
|
this._lastError = undefined;
|
|
110
119
|
this._reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
|
|
111
|
-
this._setState(
|
|
120
|
+
this._setState(State.CONNECTED);
|
|
112
121
|
}));
|
|
113
122
|
|
|
114
|
-
this.
|
|
123
|
+
this._cleanupSubscriptions.push(this._client.error.on(error => {
|
|
115
124
|
log(`Socket error: ${error.message}`);
|
|
116
|
-
if (this._state ===
|
|
125
|
+
if (this._state === State.CLOSED) {
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
if (this._state ===
|
|
129
|
+
if (this._state === State.RE_CONNECTING) {
|
|
121
130
|
this._reconnectAfter *= 2;
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
this._lastError = error;
|
|
125
|
-
this._setState(
|
|
134
|
+
this._setState(State.DISCONNECTED);
|
|
126
135
|
|
|
127
136
|
this._reconnect();
|
|
128
137
|
}));
|
|
129
138
|
|
|
130
|
-
this.
|
|
139
|
+
this._cleanupSubscriptions.push(this._client.disconnected.on(() => {
|
|
131
140
|
log('Socket disconnected');
|
|
132
141
|
// This is also called in case of error, but we already have disconnected the socket on error, so no need to do anything here.
|
|
133
|
-
if (this._state !==
|
|
142
|
+
if (this._state !== State.CONNECTING && this._state !== State.RE_CONNECTING) {
|
|
134
143
|
return;
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
if (this._state ===
|
|
146
|
+
if (this._state === State.RE_CONNECTING) {
|
|
138
147
|
this._reconnectAfter *= 2;
|
|
139
148
|
}
|
|
140
149
|
|
|
141
|
-
this._setState(
|
|
150
|
+
this._setState(State.DISCONNECTED);
|
|
142
151
|
this._reconnect();
|
|
143
152
|
}));
|
|
144
|
-
|
|
145
|
-
this._clientCleanup.push(this._client.commandTrace.on(trace => this.commandTrace.emit(trace)));
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
private _reconnect () {
|
|
156
|
+
log(`Reconnecting in ${this._reconnectAfter}ms`);
|
|
149
157
|
if (this._reconnectIntervalId !== undefined) {
|
|
150
158
|
console.error('Signal api already reconnecting.');
|
|
151
159
|
return;
|
|
152
160
|
}
|
|
153
|
-
if (this._state ===
|
|
161
|
+
if (this._state === State.CLOSED) {
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
this._reconnectIntervalId = setTimeout(() => {
|
|
158
166
|
this._reconnectIntervalId = undefined;
|
|
159
167
|
|
|
160
|
-
this.
|
|
161
|
-
this._clientCleanup = [];
|
|
168
|
+
this._cleanupSubscriptions.unsubscribe();
|
|
162
169
|
|
|
163
170
|
// Close client if it wasn't already closed.
|
|
164
171
|
this._client.close().catch(() => {});
|
|
165
172
|
|
|
166
|
-
this._setState(
|
|
173
|
+
this._setState(State.RE_CONNECTING);
|
|
167
174
|
this._createClient();
|
|
168
175
|
}, this._reconnectAfter);
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
async close () {
|
|
172
|
-
this.
|
|
173
|
-
this._clientCleanup = [];
|
|
179
|
+
this._cleanupSubscriptions.unsubscribe();
|
|
174
180
|
|
|
175
181
|
if (this._reconnectIntervalId !== undefined) {
|
|
176
182
|
clearTimeout(this._reconnectIntervalId);
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
await this._client.close();
|
|
180
|
-
this._setState(
|
|
186
|
+
this._setState(State.CLOSED);
|
|
187
|
+
log('Closed.');
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
getStatus (): SignalApi.Status {
|
|
@@ -191,73 +198,72 @@ export class SignalClient {
|
|
|
191
198
|
};
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
async join (topic: PublicKey, peerId: PublicKey): Promise<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
return peers.map(id => PublicKey.from(id));
|
|
201
|
+
async join (topic: PublicKey, peerId: PublicKey): Promise<void> {
|
|
202
|
+
log(`Join: topic=${topic} peerId=${peerId}`);
|
|
203
|
+
await this._subscribeMessages(peerId);
|
|
204
|
+
await this._subscribeSwarmEvents(topic, peerId);
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
async leave (topic: PublicKey, peerId: PublicKey): Promise<void> {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
log(`Leave: topic=${topic} peerId=${peerId}`);
|
|
209
|
+
|
|
210
|
+
this._swarmStreams.get(topic)?.close();
|
|
211
|
+
this._swarmStreams.delete(topic);
|
|
212
|
+
|
|
213
|
+
this._messageStreams.get(topic)?.close();
|
|
214
|
+
this._messageStreams.delete(topic);
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
async
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
async signal (message: SignalMessage): Promise<void> {
|
|
218
|
+
const payload: Any = {
|
|
219
|
+
type_url: 'dxos.mesh.signalMessage.SignalMessage',
|
|
220
|
+
value: schema.getCodecForType('dxos.mesh.signalMessage.SignalMessage').encode(message)
|
|
221
|
+
};
|
|
222
|
+
return this._client.sendMessage(message.id, message.remoteId, payload);
|
|
214
223
|
}
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
@synchronized
|
|
226
|
+
private async _subscribeSwarmEvents (topic: PublicKey, peerId: PublicKey): Promise<void> {
|
|
227
|
+
assert(!this._swarmStreams.has(topic));
|
|
228
|
+
const swarmStream = await this._client.join(topic, peerId);
|
|
229
|
+
// Subscribing to swarm events.
|
|
230
|
+
// TODO(mykola): What happens when the swarm stream is closed? Maybe send leave event for each peer?
|
|
231
|
+
swarmStream.subscribe((swarmEvent: SwarmEvent) => {
|
|
232
|
+
this.swarmEvent.emit([topic, swarmEvent]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Saving swarm stream.
|
|
236
|
+
this._swarmStreams.set(topic, swarmStream);
|
|
237
|
+
|
|
238
|
+
this._cleanupSubscriptions.push(() => {
|
|
239
|
+
swarmStream.close();
|
|
240
|
+
this._swarmStreams.delete(topic);
|
|
228
241
|
});
|
|
229
242
|
}
|
|
230
243
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
private async _subscribeMessages (peerId: PublicKey) {
|
|
245
|
+
// Subscribing to messages.
|
|
246
|
+
const messageStream = await this._client.receiveMessages(peerId);
|
|
247
|
+
messageStream.subscribe(async (message: Message) => {
|
|
248
|
+
if (message.payload.type_url === 'dxos.mesh.signalMessage.SignalMessage') {
|
|
249
|
+
const signalMessage = schema.getCodecForType('dxos.mesh.signalMessage.SignalMessage').decode(message.payload.value);
|
|
250
|
+
log('Message received: ' + JSON.stringify(signalMessage));
|
|
251
|
+
assert(signalMessage.remoteId.equals(peerId));
|
|
252
|
+
await this._onSignal(signalMessage);
|
|
253
|
+
} else {
|
|
254
|
+
log('Unknown message type: ' + message.payload.type_url);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Saving message stream.
|
|
259
|
+
if (!this._messageStreams.has(peerId)) {
|
|
260
|
+
this._messageStreams.set(peerId, messageStream);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this._cleanupSubscriptions.push(() => {
|
|
264
|
+
messageStream.close();
|
|
265
|
+
this._messageStreams.delete(peerId);
|
|
266
|
+
});
|
|
244
267
|
}
|
|
245
|
-
}
|
|
246
268
|
|
|
247
|
-
/**
|
|
248
|
-
* Messages as processed by the signal server.
|
|
249
|
-
*/
|
|
250
|
-
interface SignalMessage{
|
|
251
|
-
/**
|
|
252
|
-
* Sender's public key.
|
|
253
|
-
*/
|
|
254
|
-
id?: Buffer;
|
|
255
|
-
/**
|
|
256
|
-
* Receiver`s public key.
|
|
257
|
-
*/
|
|
258
|
-
remoteId?: Buffer;
|
|
259
|
-
topic?: Buffer;
|
|
260
|
-
sessionId?: Buffer;
|
|
261
|
-
data?: Buffer;
|
|
262
|
-
messageId?: Buffer;
|
|
263
269
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import debug from 'debug';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
|
|
8
|
+
import { Event, synchronized } from '@dxos/async';
|
|
9
|
+
import { PublicKey } from '@dxos/protocols';
|
|
10
|
+
import { ComplexMap } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { SwarmEvent } from '../proto/gen/dxos/mesh/signal';
|
|
13
|
+
import { SignalMessage } from '../proto/gen/dxos/mesh/signalMessage';
|
|
14
|
+
import { SignalApi } from './signal-api';
|
|
15
|
+
import { SignalClient } from './signal-client';
|
|
16
|
+
import { SignalManager } from './signal-manager';
|
|
17
|
+
|
|
18
|
+
const log = debug('dxos:network-manager:signal-manager-impl');
|
|
19
|
+
|
|
20
|
+
export class SignalManagerImpl implements SignalManager {
|
|
21
|
+
private readonly _servers = new Map<string, SignalClient>();
|
|
22
|
+
|
|
23
|
+
/** Topics joined: topic => peerId */
|
|
24
|
+
private readonly _topicsJoined = new ComplexMap<PublicKey, PublicKey>(topic => topic.toHex());
|
|
25
|
+
/** host => topic => peerId */
|
|
26
|
+
private readonly _topicsJoinedPerSignal = new Map<string, ComplexMap<PublicKey, PublicKey>>();
|
|
27
|
+
|
|
28
|
+
private _reconciling?: boolean = false;
|
|
29
|
+
private _reconcileTimeoutId?: NodeJS.Timeout;
|
|
30
|
+
private _destroyed = false;
|
|
31
|
+
|
|
32
|
+
readonly statusChanged = new Event<SignalApi.Status[]>();
|
|
33
|
+
readonly commandTrace = new Event<SignalApi.CommandTrace>();
|
|
34
|
+
readonly swarmEvent = new Event<[topic: PublicKey, swarmEvent: SwarmEvent]>();
|
|
35
|
+
readonly onMessage = new Event<SignalMessage>();
|
|
36
|
+
|
|
37
|
+
constructor (
|
|
38
|
+
private readonly _hosts: string[]
|
|
39
|
+
) {
|
|
40
|
+
log(`Created WebsocketSignalManager with signal servers: ${_hosts}`);
|
|
41
|
+
assert(_hosts.length === 1, 'Only a single signaling server connection is supported');
|
|
42
|
+
for (const host of this._hosts) {
|
|
43
|
+
const server = new SignalClient(
|
|
44
|
+
host,
|
|
45
|
+
async msg => this.onMessage.emit(msg)
|
|
46
|
+
);
|
|
47
|
+
// TODO(mykola): Add subscription group
|
|
48
|
+
server.swarmEvent.on(data => this.swarmEvent.emit(data));
|
|
49
|
+
server.statusChanged.on(() => this.statusChanged.emit(this.getStatus()));
|
|
50
|
+
|
|
51
|
+
this._servers.set(host, server);
|
|
52
|
+
server.commandTrace.on(trace => this.commandTrace.emit(trace));
|
|
53
|
+
this._topicsJoinedPerSignal.set(host, new ComplexMap(x => x.toHex()));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getStatus (): SignalApi.Status[] {
|
|
58
|
+
return Array.from(this._servers.values()).map(server => server.getStatus());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
join (topic: PublicKey, peerId: PublicKey) {
|
|
62
|
+
assert(!this._topicsJoined.has(topic), `Topic ${topic} is already joined`);
|
|
63
|
+
log(`Join ${topic} ${peerId}`);
|
|
64
|
+
this._topicsJoined.set(topic, peerId);
|
|
65
|
+
this._scheduleReconcile();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
leave (topic: PublicKey, peerId: PublicKey) {
|
|
69
|
+
assert(!!this._topicsJoined.has(topic), `Topic ${topic} was not joined`);
|
|
70
|
+
log(`Leave ${topic} ${peerId}`);
|
|
71
|
+
this._topicsJoined.delete(topic);
|
|
72
|
+
this._scheduleReconcile();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private _scheduleReconcile () {
|
|
76
|
+
if (this._destroyed) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this._reconciling) {
|
|
81
|
+
this._reconciling = true;
|
|
82
|
+
this._reconcileJoinedTopics().then(
|
|
83
|
+
() => {
|
|
84
|
+
this._reconciling = false;
|
|
85
|
+
},
|
|
86
|
+
err => {
|
|
87
|
+
this._reconciling = false;
|
|
88
|
+
log(`Error while reconciling: ${err}`);
|
|
89
|
+
this._reconcileLater();
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
this._reconcileLater();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private _reconcileLater () {
|
|
98
|
+
if (this._destroyed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this._reconcileTimeoutId) {
|
|
103
|
+
this._reconcileTimeoutId = setTimeout(async () => this._scheduleReconcile(), 3000);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@synchronized
|
|
108
|
+
private async _reconcileJoinedTopics () {
|
|
109
|
+
// TODO(mykola): Handle reconnects to SS. Maybe move map of joined topics to signal-client.
|
|
110
|
+
log('Reconciling..');
|
|
111
|
+
for (const [host, server] of this._servers) {
|
|
112
|
+
const actualJoinedTopics = this._topicsJoinedPerSignal.get(host)!;
|
|
113
|
+
|
|
114
|
+
// Leave swarms
|
|
115
|
+
for (const [topic, actualPeerId] of actualJoinedTopics) {
|
|
116
|
+
try {
|
|
117
|
+
const desiredPeerId = this._topicsJoined.get(topic);
|
|
118
|
+
if (!desiredPeerId || !desiredPeerId.equals(actualPeerId)) {
|
|
119
|
+
await server.leave(topic, actualPeerId);
|
|
120
|
+
actualJoinedTopics.delete(topic);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log(`Error leaving swarm: ${err}`);
|
|
124
|
+
this._scheduleReconcile();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Join swarms
|
|
129
|
+
for (const [topic, desiredPeerId] of this._topicsJoined) {
|
|
130
|
+
try {
|
|
131
|
+
const actualPeerId = actualJoinedTopics.get(topic);
|
|
132
|
+
if (!actualPeerId) {
|
|
133
|
+
await server.join(topic, desiredPeerId);
|
|
134
|
+
actualJoinedTopics.set(topic, desiredPeerId);
|
|
135
|
+
} else {
|
|
136
|
+
if (!actualPeerId.equals(desiredPeerId)) {
|
|
137
|
+
throw new Error(`Joined with peerId different from desired: ${JSON.stringify({ actualPeerId, desiredPeerId })}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
log(`Error joining swarm: ${err}`);
|
|
142
|
+
this._scheduleReconcile();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
log('Done reconciling..');
|
|
147
|
+
this._reconciling = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async message (msg: SignalMessage) {
|
|
151
|
+
log(`Signal ${msg.remoteId}`);
|
|
152
|
+
for (const server of this._servers.values()) {
|
|
153
|
+
server.signal(msg).catch(err => {
|
|
154
|
+
log(`Error signaling: ${err}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async destroy () {
|
|
160
|
+
this._destroyed = true;
|
|
161
|
+
if (this._reconcileTimeoutId) {
|
|
162
|
+
clearTimeout(this._reconcileTimeoutId);
|
|
163
|
+
}
|
|
164
|
+
await Promise.all(Array.from(this._servers.values()).map(server => server.close()));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -5,18 +5,14 @@
|
|
|
5
5
|
import { Event } from '@dxos/async';
|
|
6
6
|
import { PublicKey } from '@dxos/protocols';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { SwarmEvent } from '../proto/gen/dxos/mesh/signal';
|
|
9
|
+
import { Answer, SignalMessage } from '../proto/gen/dxos/mesh/signalMessage';
|
|
9
10
|
import { SignalApi } from './signal-api';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Signal peer discovery interface.
|
|
13
14
|
*/
|
|
14
15
|
export interface SignalConnection {
|
|
15
|
-
/**
|
|
16
|
-
* Find peers (triggers async event).
|
|
17
|
-
*/
|
|
18
|
-
lookup (topic: PublicKey): void
|
|
19
|
-
|
|
20
16
|
/**
|
|
21
17
|
* Join topic on signal network, to be discoverable by other peers.
|
|
22
18
|
*/
|
|
@@ -35,20 +31,24 @@ export interface SignalMessaging {
|
|
|
35
31
|
/**
|
|
36
32
|
* Offer/answer RPC.
|
|
37
33
|
*/
|
|
38
|
-
offer (msg:
|
|
34
|
+
offer (msg: SignalMessage): Promise<Answer>
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
|
-
*
|
|
37
|
+
* Reliably send a signal to a peer.
|
|
42
38
|
*/
|
|
43
|
-
signal (msg:
|
|
39
|
+
signal (msg: SignalMessage): Promise<void>
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
export interface SignalManager extends SignalConnection
|
|
42
|
+
export interface SignalManager extends SignalConnection {
|
|
47
43
|
statusChanged: Event<SignalApi.Status[]>
|
|
48
44
|
commandTrace: Event<SignalApi.CommandTrace>
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
swarmEvent: Event<[topic: PublicKey, swarmEvent: SwarmEvent]>
|
|
46
|
+
onMessage: Event<SignalMessage>
|
|
51
47
|
|
|
52
48
|
getStatus (): SignalApi.Status[]
|
|
53
49
|
destroy(): Promise<void>
|
|
50
|
+
/**
|
|
51
|
+
* Send message to peer.
|
|
52
|
+
*/
|
|
53
|
+
message (msg: SignalMessage): Promise<void>
|
|
54
54
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { expect } from 'earljs';
|
|
5
|
+
|
|
6
|
+
import { Any } from '@dxos/codec-protobuf';
|
|
7
|
+
import { PublicKey } from '@dxos/protocols';
|
|
8
|
+
import { createTestBroker, TestBroker } from '@dxos/signal';
|
|
9
|
+
|
|
10
|
+
import { Message, SwarmEvent } from '../proto/gen/dxos/mesh/signal';
|
|
11
|
+
import { SignalRPCClient } from './signal-rpc-client';
|
|
12
|
+
|
|
13
|
+
describe('SignalRPCClient', () => {
|
|
14
|
+
let broker: TestBroker;
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
broker = await createTestBroker();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
after(() => {
|
|
21
|
+
broker.stop();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const setup = async () => {
|
|
25
|
+
const client = new SignalRPCClient(broker.url());
|
|
26
|
+
return client;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it('signal between 2 peers', async () => {
|
|
30
|
+
const client1 = await setup();
|
|
31
|
+
const client2 = await setup();
|
|
32
|
+
|
|
33
|
+
const peerId1 = PublicKey.random();
|
|
34
|
+
const peerId2 = PublicKey.random();
|
|
35
|
+
|
|
36
|
+
const stream1 = await client1.receiveMessages(peerId1);
|
|
37
|
+
const message: Any = {
|
|
38
|
+
'type_url': 'test',
|
|
39
|
+
value: Uint8Array.from([1, 2, 3])
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await client2.sendMessage(peerId2, peerId1, message);
|
|
43
|
+
|
|
44
|
+
const received: Message = await new Promise(resolve => {
|
|
45
|
+
stream1.subscribe(message => {
|
|
46
|
+
resolve(message);
|
|
47
|
+
}, (error) => {
|
|
48
|
+
if (error) {
|
|
49
|
+
console.log(error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
expect(received.author).toEqual(peerId2.asUint8Array());
|
|
55
|
+
stream1.close();
|
|
56
|
+
}).timeout(10000);
|
|
57
|
+
|
|
58
|
+
it('join', async () => {
|
|
59
|
+
const client1 = await setup();
|
|
60
|
+
const client2 = await setup();
|
|
61
|
+
|
|
62
|
+
const peerId1 = PublicKey.random();
|
|
63
|
+
const peerId2 = PublicKey.random();
|
|
64
|
+
const topic = PublicKey.random();
|
|
65
|
+
|
|
66
|
+
const stream1 = await client1.join(topic, peerId1);
|
|
67
|
+
const promise = new Promise<SwarmEvent>(resolve => {
|
|
68
|
+
stream1.subscribe((event: SwarmEvent) => {
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
70
|
+
if (peerId2.equals(event.peerAvailable?.peer!)) {
|
|
71
|
+
resolve(event);
|
|
72
|
+
}
|
|
73
|
+
}, (error) => {
|
|
74
|
+
if (error) {
|
|
75
|
+
console.log(error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const stream2 = await client2.join(topic, peerId2);
|
|
81
|
+
|
|
82
|
+
expect((await promise).peerAvailable?.peer).toEqual(peerId2.asBuffer());
|
|
83
|
+
stream1.close();
|
|
84
|
+
stream2.close();
|
|
85
|
+
}).timeout(10000);
|
|
86
|
+
});
|