@dxos/messaging 0.5.8 → 0.5.9-main.0a0e87d

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 (44) hide show
  1. package/dist/lib/browser/index.mjs +812 -559
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +778 -545
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/messenger-monitor.d.ts +8 -0
  8. package/dist/types/src/messenger-monitor.d.ts.map +1 -0
  9. package/dist/types/src/messenger.d.ts +1 -0
  10. package/dist/types/src/messenger.d.ts.map +1 -1
  11. package/dist/types/src/signal-client/signal-client-monitor.d.ts +30 -0
  12. package/dist/types/src/signal-client/signal-client-monitor.d.ts.map +1 -0
  13. package/dist/types/src/signal-client/signal-client.d.ts +25 -50
  14. package/dist/types/src/signal-client/signal-client.d.ts.map +1 -1
  15. package/dist/types/src/signal-client/signal-local-state.d.ts +46 -0
  16. package/dist/types/src/signal-client/signal-local-state.d.ts.map +1 -0
  17. package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts +6 -0
  18. package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts.map +1 -0
  19. package/dist/types/src/signal-client/signal-rpc-client.d.ts +4 -2
  20. package/dist/types/src/signal-client/signal-rpc-client.d.ts.map +1 -1
  21. package/dist/types/src/signal-manager/memory-signal-manager.d.ts +0 -2
  22. package/dist/types/src/signal-manager/memory-signal-manager.d.ts.map +1 -1
  23. package/dist/types/src/signal-manager/signal-manager.d.ts +0 -2
  24. package/dist/types/src/signal-manager/signal-manager.d.ts.map +1 -1
  25. package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts +8 -0
  26. package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts.map +1 -0
  27. package/dist/types/src/signal-manager/websocket-signal-manager.d.ts +7 -3
  28. package/dist/types/src/signal-manager/websocket-signal-manager.d.ts.map +1 -1
  29. package/dist/types/src/signal-methods.d.ts +6 -4
  30. package/dist/types/src/signal-methods.d.ts.map +1 -1
  31. package/package.json +13 -12
  32. package/src/messenger-monitor.ts +20 -0
  33. package/src/messenger.ts +16 -5
  34. package/src/signal-client/signal-client-monitor.ts +111 -0
  35. package/src/signal-client/signal-client.test.ts +111 -259
  36. package/src/signal-client/signal-client.ts +141 -252
  37. package/src/signal-client/signal-local-state.ts +156 -0
  38. package/src/signal-client/signal-rpc-client-monitor.ts +15 -0
  39. package/src/signal-client/signal-rpc-client.ts +38 -21
  40. package/src/signal-manager/memory-signal-manager.ts +0 -2
  41. package/src/signal-manager/signal-manager.ts +0 -3
  42. package/src/signal-manager/websocket-signal-manager-monitor.ts +20 -0
  43. package/src/signal-manager/websocket-signal-manager.ts +48 -26
  44. package/src/signal-methods.ts +7 -4
@@ -2,16 +2,17 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import { DeferredTask, Event, Trigger, asyncTimeout, scheduleTask, scheduleTaskInterval, sleep } from '@dxos/async';
6
- import { type Any, type Stream } from '@dxos/codec-protobuf';
7
- import { Context, cancelWithContext } from '@dxos/context';
5
+ import { DeferredTask, Event, Trigger, scheduleTask, scheduleTaskInterval, sleep } from '@dxos/async';
6
+ import { type Any } from '@dxos/codec-protobuf';
7
+ import { type Context, cancelWithContext, Resource } from '@dxos/context';
8
8
  import { invariant } from '@dxos/invariant';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { log } from '@dxos/log';
11
11
  import { trace } from '@dxos/protocols';
12
- import { type Message as SignalMessage, SignalState, type SwarmEvent } from '@dxos/protocols/proto/dxos/mesh/signal';
13
- import { ComplexMap, ComplexSet } from '@dxos/util';
12
+ import { SignalState, type SwarmEvent } from '@dxos/protocols/proto/dxos/mesh/signal';
14
13
 
14
+ import { SignalClientMonitor } from './signal-client-monitor';
15
+ import { SignalLocalState } from './signal-local-state';
15
16
  import { SignalRPCClient } from './signal-rpc-client';
16
17
  import { type Message, type SignalClientMethods, type SignalStatus } from '../signal-methods';
17
18
 
