@dxos/network-manager 2.33.5-dev.fa6b779b → 2.33.6-dev.21302cba
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/browser-mocha/bundle.js +413 -279
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/network-manager.blueprint-test.js +5 -1
- package/dist/src/network-manager.blueprint-test.js.map +1 -1
- package/dist/src/network-manager.d.ts +1 -1
- package/dist/src/network-manager.d.ts.map +1 -1
- package/dist/src/network-manager.js +19 -17
- package/dist/src/network-manager.js.map +1 -1
- package/dist/src/signal/in-memory-signal-manager.d.ts +2 -2
- package/dist/src/signal/in-memory-signal-manager.d.ts.map +1 -1
- package/dist/src/signal/in-memory-signal-manager.js +2 -1
- package/dist/src/signal/in-memory-signal-manager.js.map +1 -1
- package/dist/src/signal/index.d.ts +2 -1
- package/dist/src/signal/index.d.ts.map +1 -1
- package/dist/src/signal/index.js +7 -2
- package/dist/src/signal/index.js.map +1 -1
- package/dist/src/signal/signal-api.d.ts +8 -66
- package/dist/src/signal/signal-api.d.ts.map +1 -1
- package/dist/src/signal/signal-api.js +2 -190
- package/dist/src/signal/signal-api.js.map +1 -1
- package/dist/src/signal/signal-client.d.ts +54 -0
- package/dist/src/signal/signal-client.d.ts.map +1 -0
- package/dist/src/signal/signal-client.js +198 -0
- package/dist/src/signal/signal-client.js.map +1 -0
- package/dist/src/signal/signal-client.test.d.ts +2 -0
- package/dist/src/signal/signal-client.test.d.ts.map +1 -0
- package/dist/src/signal/{signal-api.test.js → signal-client.test.js} +27 -27
- package/dist/src/signal/signal-client.test.js.map +1 -0
- package/dist/src/signal/{interface.d.ts → signal-manager.d.ts} +16 -5
- package/dist/src/signal/signal-manager.d.ts.map +1 -0
- package/dist/src/signal/{interface.js → signal-manager.js} +1 -1
- package/dist/src/signal/signal-manager.js.map +1 -0
- package/dist/src/signal/websocket-rpc.d.ts.map +1 -1
- package/dist/src/signal/websocket-rpc.js.map +1 -1
- package/dist/src/signal/websocket-signal-manager.d.ts +4 -4
- package/dist/src/signal/websocket-signal-manager.d.ts.map +1 -1
- package/dist/src/signal/websocket-signal-manager.js +4 -6
- package/dist/src/signal/websocket-signal-manager.js.map +1 -1
- package/dist/src/swarm/connection.d.ts +4 -1
- package/dist/src/swarm/connection.d.ts.map +1 -1
- package/dist/src/swarm/connection.js +6 -3
- package/dist/src/swarm/connection.js.map +1 -1
- package/dist/src/swarm/index.js +5 -1
- package/dist/src/swarm/index.js.map +1 -1
- package/dist/src/swarm/swarm.d.ts +4 -6
- package/dist/src/swarm/swarm.d.ts.map +1 -1
- package/dist/src/swarm/swarm.js +11 -12
- package/dist/src/swarm/swarm.js.map +1 -1
- package/dist/src/swarm/swarm.test.js +17 -14
- package/dist/src/swarm/swarm.test.js.map +1 -1
- package/dist/src/testing/test-protocol.d.ts +1 -0
- package/dist/src/testing/test-protocol.d.ts.map +1 -1
- package/dist/src/topology/index.js +5 -1
- package/dist/src/topology/index.js.map +1 -1
- package/dist/src/transport/in-memory-transport.d.ts +1 -1
- package/dist/src/transport/in-memory-transport.d.ts.map +1 -1
- package/dist/src/transport/in-memory-transport.js +2 -2
- package/dist/src/transport/in-memory-transport.js.map +1 -1
- package/dist/src/transport/index.js +5 -1
- package/dist/src/transport/index.js.map +1 -1
- package/dist/src/transport/transport.d.ts +1 -1
- package/dist/src/transport/transport.d.ts.map +1 -1
- package/dist/src/transport/webrtc-transport.d.ts +4 -4
- package/dist/src/transport/webrtc-transport.d.ts.map +1 -1
- package/dist/src/transport/webrtc-transport.js +8 -7
- package/dist/src/transport/webrtc-transport.js.map +1 -1
- package/dist/src/transport/webrtc-transport.test.js +4 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/network-manager.ts +23 -23
- package/src/signal/in-memory-signal-manager.ts +6 -5
- package/src/signal/index.ts +2 -1
- package/src/signal/signal-api.ts +13 -246
- package/src/signal/{signal-api.test.ts → signal-client.test.ts} +27 -26
- package/src/signal/signal-client.ts +235 -0
- package/src/signal/signal-manager.ts +39 -0
- package/src/signal/websocket-rpc.ts +3 -6
- package/src/signal/websocket-signal-manager.ts +11 -15
- package/src/swarm/connection.ts +7 -6
- package/src/swarm/swarm.test.ts +32 -25
- package/src/swarm/swarm.ts +16 -15
- package/src/transport/in-memory-transport.ts +2 -2
- package/src/transport/transport.ts +13 -17
- package/src/transport/webrtc-transport.test.ts +5 -5
- package/src/transport/webrtc-transport.ts +6 -5
- package/dist/src/signal/interface.d.ts.map +0 -1
- package/dist/src/signal/interface.js.map +0 -1
- package/dist/src/signal/signal-api.test.d.ts +0 -2
- package/dist/src/signal/signal-api.test.d.ts.map +0 -1
- package/dist/src/signal/signal-api.test.js.map +0 -1
- package/src/signal/interface.ts +0 -32
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import debug from 'debug';
|
|
6
|
+
|
|
7
|
+
import { Event } from '@dxos/async';
|
|
8
|
+
import { PublicKey } from '@dxos/crypto';
|
|
9
|
+
|
|
10
|
+
import { SignalApi } from './signal-api';
|
|
11
|
+
import { WebsocketRpc } from './websocket-rpc';
|
|
12
|
+
|
|
13
|
+
const log = debug('dxos:network-manager:signal-client');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Establishes a websocket connection to signal server and provides RPC methods.
|
|
19
|
+
*/
|
|
20
|
+
export class SignalClient {
|
|
21
|
+
private _state = SignalApi.State.CONNECTING;
|
|
22
|
+
|
|
23
|
+
private _lastError?: Error;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Number of milliseconds after which the connection will be attempted again in case of error.
|
|
27
|
+
*/
|
|
28
|
+
private _reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Timestamp of when the connection attempt was began.
|
|
32
|
+
*/
|
|
33
|
+
private _connectionStarted = Date.now();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Timestamp of last state change.
|
|
37
|
+
*/
|
|
38
|
+
private _lastStateChange = Date.now();
|
|
39
|
+
|
|
40
|
+
private _reconnectIntervalId?: NodeJS.Timeout;
|
|
41
|
+
|
|
42
|
+
private _client!: WebsocketRpc;
|
|
43
|
+
|
|
44
|
+
private _clientCleanup: (() => void)[] = [];
|
|
45
|
+
|
|
46
|
+
readonly statusChanged = new Event<SignalApi.Status>();
|
|
47
|
+
readonly commandTrace = new Event<SignalApi.CommandTrace>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param _host Signal server websocket URL.
|
|
51
|
+
* @param _onOffer See `SignalApi.offer`.
|
|
52
|
+
* @param _onSignal See `SignalApi.signal`.
|
|
53
|
+
*/
|
|
54
|
+
constructor (
|
|
55
|
+
private readonly _host: string,
|
|
56
|
+
private readonly _onOffer: (message: SignalApi.SignalMessage) => Promise<SignalApi.Answer>,
|
|
57
|
+
private readonly _onSignal: (message: SignalApi.SignalMessage) => Promise<void>
|
|
58
|
+
) {
|
|
59
|
+
this._setState(SignalApi.State.CONNECTING);
|
|
60
|
+
this._createClient();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _setState (newState: SignalApi.State) {
|
|
64
|
+
this._state = newState;
|
|
65
|
+
this._lastStateChange = Date.now();
|
|
66
|
+
log(`Signal state changed ${JSON.stringify(this.getStatus())}`);
|
|
67
|
+
this.statusChanged.emit(this.getStatus());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private _createClient () {
|
|
71
|
+
this._connectionStarted = Date.now();
|
|
72
|
+
try {
|
|
73
|
+
this._client = new WebsocketRpc(this._host);
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
if (this._state === SignalApi.State.RE_CONNECTING) {
|
|
76
|
+
this._reconnectAfter *= 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this._lastError = error;
|
|
80
|
+
this._setState(SignalApi.State.DISCONNECTED);
|
|
81
|
+
this._reconnect();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this._client.addHandler('offer', (message: any) => this._onOffer({
|
|
85
|
+
id: PublicKey.from(message.id),
|
|
86
|
+
remoteId: PublicKey.from(message.remoteId),
|
|
87
|
+
topic: PublicKey.from(message.topic),
|
|
88
|
+
sessionId: PublicKey.from(message.sessionId),
|
|
89
|
+
data: message.data
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
this._client.subscribe('signal', (msg: SignalApi.SignalMessage) => this._onSignal({
|
|
93
|
+
id: PublicKey.from(msg.id),
|
|
94
|
+
remoteId: PublicKey.from(msg.remoteId),
|
|
95
|
+
topic: PublicKey.from(msg.topic),
|
|
96
|
+
sessionId: PublicKey.from(msg.sessionId),
|
|
97
|
+
data: msg.data
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
this._clientCleanup.push(this._client.connected.on(() => {
|
|
101
|
+
log('Socket connected');
|
|
102
|
+
this._lastError = undefined;
|
|
103
|
+
this._reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
|
|
104
|
+
this._setState(SignalApi.State.CONNECTED);
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
this._clientCleanup.push(this._client.error.on(error => {
|
|
108
|
+
log(`Socket error: ${error.message}`);
|
|
109
|
+
if (this._state === SignalApi.State.CLOSED) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this._state === SignalApi.State.RE_CONNECTING) {
|
|
114
|
+
this._reconnectAfter *= 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this._lastError = error;
|
|
118
|
+
this._setState(SignalApi.State.DISCONNECTED);
|
|
119
|
+
|
|
120
|
+
this._reconnect();
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
this._clientCleanup.push(this._client.disconnected.on(() => {
|
|
124
|
+
log('Socket disconnected');
|
|
125
|
+
// This is also called in case of error, but we already have disconnected the socket on error, so no need to do anything here.
|
|
126
|
+
if (this._state !== SignalApi.State.CONNECTING && this._state !== SignalApi.State.RE_CONNECTING) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this._state === SignalApi.State.RE_CONNECTING) {
|
|
131
|
+
this._reconnectAfter *= 2;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this._setState(SignalApi.State.DISCONNECTED);
|
|
135
|
+
this._reconnect();
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
this._clientCleanup.push(this._client.commandTrace.on(trace => this.commandTrace.emit(trace)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _reconnect () {
|
|
142
|
+
if (this._reconnectIntervalId !== undefined) {
|
|
143
|
+
console.error('Signal api already reconnecting.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (this._state === SignalApi.State.CLOSED) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this._reconnectIntervalId = setTimeout(() => {
|
|
151
|
+
this._reconnectIntervalId = undefined;
|
|
152
|
+
|
|
153
|
+
this._clientCleanup.forEach(cb => cb());
|
|
154
|
+
this._clientCleanup = [];
|
|
155
|
+
|
|
156
|
+
// Close client if it wasn't already closed.
|
|
157
|
+
this._client.close().catch(() => {});
|
|
158
|
+
|
|
159
|
+
this._setState(SignalApi.State.RE_CONNECTING);
|
|
160
|
+
this._createClient();
|
|
161
|
+
}, this._reconnectAfter);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async close () {
|
|
165
|
+
this._clientCleanup.forEach(cb => cb());
|
|
166
|
+
this._clientCleanup = [];
|
|
167
|
+
|
|
168
|
+
if (this._reconnectIntervalId !== undefined) {
|
|
169
|
+
clearTimeout(this._reconnectIntervalId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await this._client.close();
|
|
173
|
+
this._setState(SignalApi.State.CLOSED);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getStatus (): SignalApi.Status {
|
|
177
|
+
return {
|
|
178
|
+
host: this._host,
|
|
179
|
+
state: this._state,
|
|
180
|
+
error: this._lastError?.message,
|
|
181
|
+
reconnectIn: this._reconnectAfter,
|
|
182
|
+
connectionStarted: this._connectionStarted,
|
|
183
|
+
lastStateChange: this._lastStateChange
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async join (topic: PublicKey, peerId: PublicKey): Promise<PublicKey[]> {
|
|
188
|
+
const peers: Buffer[] = await this._client.call('join', {
|
|
189
|
+
id: peerId.asBuffer(),
|
|
190
|
+
topic: topic.asBuffer()
|
|
191
|
+
});
|
|
192
|
+
return peers.map(id => PublicKey.from(id));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async leave (topic: PublicKey, peerId: PublicKey): Promise<void> {
|
|
196
|
+
await this._client.call('leave', {
|
|
197
|
+
id: peerId.asBuffer(),
|
|
198
|
+
topic: topic.asBuffer()
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async lookup (topic: PublicKey): Promise<PublicKey[]> {
|
|
203
|
+
const peers: Buffer[] = await this._client.call('lookup', {
|
|
204
|
+
topic: topic.asBuffer()
|
|
205
|
+
});
|
|
206
|
+
return peers.map(id => PublicKey.from(id));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Routes an offer to the other peer's _onOffer callback.
|
|
211
|
+
* @returns Other peer's _onOffer callback return value.
|
|
212
|
+
*/
|
|
213
|
+
async offer (payload: SignalApi.SignalMessage): Promise<SignalApi.Answer> {
|
|
214
|
+
return this._client.call('offer', {
|
|
215
|
+
id: payload.id.asBuffer(),
|
|
216
|
+
remoteId: payload.remoteId.asBuffer(),
|
|
217
|
+
topic: payload.topic.asBuffer(),
|
|
218
|
+
sessionId: payload.sessionId.asBuffer(),
|
|
219
|
+
data: payload.data
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Routes an offer to the other peer's _onSignal callback.
|
|
225
|
+
*/
|
|
226
|
+
async signal (payload: SignalApi.SignalMessage): Promise<void> {
|
|
227
|
+
return this._client.emit('signal', {
|
|
228
|
+
id: payload.id.asBuffer(),
|
|
229
|
+
remoteId: payload.remoteId.asBuffer(),
|
|
230
|
+
topic: payload.topic.asBuffer(),
|
|
231
|
+
sessionId: payload.sessionId.asBuffer(),
|
|
232
|
+
data: payload.data
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Event } from '@dxos/async';
|
|
6
|
+
import { PublicKey } from '@dxos/crypto';
|
|
7
|
+
|
|
8
|
+
import { SignalApi } from './signal-api';
|
|
9
|
+
|
|
10
|
+
// TODO(burdon): Document methods.
|
|
11
|
+
export interface SignalConnection {
|
|
12
|
+
/**
|
|
13
|
+
* Find peers (triggers async event).
|
|
14
|
+
*/
|
|
15
|
+
lookup (topic: PublicKey): void
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
// TODO(burdon): Document.
|
|
21
|
+
offer (msg: SignalApi.SignalMessage): Promise<SignalApi.Answer>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Send message to peer.
|
|
25
|
+
*/
|
|
26
|
+
signal (msg: SignalApi.SignalMessage): Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SignalManager extends SignalConnection {
|
|
30
|
+
statusChanged: Event<SignalApi.Status[]>
|
|
31
|
+
commandTrace: Event<SignalApi.CommandTrace>
|
|
32
|
+
peerCandidatesChanged: Event<[topic: PublicKey, candidates: PublicKey[]]>
|
|
33
|
+
onSignal: Event<SignalApi.SignalMessage>
|
|
34
|
+
|
|
35
|
+
getStatus (): SignalApi.Status[]
|
|
36
|
+
join (topic: PublicKey, peerId: PublicKey): void
|
|
37
|
+
leave (topic: PublicKey, peerId: PublicKey): void
|
|
38
|
+
destroy(): Promise<void>
|
|
39
|
+
}
|
|
@@ -25,19 +25,13 @@ const RPC_TIMEOUT = 3_000;
|
|
|
25
25
|
*/
|
|
26
26
|
export class WebsocketRpc {
|
|
27
27
|
private readonly _connectTrigger = new Trigger();
|
|
28
|
-
|
|
29
28
|
private readonly _socket: WebSocket;
|
|
30
|
-
|
|
31
29
|
private readonly _rpc: any;
|
|
32
|
-
|
|
33
30
|
private _messageId = Date.now();
|
|
34
31
|
|
|
35
32
|
readonly commandTrace = new Event<SignalApi.CommandTrace>();
|
|
36
|
-
|
|
37
33
|
readonly connected = new Event();
|
|
38
|
-
|
|
39
34
|
readonly disconnected = new Event();
|
|
40
|
-
|
|
41
35
|
readonly error = new Event<Error>();
|
|
42
36
|
|
|
43
37
|
/**
|
|
@@ -58,6 +52,7 @@ export class WebsocketRpc {
|
|
|
58
52
|
this.error.emit(err);
|
|
59
53
|
}
|
|
60
54
|
};
|
|
55
|
+
|
|
61
56
|
this._socket.onclose = async () => {
|
|
62
57
|
log(`Disconnected ${this._host}`);
|
|
63
58
|
this.disconnected.emit();
|
|
@@ -67,10 +62,12 @@ export class WebsocketRpc {
|
|
|
67
62
|
this.error.emit(err);
|
|
68
63
|
}
|
|
69
64
|
};
|
|
65
|
+
|
|
70
66
|
this._socket.onerror = e => {
|
|
71
67
|
log(`Signal socket error ${this._host} ${e.message}`);
|
|
72
68
|
this.error.emit(e.error ?? new Error(e.message));
|
|
73
69
|
};
|
|
70
|
+
|
|
74
71
|
this._rpc = nanomessagerpc({
|
|
75
72
|
send: async (data: Uint8Array) => {
|
|
76
73
|
await this._connectTrigger.wait();
|
|
@@ -9,24 +9,25 @@ import { Event, synchronized } from '@dxos/async';
|
|
|
9
9
|
import { PublicKey } from '@dxos/crypto';
|
|
10
10
|
import { ComplexMap } from '@dxos/util';
|
|
11
11
|
|
|
12
|
-
import { SignalManager } from './interface';
|
|
13
12
|
import { SignalApi } from './signal-api';
|
|
13
|
+
import { SignalClient } from './signal-client';
|
|
14
|
+
import { SignalManager } from './signal-manager';
|
|
14
15
|
|
|
15
16
|
const log = debug('dxos:network-manager:websocket-signal-manager');
|
|
16
17
|
|
|
17
18
|
export class WebsocketSignalManager implements SignalManager {
|
|
18
|
-
private readonly _servers = new Map<string,
|
|
19
|
+
private readonly _servers = new Map<string, SignalClient>();
|
|
19
20
|
|
|
20
21
|
/** Topics joined: topic => peerId */
|
|
21
|
-
private readonly _topicsJoined = new ComplexMap<PublicKey, PublicKey>(
|
|
22
|
-
|
|
22
|
+
private readonly _topicsJoined = new ComplexMap<PublicKey, PublicKey>(topic => topic.toHex());
|
|
23
23
|
private readonly _topicsJoinedPerSignal = new Map<string, ComplexMap<PublicKey, PublicKey>>();
|
|
24
24
|
|
|
25
25
|
private _reconcileTimeoutId?: NodeJS.Timeout;
|
|
26
26
|
|
|
27
27
|
readonly statusChanged = new Event<SignalApi.Status[]>();
|
|
28
|
-
|
|
29
28
|
readonly commandTrace = new Event<SignalApi.CommandTrace>();
|
|
29
|
+
readonly peerCandidatesChanged = new Event<[topic: PublicKey, candidates: PublicKey[]]>()
|
|
30
|
+
readonly onSignal = new Event<SignalApi.SignalMessage>();
|
|
30
31
|
|
|
31
32
|
constructor (
|
|
32
33
|
private readonly _hosts: string[],
|
|
@@ -35,13 +36,12 @@ export class WebsocketSignalManager implements SignalManager {
|
|
|
35
36
|
log(`Created WebsocketSignalManager with signal servers: ${_hosts}`);
|
|
36
37
|
assert(_hosts.length === 1, 'Only a single signaling server connection is supported');
|
|
37
38
|
for (const host of this._hosts) {
|
|
38
|
-
const server = new
|
|
39
|
+
const server = new SignalClient(
|
|
39
40
|
host,
|
|
40
|
-
async
|
|
41
|
-
async msg =>
|
|
42
|
-
this.onSignal.emit(msg);
|
|
43
|
-
}
|
|
41
|
+
async msg => this._onOffer(msg),
|
|
42
|
+
async msg => this.onSignal.emit(msg)
|
|
44
43
|
);
|
|
44
|
+
|
|
45
45
|
this._servers.set(host, server);
|
|
46
46
|
server.statusChanged.on(() => this.statusChanged.emit(this.getStatus()));
|
|
47
47
|
server.commandTrace.on(trace => this.commandTrace.emit(trace));
|
|
@@ -143,7 +143,7 @@ export class WebsocketSignalManager implements SignalManager {
|
|
|
143
143
|
return Array.from(this._servers.values())[0].offer(msg);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
signal (msg: SignalApi.SignalMessage) {
|
|
146
|
+
async signal (msg: SignalApi.SignalMessage) {
|
|
147
147
|
log(`Signal ${msg.remoteId}`);
|
|
148
148
|
for (const server of this._servers.values()) {
|
|
149
149
|
void server.signal(msg);
|
|
@@ -154,8 +154,4 @@ export class WebsocketSignalManager implements SignalManager {
|
|
|
154
154
|
async destroy () {
|
|
155
155
|
await Promise.all(Array.from(this._servers.values()).map(server => server.close()));
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
peerCandidatesChanged = new Event<[topic: PublicKey, candidates: PublicKey[]]>()
|
|
159
|
-
|
|
160
|
-
onSignal = new Event<SignalApi.SignalMessage>();
|
|
161
157
|
}
|
package/src/swarm/connection.ts
CHANGED
|
@@ -45,15 +45,15 @@ export enum ConnectionState {
|
|
|
45
45
|
CLOSED = 'CLOSED',
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Represents a connection to a remote peer.
|
|
50
|
+
*/
|
|
48
51
|
export class Connection {
|
|
49
52
|
private _state: ConnectionState = ConnectionState.INITIAL;
|
|
50
|
-
|
|
51
53
|
private _transport: Transport | undefined;
|
|
52
|
-
|
|
53
54
|
private _bufferedSignals: SignalApi.SignalMessage[] = [];
|
|
54
55
|
|
|
55
56
|
readonly stateChanged = new Event<ConnectionState>();
|
|
56
|
-
|
|
57
57
|
readonly errors = new ErrorStream();
|
|
58
58
|
|
|
59
59
|
constructor (
|
|
@@ -110,12 +110,13 @@ export class Connection {
|
|
|
110
110
|
|
|
111
111
|
// Replay signals that were received before transport was created.
|
|
112
112
|
for (const signal of this._bufferedSignals) {
|
|
113
|
-
this._transport.signal(signal);
|
|
113
|
+
void this._transport.signal(signal); // TODO(burdon): Remove async?
|
|
114
114
|
}
|
|
115
|
+
|
|
115
116
|
this._bufferedSignals = [];
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
signal (msg: SignalApi.SignalMessage) {
|
|
119
|
+
async signal (msg: SignalApi.SignalMessage) {
|
|
119
120
|
if (!msg.sessionId.equals(this.sessionId)) {
|
|
120
121
|
log('Dropping signal for incorrect session id.');
|
|
121
122
|
return;
|
|
@@ -134,7 +135,7 @@ export class Connection {
|
|
|
134
135
|
|
|
135
136
|
assert(this._transport, 'Connection not ready to accept signals.');
|
|
136
137
|
log(`${this.ownId} received signal from ${this.remoteId}: ${msg.data.type}`);
|
|
137
|
-
this._transport.signal(msg);
|
|
138
|
+
await this._transport.signal(msg);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
@synchronized
|
package/src/swarm/swarm.test.ts
CHANGED
|
@@ -12,50 +12,57 @@ import { PublicKey } from '@dxos/crypto';
|
|
|
12
12
|
import { Protocol } from '@dxos/mesh-protocol';
|
|
13
13
|
import { afterTest } from '@dxos/testutils';
|
|
14
14
|
|
|
15
|
+
import { SignalApi, SignalConnection } from '../signal';
|
|
15
16
|
import { FullyConnectedTopology } from '../topology';
|
|
16
|
-
import {
|
|
17
|
+
import { createWebRTCTransportFactory, WebRTCTransport } from '../transport';
|
|
17
18
|
import { Swarm } from './swarm';
|
|
18
19
|
|
|
19
20
|
const log = debug('dxos:network-manager:swarm:test');
|
|
20
21
|
|
|
22
|
+
class MockSignalConnection implements SignalConnection {
|
|
23
|
+
constructor (
|
|
24
|
+
readonly _swarm: () => Swarm,
|
|
25
|
+
readonly _delay = 10
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
lookup (topic: PublicKey) {}
|
|
29
|
+
|
|
30
|
+
async offer (msg: SignalApi.SignalMessage) {
|
|
31
|
+
await sleep(this._delay);
|
|
32
|
+
return this._swarm().onOffer(msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async signal (msg: SignalApi.SignalMessage) {
|
|
36
|
+
await sleep(this._delay);
|
|
37
|
+
await this._swarm().onSignal(msg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
const setup = () => {
|
|
22
42
|
const topic = PublicKey.random();
|
|
23
43
|
const peerId1 = PublicKey.random();
|
|
24
44
|
const peerId2 = PublicKey.random();
|
|
45
|
+
|
|
25
46
|
const swarm1: Swarm = new Swarm(
|
|
26
47
|
topic,
|
|
27
48
|
peerId1,
|
|
28
49
|
new FullyConnectedTopology(),
|
|
29
50
|
() => new Protocol(),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return swarm2.onOffer(msg);
|
|
33
|
-
},
|
|
34
|
-
async msg => {
|
|
35
|
-
await sleep(10); // Simulating network delay.
|
|
36
|
-
await swarm2.onSignal(msg);
|
|
37
|
-
},
|
|
38
|
-
() => {},
|
|
39
|
-
createWebRtcTransportFactory(),
|
|
51
|
+
new MockSignalConnection(() => swarm2),
|
|
52
|
+
createWebRTCTransportFactory(),
|
|
40
53
|
undefined
|
|
41
54
|
);
|
|
55
|
+
|
|
42
56
|
const swarm2: Swarm = new Swarm(
|
|
43
57
|
topic,
|
|
44
58
|
peerId2,
|
|
45
59
|
new FullyConnectedTopology(),
|
|
46
60
|
() => new Protocol(),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return swarm1.onOffer(msg);
|
|
50
|
-
},
|
|
51
|
-
async msg => {
|
|
52
|
-
await sleep(10); // Simulating network delay.
|
|
53
|
-
await swarm1.onSignal(msg);
|
|
54
|
-
},
|
|
55
|
-
() => {},
|
|
56
|
-
createWebRtcTransportFactory(),
|
|
61
|
+
new MockSignalConnection(() => swarm1),
|
|
62
|
+
createWebRTCTransportFactory(),
|
|
57
63
|
undefined
|
|
58
64
|
);
|
|
65
|
+
|
|
59
66
|
afterTest(async () => {
|
|
60
67
|
await swarm1.destroy();
|
|
61
68
|
await swarm2.destroy();
|
|
@@ -84,10 +91,10 @@ test('connects two peers in a swarm', async () => {
|
|
|
84
91
|
const swarm1Connection = swarm1.connections[0];
|
|
85
92
|
const swarm2Connection = swarm2.connections[0];
|
|
86
93
|
const onData = mockFn<(data: Buffer) => void>().returns(undefined);
|
|
87
|
-
(swarm2Connection.transport as
|
|
94
|
+
(swarm2Connection.transport as WebRTCTransport).peer!.on('data', onData);
|
|
88
95
|
|
|
89
96
|
const data = Buffer.from('1234');
|
|
90
|
-
(swarm1Connection.transport as
|
|
97
|
+
(swarm1Connection.transport as WebRTCTransport).peer!.send(data);
|
|
91
98
|
await waitForExpect(() => {
|
|
92
99
|
expect(onData).toHaveBeenCalledWith([data]);
|
|
93
100
|
});
|
|
@@ -126,10 +133,10 @@ test('second peer discovered after delay', async () => {
|
|
|
126
133
|
const swarm1Connection = swarm1.connections[0];
|
|
127
134
|
const swarm2Connection = swarm2.connections[0];
|
|
128
135
|
const onData = mockFn<(data: Buffer) => void>().returns(undefined);
|
|
129
|
-
(swarm2Connection.transport as
|
|
136
|
+
(swarm2Connection.transport as WebRTCTransport).peer!.on('data', onData);
|
|
130
137
|
|
|
131
138
|
const data = Buffer.from('1234');
|
|
132
|
-
(swarm1Connection.transport as
|
|
139
|
+
(swarm1Connection.transport as WebRTCTransport).peer!.send(data);
|
|
133
140
|
await waitForExpect(() => {
|
|
134
141
|
expect(onData).toHaveBeenCalledWith([data]);
|
|
135
142
|
});
|
package/src/swarm/swarm.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ErrorStream } from '@dxos/debug';
|
|
|
11
11
|
import { ComplexMap, ComplexSet } from '@dxos/util';
|
|
12
12
|
|
|
13
13
|
import { ProtocolProvider } from '../network-manager';
|
|
14
|
-
import { SignalApi } from '../signal';
|
|
14
|
+
import { SignalApi, SignalConnection } from '../signal';
|
|
15
15
|
import { SwarmController, Topology } from '../topology';
|
|
16
16
|
import { TransportFactory } from '../transport';
|
|
17
17
|
import { Topic } from '../types';
|
|
@@ -31,9 +31,7 @@ export class Swarm {
|
|
|
31
31
|
readonly id = PublicKey.random();
|
|
32
32
|
|
|
33
33
|
private readonly _connections = new ComplexMap<PublicKey, Connection>(x => x.toHex());
|
|
34
|
-
|
|
35
34
|
private readonly _discoveredPeers = new ComplexSet<PublicKey>(x => x.toHex());
|
|
36
|
-
|
|
37
35
|
private readonly _peerCandidatesUpdated = new Event();
|
|
38
36
|
|
|
39
37
|
get connections () {
|
|
@@ -62,10 +60,8 @@ export class Swarm {
|
|
|
62
60
|
private readonly _topic: PublicKey,
|
|
63
61
|
private readonly _ownPeerId: PublicKey,
|
|
64
62
|
private _topology: Topology,
|
|
65
|
-
private readonly
|
|
66
|
-
private readonly
|
|
67
|
-
private readonly _sendSignal: (message: SignalApi.SignalMessage) => Promise<void>,
|
|
68
|
-
private readonly _lookup: () => void,
|
|
63
|
+
private readonly _protocolProvider: ProtocolProvider,
|
|
64
|
+
private readonly _signalConnection: SignalConnection,
|
|
69
65
|
private readonly _transportFactory: TransportFactory,
|
|
70
66
|
private readonly _label: string | undefined
|
|
71
67
|
) {
|
|
@@ -148,7 +144,8 @@ export class Swarm {
|
|
|
148
144
|
log(`Dropping signal message for non-existent connection: topic=${this._topic}, peerId=${message.id}`);
|
|
149
145
|
return;
|
|
150
146
|
}
|
|
151
|
-
|
|
147
|
+
|
|
148
|
+
await connection.signal(message);
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
async setTopology (newTopology: Topology) {
|
|
@@ -185,7 +182,7 @@ export class Swarm {
|
|
|
185
182
|
this._topology.update();
|
|
186
183
|
},
|
|
187
184
|
lookup: () => {
|
|
188
|
-
this.
|
|
185
|
+
this._signalConnection.lookup(this._topic);
|
|
189
186
|
}
|
|
190
187
|
};
|
|
191
188
|
}
|
|
@@ -199,7 +196,7 @@ export class Swarm {
|
|
|
199
196
|
const sessionId = PublicKey.random();
|
|
200
197
|
|
|
201
198
|
const connection = this._createConnection(true, remoteId, sessionId);
|
|
202
|
-
this.
|
|
199
|
+
this._signalConnection.offer({
|
|
203
200
|
id: this._ownPeerId,
|
|
204
201
|
remoteId,
|
|
205
202
|
sessionId,
|
|
@@ -209,7 +206,7 @@ export class Swarm {
|
|
|
209
206
|
.then(answer => {
|
|
210
207
|
log(`Received answer: ${JSON.stringify(answer)} topic=${this._topic} ownId=${this._ownPeerId} remoteId=${remoteId}`);
|
|
211
208
|
if (connection.state !== ConnectionState.INITIAL) {
|
|
212
|
-
log('Ignoring answer');
|
|
209
|
+
log('Ignoring answer.');
|
|
213
210
|
return;
|
|
214
211
|
}
|
|
215
212
|
|
|
@@ -228,12 +225,13 @@ export class Swarm {
|
|
|
228
225
|
.catch(err => {
|
|
229
226
|
this.errors.raise(err);
|
|
230
227
|
});
|
|
228
|
+
|
|
231
229
|
this._topology.update();
|
|
232
230
|
}
|
|
233
231
|
|
|
234
232
|
private _createConnection (initiator: boolean, remoteId: PublicKey, sessionId: PublicKey) {
|
|
235
233
|
log(`Create connection topic=${this._topic} remoteId=${remoteId} initiator=${initiator}`);
|
|
236
|
-
assert(!this._connections.has(remoteId), 'Peer already connected');
|
|
234
|
+
assert(!this._connections.has(remoteId), 'Peer already connected.');
|
|
237
235
|
|
|
238
236
|
const connection = new Connection(
|
|
239
237
|
this._topic,
|
|
@@ -241,8 +239,8 @@ export class Swarm {
|
|
|
241
239
|
remoteId,
|
|
242
240
|
sessionId,
|
|
243
241
|
initiator,
|
|
244
|
-
this.
|
|
245
|
-
this.
|
|
242
|
+
(msg: SignalApi.SignalMessage) => this._signalConnection.signal(msg),
|
|
243
|
+
this._protocolProvider({ channel: discoveryKey(this._topic), initiator }),
|
|
246
244
|
this._transportFactory
|
|
247
245
|
);
|
|
248
246
|
|
|
@@ -258,12 +256,14 @@ export class Swarm {
|
|
|
258
256
|
|
|
259
257
|
void connection.stateChanged.waitFor(s => s === ConnectionState.CLOSED).then(() => {
|
|
260
258
|
log(`Connection closed topic=${this._topic} remoteId=${remoteId} initiator=${initiator}`);
|
|
261
|
-
// Connection might have been already closed or replace by a different one.
|
|
259
|
+
// Connection might have been already closed or replace by a different one.
|
|
260
|
+
// Only remove the connection if it has the same session id.
|
|
262
261
|
if (this._connections.get(remoteId)?.sessionId.equals(sessionId)) {
|
|
263
262
|
this._connections.delete(remoteId);
|
|
264
263
|
this.connectionRemoved.emit(connection);
|
|
265
264
|
}
|
|
266
265
|
});
|
|
266
|
+
|
|
267
267
|
return connection;
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -273,6 +273,7 @@ export class Swarm {
|
|
|
273
273
|
if (!connection) {
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
|
+
|
|
276
277
|
this._connections.delete(peerId);
|
|
277
278
|
await connection.close();
|
|
278
279
|
}
|
|
@@ -79,8 +79,8 @@ export class InMemoryTransport implements Transport {
|
|
|
79
79
|
return this._sessionId;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
signal (msg: SignalApi.SignalMessage)
|
|
83
|
-
//
|
|
82
|
+
async signal (msg: SignalApi.SignalMessage) {
|
|
83
|
+
// No-op.
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async close (): Promise<void> {
|