@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.
Files changed (148) hide show
  1. package/dist/src/network-manager.blueprint-test.d.ts +3 -1
  2. package/dist/src/network-manager.blueprint-test.d.ts.map +1 -1
  3. package/dist/src/network-manager.blueprint-test.js +46 -17
  4. package/dist/src/network-manager.blueprint-test.js.map +1 -1
  5. package/dist/src/network-manager.browser-test.js +1 -1
  6. package/dist/src/network-manager.browser-test.js.map +1 -1
  7. package/dist/src/network-manager.d.ts.map +1 -1
  8. package/dist/src/network-manager.js +6 -6
  9. package/dist/src/network-manager.js.map +1 -1
  10. package/dist/src/network-manager.test.js +5 -4
  11. package/dist/src/network-manager.test.js.map +1 -1
  12. package/dist/src/proto/gen/dxos/credentials.d.ts.map +1 -1
  13. package/dist/src/proto/gen/dxos/halo/keys.d.ts.map +1 -1
  14. package/dist/src/proto/gen/dxos/halo/keys.js.map +1 -1
  15. package/dist/src/proto/gen/dxos/mesh/signal.d.ts +52 -45
  16. package/dist/src/proto/gen/dxos/mesh/signal.d.ts.map +1 -1
  17. package/dist/src/proto/gen/dxos/mesh/signalMessage.d.ts +79 -0
  18. package/dist/src/proto/gen/dxos/mesh/signalMessage.d.ts.map +1 -0
  19. package/dist/src/proto/gen/dxos/mesh/signalMessage.js +3 -0
  20. package/dist/src/proto/gen/dxos/mesh/signalMessage.js.map +1 -0
  21. package/dist/src/proto/gen/google/protobuf.d.ts +6 -0
  22. package/dist/src/proto/gen/google/protobuf.d.ts.map +1 -1
  23. package/dist/src/proto/gen/index.d.ts +17 -5
  24. package/dist/src/proto/gen/index.d.ts.map +1 -1
  25. package/dist/src/proto/gen/index.js +1 -1
  26. package/dist/src/proto/gen/index.js.map +1 -1
  27. package/dist/src/proto/substitutions.d.ts +4 -0
  28. package/dist/src/proto/substitutions.d.ts.map +1 -1
  29. package/dist/src/proto/substitutions.js +3 -1
  30. package/dist/src/proto/substitutions.js.map +1 -1
  31. package/dist/src/signal/in-memory-signal-manager.d.ts +7 -7
  32. package/dist/src/signal/in-memory-signal-manager.d.ts.map +1 -1
  33. package/dist/src/signal/in-memory-signal-manager.js +29 -8
  34. package/dist/src/signal/in-memory-signal-manager.js.map +1 -1
  35. package/dist/src/signal/index.d.ts +1 -2
  36. package/dist/src/signal/index.d.ts.map +1 -1
  37. package/dist/src/signal/index.js +1 -2
  38. package/dist/src/signal/index.js.map +1 -1
  39. package/dist/src/signal/integration.test.d.ts +2 -0
  40. package/dist/src/signal/integration.test.d.ts.map +1 -0
  41. package/dist/src/signal/integration.test.js +102 -0
  42. package/dist/src/signal/integration.test.js.map +1 -0
  43. package/dist/src/signal/message-router.d.ts +7 -7
  44. package/dist/src/signal/message-router.d.ts.map +1 -1
  45. package/dist/src/signal/message-router.js +6 -1
  46. package/dist/src/signal/message-router.js.map +1 -1
  47. package/dist/src/signal/message-router.test.js +15 -19
  48. package/dist/src/signal/message-router.test.js.map +1 -1
  49. package/dist/src/signal/signal-client.d.ts +33 -18
  50. package/dist/src/signal/signal-client.d.ts.map +1 -1
  51. package/dist/src/signal/signal-client.js +102 -92
  52. package/dist/src/signal/signal-client.js.map +1 -1
  53. package/dist/src/signal/signal-client.test.js +60 -77
  54. package/dist/src/signal/signal-client.test.js.map +1 -1
  55. package/dist/src/signal/{websocket-signal-manager.d.ts → signal-manager-impl.d.ts} +13 -11
  56. package/dist/src/signal/signal-manager-impl.d.ts.map +1 -0
  57. package/dist/src/signal/signal-manager-impl.js +151 -0
  58. package/dist/src/signal/signal-manager-impl.js.map +1 -0
  59. package/dist/src/signal/signal-manager.d.ts +12 -11
  60. package/dist/src/signal/signal-manager.d.ts.map +1 -1
  61. package/dist/src/signal/signal-rpc-client.d.ts +19 -0
  62. package/dist/src/signal/signal-rpc-client.d.ts.map +1 -0
  63. package/dist/src/signal/signal-rpc-client.js +108 -0
  64. package/dist/src/signal/signal-rpc-client.js.map +1 -0
  65. package/dist/src/signal/signal-rpc-client.test.d.ts +2 -0
  66. package/dist/src/signal/signal-rpc-client.test.d.ts.map +1 -0
  67. package/dist/src/signal/signal-rpc-client.test.js +74 -0
  68. package/dist/src/signal/signal-rpc-client.test.js.map +1 -0
  69. package/dist/src/swarm/connection.d.ts +3 -3
  70. package/dist/src/swarm/connection.d.ts.map +1 -1
  71. package/dist/src/swarm/connection.js +1 -4
  72. package/dist/src/swarm/connection.js.map +1 -1
  73. package/dist/src/swarm/swarm.d.ts +6 -7
  74. package/dist/src/swarm/swarm.d.ts.map +1 -1
  75. package/dist/src/swarm/swarm.js +21 -17
  76. package/dist/src/swarm/swarm.js.map +1 -1
  77. package/dist/src/swarm/swarm.test.js +156 -117
  78. package/dist/src/swarm/swarm.test.js.map +1 -1
  79. package/dist/src/topology/fully-connected-topology.d.ts +0 -1
  80. package/dist/src/topology/fully-connected-topology.d.ts.map +1 -1
  81. package/dist/src/topology/fully-connected-topology.js +1 -6
  82. package/dist/src/topology/fully-connected-topology.js.map +1 -1
  83. package/dist/src/topology/mmst-topology.d.ts +0 -1
  84. package/dist/src/topology/mmst-topology.d.ts.map +1 -1
  85. package/dist/src/topology/mmst-topology.js +1 -6
  86. package/dist/src/topology/mmst-topology.js.map +1 -1
  87. package/dist/src/topology/star-topology.d.ts +0 -1
  88. package/dist/src/topology/star-topology.d.ts.map +1 -1
  89. package/dist/src/topology/star-topology.js +1 -6
  90. package/dist/src/topology/star-topology.js.map +1 -1
  91. package/dist/src/topology/topology.d.ts +0 -6
  92. package/dist/src/topology/topology.d.ts.map +1 -1
  93. package/dist/src/transport/in-memory-transport.d.ts +2 -2
  94. package/dist/src/transport/in-memory-transport.d.ts.map +1 -1
  95. package/dist/src/transport/in-memory-transport.js.map +1 -1
  96. package/dist/src/transport/transport.d.ts +3 -3
  97. package/dist/src/transport/transport.d.ts.map +1 -1
  98. package/dist/src/transport/webrtc-transport.d.ts +3 -3
  99. package/dist/src/transport/webrtc-transport.d.ts.map +1 -1
  100. package/dist/src/transport/webrtc-transport.js.map +1 -1
  101. package/dist/tests-setup.js +1 -1
  102. package/dist/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +14 -12
  104. package/src/network-manager.blueprint-test.ts +57 -22
  105. package/src/network-manager.browser-test.ts +1 -1
  106. package/src/network-manager.test.ts +8 -7
  107. package/src/network-manager.ts +8 -9
  108. package/src/proto/defs/dxos/mesh/signal.proto +53 -35
  109. package/src/proto/defs/dxos/mesh/signalMessage.proto +51 -0
  110. package/src/proto/gen/dxos/credentials.ts +1 -0
  111. package/src/proto/gen/dxos/halo/keys.ts +1 -0
  112. package/src/proto/gen/dxos/mesh/signal.ts +51 -45
  113. package/src/proto/gen/dxos/mesh/signalMessage.ts +83 -0
  114. package/src/proto/gen/google/protobuf.ts +7 -0
  115. package/src/proto/gen/index.ts +18 -6
  116. package/src/proto/substitutions.ts +3 -1
  117. package/src/signal/in-memory-signal-manager.ts +37 -12
  118. package/src/signal/index.ts +1 -2
  119. package/src/signal/integration.test.ts +117 -0
  120. package/src/signal/message-router.test.ts +36 -41
  121. package/src/signal/message-router.ts +22 -18
  122. package/src/signal/signal-client.test.ts +70 -92
  123. package/src/signal/signal-client.ts +119 -113
  124. package/src/signal/signal-manager-impl.ts +166 -0
  125. package/src/signal/signal-manager.ts +12 -12
  126. package/src/signal/signal-rpc-client.test.ts +86 -0
  127. package/src/signal/signal-rpc-client.ts +121 -0
  128. package/src/swarm/connection.ts +5 -8
  129. package/src/swarm/swarm.test.ts +208 -169
  130. package/src/swarm/swarm.ts +24 -20
  131. package/src/topology/fully-connected-topology.ts +1 -9
  132. package/src/topology/mmst-topology.ts +1 -9
  133. package/src/topology/star-topology.ts +1 -7
  134. package/src/topology/topology.ts +0 -7
  135. package/src/transport/in-memory-transport.ts +2 -2
  136. package/src/transport/transport.ts +3 -3
  137. package/src/transport/webrtc-transport.ts +3 -3
  138. package/dist/browser-mocha/bundle.js +0 -119346
  139. package/dist/browser-mocha/main.js +0 -27
  140. package/dist/src/signal/websocket-rpc.d.ts +0 -30
  141. package/dist/src/signal/websocket-rpc.d.ts.map +0 -1
  142. package/dist/src/signal/websocket-rpc.js +0 -203
  143. package/dist/src/signal/websocket-rpc.js.map +0 -1
  144. package/dist/src/signal/websocket-signal-manager.d.ts.map +0 -1
  145. package/dist/src/signal/websocket-signal-manager.js +0 -134
  146. package/dist/src/signal/websocket-signal-manager.js.map +0 -1
  147. package/src/signal/websocket-rpc.ts +0 -208
  148. 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 { failUndefined } from '@dxos/debug';
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 { Answer, Message } from '../proto/gen/dxos/mesh/signal';
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 { WebsocketRpc } from './websocket-rpc';
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 = SignalApi.State.CONNECTING;
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!: WebsocketRpc;
74
+ private _client!: SignalRPCClient;
46
75
 