@@ -20,133 +21,84 @@ const MAX_RECONNECT_TIMEOUT = 5_000;
20
21
  const ERROR_RECONCILE_DELAY = 1_000;
21
22
  const RECONCILE_INTERVAL = 5_000;
22
23
 
23
- export type CommandTrace = {
24
- messageId: string;
25
- host: string;
26
- incoming: boolean;
27
- time: number;
28
- method: string;
29
- payload: any;
30
- response?: any;
31
- error?: string;
32
- };
33
-
34
24
  /**
35
25
  * KUBE-specific signaling client.
36
26
  * Establishes a websocket connection to signal server and provides RPC methods.
27
+ * Subscription state updates are executed immediately against the local state which
28
+ * is reconciled periodically.
37
29
  */
38
30
  // TODO(burdon): Rename impl.
39
- export class SignalClient implements SignalClientMethods {
40
- private _state = SignalState.CLOSED;
31
+ export class SignalClient extends Resource implements SignalClientMethods {
32
+ private readonly _monitor = new SignalClientMonitor();
41
33
 
34
+ private _state = SignalState.CLOSED;
42
35
  private _lastError?: Error;
36
+ private _lastReconciliationFailed = false;
43
37
 
44
- /**
45
- * Number of milliseconds after which the connection will be attempted again in case of error.
46
- */
47
- private _reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
48
-
49
- /**
50
- * Timestamp of when the connection attempt was began.
51
- */
52
- private _connectionStarted = new Date();
53
-
54
- /**
55
- * Timestamp of last state change.
56
- */
57
- private _lastStateChange = new Date();
58
-
59
- private _client?: SignalRPCClient;
60
38
  private readonly _clientReady = new Trigger();
61
-
62
- private _ctx?: Context;
63
-
64
39
  private _connectionCtx?: Context;
40
+ private _client?: SignalRPCClient;
65
41
 
66
42
  private _reconcileTask?: DeferredTask;
67
43
  private _reconnectTask?: DeferredTask;
68
44
 
69
- readonly statusChanged = new Event<SignalStatus>();
70
- readonly commandTrace = new Event<CommandTrace>();
71
-
72
45
  /**
73
- * Swarm events streams. Keys represent actually joined topic and peerId.
74
- */
75
- private readonly _swarmStreams = new ComplexMap<{ topic: PublicKey; peerId: PublicKey }, Stream<SwarmEvent>>(
76
- ({ topic, peerId }) => topic.toHex() + peerId.toHex(),
77
- );
78
-
79
- /**
80
- * Represent desired joined topic and peerId.
81
- */
82
- private readonly _joinedTopics = new ComplexSet<{ topic: PublicKey; peerId: PublicKey }>(
83
- ({ topic, peerId }) => topic.toHex() + peerId.toHex(),
84
- );
85
-
86
- /**
87
- * Message streams. Keys represents actually subscribed peers.
88
- * @internal
46
+ * Number of milliseconds after which the connection will be attempted again in case of error.
89
47
  */
90
- public readonly _messageStreams = new ComplexMap<PublicKey, Stream<SignalMessage>>((key) => key.toHex());
48
+ private _reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
91
49
 
92
- /**
93
- * Represent desired message subscriptions.
94
- */
95
- private readonly _subscribedMessages = new ComplexSet<{ peerId: PublicKey }>(({ peerId }) => peerId.toHex());
50
+ private readonly _instanceId = PublicKey.random().toHex();
96
51
 
97
52
  /**
98
- * Event to use in tests to wait till subscription is successfully established.
99
53
  * @internal
100
54
  */
101
- public _reconciled = new Event();
102
-
103
- private readonly _instanceId = PublicKey.random().toHex();
55
+ readonly localState: SignalLocalState;
104
56
 
105
- private readonly _performance = {
106
- sentMessages: 0,
107
- receivedMessages: 0,
108
- reconnectCounter: 0,
109
- joinCounter: 0,
110
- leaveCounter: 0,
111
- };
57
+ readonly statusChanged = new Event<SignalStatus>();
112
58
 
113
59
  /**
114
60
  * @param _host Signal server websocket URL.
61
+ * @param onMessage called when a new message is received.
62
+ * @param onSwarmEvent called when a new swarm event is received.
63
+ * @param _getMetadata signal-message metadata provider, called for every message.
115
64
  */
116
65
  constructor(
117
66
  private readonly _host: string,
118
- private readonly _onMessage: (params: { author: PublicKey; recipient: PublicKey; payload: Any }) => Promise<void>,
119
- private readonly _onSwarmEvent: (params: { topic: PublicKey; swarmEvent: SwarmEvent }) => Promise<void>,
67
+ onMessage: (params: { author: PublicKey; recipient: PublicKey; payload: Any }) => Promise<void>,
68
+ onSwarmEvent: (params: { topic: PublicKey; swarmEvent: SwarmEvent }) => Promise<void>,
120
69
  private readonly _getMetadata?: () => any,
121
70
  ) {
71
+ super();
122
72
  if (!this._host.startsWith('wss://') && !this._host.startsWith('ws://')) {
123
73
  throw new Error(`Signal server requires a websocket URL. Provided: ${this._host}`);
124
74
  }
75
+
76
+ this.localState = new SignalLocalState((message) => {
77
+ this._monitor.recordMessageReceived(message);
78
+ return onMessage(message);
79
+ }, onSwarmEvent);
125
80
  }
126
81
 
127
- async open() {
82
+ protected override async _open() {
128
83
  log.trace('dxos.mesh.signal-client.open', trace.begin({ id: this._instanceId }));
129
84
 
130
85
  if ([SignalState.CONNECTED, SignalState.CONNECTING].includes(this._state)) {
131
86
  return;
132
87
  }
133
-
134
- this._ctx = new Context({
135
- onError: (err) => {
136
- if (this._state === SignalState.CLOSED || this._ctx?.disposed) {
137
- return;
138
- }
139
- if (this._state === SignalState.CONNECTED) {
140
- log.warn('SignalClient error:', err);
141
- }
142
- this._scheduleReconcileAfterError();
143
- },
144
- });
88
+ this._setState(SignalState.CONNECTING);
145
89
 
146
90
  this._reconcileTask = new DeferredTask(this._ctx, async () => {
147
- await this._reconcileSwarmSubscriptions();
148
- await this._reconcileMessageSubscriptions();
149
- this._reconciled.emit();
91
+ try {
92
+ await cancelWithContext(this._connectionCtx!, this._clientReady.wait({ timeout: 5_000 }));
93
+ invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
94
+ await this.localState.reconcile(this._connectionCtx!, this._client!);
95
+ this._monitor.recordReconciliation({ success: true });
96
+ this._lastReconciliationFailed = false;
97
+ } catch (err) {
98
+ this._lastReconciliationFailed = true;
99
+ this._monitor.recordReconciliation({ success: false });
100
+ throw err;
101
+ }
150
102
  });
151
103
 
152
104
  // Reconcile subscriptions periodically.
@@ -161,26 +113,39 @@ export class SignalClient implements SignalClientMethods {
161
113
  );
162
114
 
163
115
  this._reconnectTask = new DeferredTask(this._ctx, async () => {
164
- await this._reconnect();
116
+ try {
117
+ await this._reconnect();
118
+ this._monitor.recordReconnect({ success: true });
119
+ } catch (err) {
120
+ this._monitor.recordReconnect({ success: false });
121
+ throw err;
122
+ }
165
123
  });
166
124
 
167
- this._setState(SignalState.CONNECTING);
168
125
  this._createClient();
169
126
  log.trace('dxos.mesh.signal-client.open', trace.end({ id: this._instanceId }));
170
127
  }
171
128
 
172
- async close() {
129
+ protected override async _catch(err: Error) {
130
+ if (this._state === SignalState.CLOSED || this._ctx.disposed) {
131
+ return;
132
+ }
133
+ // Don't log consecutive reconciliation failures.
134
+ if (this._state === SignalState.CONNECTED && !this._lastReconciliationFailed) {
135
+ log.warn('SignalClient error:', err);
136
+ }
137
+ this._scheduleReconcileAfterError();
138
+ }
139
+
140
+ protected override async _close() {
173
141
  log('closing...');
174
142
  if ([SignalState.CLOSED].includes(this._state)) {
175
143
  return;
176
144
  }
177
145
 
178
- await this._ctx?.dispose();
179
-
180
- this._clientReady.reset();
181
- await this._client?.close();
182
- this._client = undefined;
183
146
  this._setState(SignalState.CLOSED);
147
+ await this._safeResetClient();
148
+
184
149
  log('closed');
185
150
  }
186
151
 
@@ -190,239 +155,163 @@ export class SignalClient implements SignalClientMethods {
190
155
  state: this._state,
191
156
  error: this._lastError?.message,
192
157
  reconnectIn: this._reconnectAfter,
193
- connectionStarted: this._connectionStarted,
194
- lastStateChange: this._lastStateChange,
158
+ ...this._monitor.getRecordedTimestamps(),
195
159
  };
196
160
  }
197
161
 
198
- async join({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }): Promise<void> {
199
- log('joining', { topic, peerId });
200
- this._performance.joinCounter++;
201
- this._joinedTopics.add({ topic, peerId });
202
- this._reconcileTask!.schedule();
162
+ async join(args: { topic: PublicKey; peerId: PublicKey }): Promise<void> {
163
+ log('joining', { topic: args.topic, peerId: args.peerId });
164
+ this._monitor.recordJoin();
165
+ this.localState.join(args);
166
+ this._reconcileTask?.schedule();
203
167
  }
204
168
 
205
- async leave({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }): Promise<void> {
206
- this._performance.leaveCounter++;
207
- log('leaving', { topic, peerId });
208
- void this._swarmStreams.get({ topic, peerId })?.close();
209
- this._swarmStreams.delete({ topic, peerId });
210
- this._joinedTopics.delete({ topic, peerId });
169
+ async leave(args: { topic: PublicKey; peerId: PublicKey }): Promise<void> {
170
+ log('leaving', { topic: args.topic, peerId: args.peerId });
171
+ this._monitor.recordLeave();
172
+ this.localState.leave(args);
211
173
  }
212
174
 
213
175
  async sendMessage(msg: Message): Promise<void> {
214
- this._performance.sentMessages++;
215
- await this._clientReady.wait();
216
- invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
217
- await this._client!.sendMessage(msg);
176
+ return this._monitor.recordMessageSending(msg, async () => {
177
+ await this._clientReady.wait();
178
+ invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
179
+ await this._client!.sendMessage(msg);
180
+ });
218
181
  }
219
182
 
220
183
  async subscribeMessages(peerId: PublicKey) {
221
184
  log('subscribing to messages', { peerId });
222
- this._subscribedMessages.add({ peerId });
223
- this._reconcileTask!.schedule();
185
+ this.localState.subscribeMessages(peerId);
186
+ this._reconcileTask?.schedule();
224
187
  }
225
188
 
226
189
  async unsubscribeMessages(peerId: PublicKey) {
227
190
  log('unsubscribing from messages', { peerId });
228
- this._subscribedMessages.delete({ peerId });
229
- void this._messageStreams.get(peerId)?.close();
230
- this._messageStreams.delete(peerId);
191
+ this.localState.unsubscribeMessages(peerId);
231
192
  }
232
193
 
233
194
  private _scheduleReconcileAfterError() {
234
- scheduleTask(
235
- this._ctx!,
236
- () => {
237
- this._reconcileTask!.schedule();
238
- },
239
- ERROR_RECONCILE_DELAY,
240
- );
241
- }
242
-
243
- private _setState(newState: SignalState) {
244
- this._state = newState;
245
- this._lastStateChange = new Date();
246
- log('signal state changed', { status: this.getStatus() });
247
- this.statusChanged.emit(this.getStatus());
195
+ scheduleTask(this._ctx, () => this._reconcileTask!.schedule(), ERROR_RECONCILE_DELAY);
248
196
  }
249
197
 
250
198
  private _createClient() {
251
199
  log('creating client', { host: this._host, state: this._state });
252
200
  invariant(!this._client, 'Client already created');
253
201
 
254
- this._connectionStarted = new Date();
202
+ this._monitor.recordConnectionStartTime();
255
203
 
256
204
  // Create new context for each connection.
257
- this._connectionCtx = this._ctx!.derive();
205
+ this._connectionCtx = this._ctx.derive();
258
206
  this._connectionCtx.onDispose(async () => {
259
207
  log('connection context disposed');
260
- await Promise.all(Array.from(this._swarmStreams.values()).map((stream) => stream.close()));
261
- await Promise.all(Array.from(this._messageStreams.values()).map((stream) => stream.close()));
262
- this._swarmStreams.clear();
263
- this._messageStreams.clear();
208
+ const { failureCount } = await this.localState.safeCloseStreams();
209
+ this._monitor.recordStreamCloseErrors(failureCount);
264
210
  });
265
211
 
266
212
  try {
267
- this._client = new SignalRPCClient({
213
+ const client = new SignalRPCClient({
268
214
  url: this._host,
269
215
  callbacks: {
270
216
  onConnected: () => {
271
- log('socket connected');
272
- this._lastError = undefined;
273
- this._reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
274
- this._setState(SignalState.CONNECTED);
275
- this._clientReady.wake();
276
- this._reconcileTask!.schedule();
217
+ if (client === this._client) {
218
+ log('socket connected');
219
+ this._onConnected();
220
+ }
277
221
  },
278
222
 
279
223
  onDisconnected: () => {
224
+ if (client !== this._client) {
225
+ return;
226
+ }
280
227
  log('socket disconnected', { state: this._state });
281
228
  if (this._state === SignalState.ERROR) {
282
229
  // Ignore disconnects after error.
283
230
  // Handled by error handler before disconnect handler.
284
231
  this._setState(SignalState.DISCONNECTED);
285
- return;
232
+ } else {
233
+ this._onDisconnected();
286
234
  }
287
- if (this._state !== SignalState.CONNECTED && this._state !== SignalState.CONNECTING) {
288
- this._incrementReconnectTimeout();
289
- }
290
- this._setState(SignalState.DISCONNECTED);
291
- this._reconnectTask!.schedule();
292
235
  },
293
236
 
294
237
  onError: (error) => {
295
- log('socket error', { error, state: this._state });
296
- this._lastError = error;
297
- if (this._state !== SignalState.CONNECTED && this._state !== SignalState.CONNECTING) {
298
- this._incrementReconnectTimeout();
238
+ if (client === this._client) {
239
+ log('socket error', { error, state: this._state });
240
+ this._onDisconnected({ error });
299
241
  }
300
- this._setState(SignalState.ERROR);
301
-
302
- this._reconnectTask!.schedule();
303
242
  },
304
243
  getMetadata: this._getMetadata,
305
244
  },
306
245
  });
307
- } catch (err: any) {
308
- if (this._state !== SignalState.CONNECTED && this._state !== SignalState.CONNECTING) {
309
- this._incrementReconnectTimeout();
310
- }
311
- this._lastError = err;
312
- this._setState(SignalState.DISCONNECTED);
313
- this._reconnectTask!.schedule();
246
+ this._client = client;
247
+ } catch (error: any) {
248
+ this._client = undefined;
249
+ this._onDisconnected({ error });
314
250
  }
315
251
  }
316
252
 
317
- private _incrementReconnectTimeout() {
318
- this._reconnectAfter *= 2;
319
- this._reconnectAfter = Math.min(this._reconnectAfter, MAX_RECONNECT_TIMEOUT);
320
- }
321
-
322
253
  private async _reconnect() {
323
254
  log(`reconnecting in ${this._reconnectAfter}ms`, { state: this._state });
324
- this._performance.reconnectCounter++;
325
255
 
326
256
  if (this._state === SignalState.RECONNECTING) {
327
- log.warn('Signal api already reconnecting.');
257
+ log.info('Signal api already reconnecting.');
328
258
  return;
329
259
  }
330
-
331
260
  if (this._state === SignalState.CLOSED) {
332
261
  return;
333
262
  }
263
+ this._setState(SignalState.RECONNECTING);
334
264
 
335
- // Close client if it wasn't already closed.
336
- this._clientReady.reset();
337
- await this._connectionCtx?.dispose();
338
- this._client?.close().catch(() => {});
339
- this._client = undefined;
265
+ await this._safeResetClient();
340
266
 
341
267
  await cancelWithContext(this._ctx!, sleep(this._reconnectAfter));
342
268
 
343
- this._setState(SignalState.RECONNECTING);
344
-
345
269
  this._createClient();
346
270
  }
347
271
 
348
- private async _reconcileSwarmSubscriptions(): Promise<void> {
349
- await asyncTimeout(cancelWithContext(this._connectionCtx!, this._clientReady.wait()), 5_000);
350
- // Copy Client reference to avoid client change during the reconcile.
351
- const client = this._client!;
352
- invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
353
-
354
- // Unsubscribe from topics that are no longer needed.
355
- for (const { topic, peerId } of this._swarmStreams.keys()) {
356
- // Join desired topics.
357
- if (this._joinedTopics.has({ topic, peerId })) {
358
- continue;
359
- }
272
+ private _onConnected() {
273
+ this._lastError = undefined;
274
+ this._lastReconciliationFailed = false;
275
+ this._reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
276
+ this._setState(SignalState.CONNECTED);
277
+ this._clientReady.wake();
278
+ this._reconcileTask!.schedule();
279
+ }
360
280
 
361
- void this._swarmStreams.get({ topic, peerId })?.close();
362
- this._swarmStreams.delete({ topic, peerId });
281
+ private _onDisconnected(options?: { error: Error }) {
282
+ this._updateReconnectTimeout();
283
+ if (this._state === SignalState.CLOSED) {
284
+ return;
363
285
  }
364
-
365
- // Subscribe to topics that are needed.
366
- for (const { topic, peerId } of this._joinedTopics.values()) {
367
- // Join desired topics.
368
- if (this._swarmStreams.has({ topic, peerId })) {
369
- continue;
370
- }
371
-
372
- const swarmStream = await asyncTimeout(
373
- cancelWithContext(this._connectionCtx!, client.join({ topic, peerId })),
374
- 5_000,
375
- );
376
- // Subscribing to swarm events.
377
- // TODO(mykola): What happens when the swarm stream is closed? Maybe send leave event for each peer?
378
- swarmStream.subscribe(async (swarmEvent: SwarmEvent) => {
379
- log('swarm event', { swarmEvent });
380
- await this._onSwarmEvent({ topic, swarmEvent });
381
- });
382
-
383
- // Saving swarm stream.
384
- this._swarmStreams.set({ topic, peerId }, swarmStream);
286
+ if (options?.error) {
287
+ this._lastError = options.error;
288
+ this._setState(SignalState.ERROR);
289
+ } else {
290
+ this._setState(SignalState.DISCONNECTED);
385
291
  }
292
+ this._reconnectTask!.schedule();
386
293
  }
387
294
 
388
- private async _reconcileMessageSubscriptions(): Promise<void> {
389
- await asyncTimeout(cancelWithContext(this._connectionCtx!, this._clientReady.wait()), 5_000);
390
- // Copy Client reference to avoid client change during the reconcile.
391
- const client = this._client!;
392
- invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
393
-
394
- // Unsubscribe from messages that are no longer needed.
395
- for (const peerId of this._messageStreams.keys()) {
396
- // Join desired topics.
397
- if (this._subscribedMessages.has({ peerId })) {
398
- continue;
399
- }
295
+ private _setState(newState: SignalState) {
296
+ this._state = newState;
297
+ this._monitor.recordStateChangeTime();
298
+ log('signal state changed', { status: this.getStatus() });
299
+ this.statusChanged.emit(this.getStatus());
300
+ }
400
301
 
401
- void this._messageStreams.get(peerId)?.close();
402
- this._messageStreams.delete(peerId);
302
+ private _updateReconnectTimeout() {
303
+ if (this._state !== SignalState.CONNECTED && this._state !== SignalState.CONNECTING) {
304
+ this._reconnectAfter *= 2;
305
+ this._reconnectAfter = Math.min(this._reconnectAfter, MAX_RECONNECT_TIMEOUT);
403
306
  }
307
+ }
404
308
 
405
- // Subscribe to messages that are needed.
406
- for (const { peerId } of this._subscribedMessages.values()) {
407
- if (this._messageStreams.has(peerId)) {
408
- continue;
409
- }
410
-
411
- const messageStream = await asyncTimeout(
412
- cancelWithContext(this._connectionCtx!, client.receiveMessages(peerId)),
413
- 5_000,
414
- );
415
- messageStream.subscribe(async (message: SignalMessage) => {
416
- this._performance.receivedMessages++;
417
- await this._onMessage({
418
- author: PublicKey.from(message.author),
419
- recipient: PublicKey.from(message.recipient),
420
- payload: message.payload,
421
- });
422
- });
309
+ private async _safeResetClient() {
310
+ await this._connectionCtx?.dispose();
311
+ this._connectionCtx = undefined;
423
312
 
424
- // Saving message stream.
425
- this._messageStreams.set(peerId, messageStream);
426
- }
313
+ this._clientReady.reset();
314
+ await this._client?.close().catch(() => {});
315
+ this._client = undefined;
427
316
  }
428
317
  }