47
- private _clientCleanup: (() => void)[] = [];
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 _onOffer: (message: Message) => Promise<Answer>,
60
- private readonly _onSignal: (message: Message) => Promise<void>
90
+ private readonly _onSignal: (message: SignalMessage) => Promise<void>
61
91
  ) {
62
- this._setState(SignalApi.State.CONNECTING);
92
+ this._setState(State.CONNECTING);
63
93
  this._createClient();
64
94
  }
65
95
 
66
- private _setState (newState: SignalApi.State) {
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 WebsocketRpc(this._host);
106
+ this._client = new SignalRPCClient(this._host);
77
107
  } catch (error: any) {
78
- if (this._state === SignalApi.State.RE_CONNECTING) {
108
+ if (this._state === State.RE_CONNECTING) {
79
109
  this._reconnectAfter *= 2;
80
110
  }
81
111
 
82
112
  this._lastError = error;
83
- this._setState(SignalApi.State.DISCONNECTED);
113
+ this._setState(State.DISCONNECTED);
84
114
  this._reconnect();
85
115
  }
86
116
 
87
- this._client.addHandler('offer', (message: any) => this._onOffer({
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(SignalApi.State.CONNECTED);
120
+ this._setState(State.CONNECTED);
112
121
  }));
113
122
 
114
- this._clientCleanup.push(this._client.error.on(error => {
123
+ this._cleanupSubscriptions.push(this._client.error.on(error => {
115
124
  log(`Socket error: ${error.message}`);
116
- if (this._state === SignalApi.State.CLOSED) {
125
+ if (this._state === State.CLOSED) {
117
126
  return;
118
127
  }
119
128
 
120
- if (this._state === SignalApi.State.RE_CONNECTING) {
129
+ if (this._state === State.RE_CONNECTING) {
121
130
  this._reconnectAfter *= 2;
122
131
  }
123
132
 
124
133
  this._lastError = error;
125
- this._setState(SignalApi.State.DISCONNECTED);
134
+ this._setState(State.DISCONNECTED);
126
135
 
127
136
  this._reconnect();
128
137
  }));
129
138
 
130
- this._clientCleanup.push(this._client.disconnected.on(() => {
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 !== SignalApi.State.CONNECTING && this._state !== SignalApi.State.RE_CONNECTING) {
142
+ if (this._state !== State.CONNECTING && this._state !== State.RE_CONNECTING) {
134
143
  return;
135
144
  }
136
145
 
137
- if (this._state === SignalApi.State.RE_CONNECTING) {
146
+ if (this._state === State.RE_CONNECTING) {
138
147
  this._reconnectAfter *= 2;
139
148
  }
140
149
 
141
- this._setState(SignalApi.State.DISCONNECTED);
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 === SignalApi.State.CLOSED) {
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._clientCleanup.forEach(cb => cb());
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(SignalApi.State.RE_CONNECTING);
173
+ this._setState(State.RE_CONNECTING);
167
174
  this._createClient();
168
175
  }, this._reconnectAfter);
169
176
  }
170
177
 
171
178
  async close () {
172
- this._clientCleanup.forEach(cb => cb());
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(SignalApi.State.CLOSED);
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<PublicKey[]> {
195
- const peers: Buffer[] = await this._client.call('join', {
196
- id: peerId.asBuffer(),
197
- topic: topic.asBuffer()
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
- await this._client.call('leave', {
204
- id: peerId.asBuffer(),
205
- topic: topic.asBuffer()
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 lookup (topic: PublicKey): Promise<PublicKey[]> {
210
- const peers: Buffer[] = await this._client.call('lookup', {
211
- topic: topic.asBuffer()
212
- });
213
- return peers.map(id => PublicKey.from(id));
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
- * Routes an offer to the other peer's _onOffer callback.
218
- * @returns Other peer's _onOffer callback return value.
219
- * @deprecated
220
- */
221
- async offer (msg: Message): Promise<Answer> {
222
- return this._client.call('offer', {
223
- id: msg.id?.asBuffer(),
224
- remoteId: msg.remoteId?.asBuffer(),
225
- topic: msg.topic?.asBuffer(),
226
- sessionId: msg.sessionId?.asBuffer(),
227
- data: msg.data
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
- * Routes an offer to the other peer's _onSignal callback.
233
- */
234
- async signal (message: Message): Promise<void> {
235
- const signalMessage: SignalMessage = {
236
- messageId: message.messageId?.asBuffer(),
237
- id: message.id?.asBuffer(),
238
- remoteId: message.remoteId?.asBuffer(),
239
- topic: message.topic?.asBuffer(),
240
- sessionId: message.sessionId?.asBuffer(),
241
- data: Buffer.from(schema.getCodecForType('dxos.mesh.signal.MessageData').encode(message.data ?? failUndefined()))
242
- };
243
- return this._client.emit('signal', signalMessage);
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 { Answer, Message } from '../proto/gen/dxos/mesh/signal';
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: Message): Promise<Answer>
34
+ offer (msg: SignalMessage): Promise<Answer>
39
35
 
40
36
  /**
41
- * Send message to peer.
37
+ * Reliably send a signal to a peer.
42
38
  */
43
- signal (msg: Message): Promise<void>
39
+ signal (msg: SignalMessage): Promise<void>
44
40
  }
45
41
 
46
- export interface SignalManager extends SignalConnection, SignalMessaging {
42
+ export interface SignalManager extends SignalConnection {
47
43
  statusChanged: Event<SignalApi.Status[]>
48
44
  commandTrace: Event<SignalApi.CommandTrace>
49
- peerCandidatesChanged: Event<[topic: PublicKey, candidates: PublicKey[]]>
50
- onSignal: Event<Message>
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
+ });