@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.
- package/dist/lib/browser/index.mjs +812 -559
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +778 -545
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/types/src/messenger-monitor.d.ts +8 -0
- package/dist/types/src/messenger-monitor.d.ts.map +1 -0
- package/dist/types/src/messenger.d.ts +1 -0
- package/dist/types/src/messenger.d.ts.map +1 -1
- package/dist/types/src/signal-client/signal-client-monitor.d.ts +30 -0
- package/dist/types/src/signal-client/signal-client-monitor.d.ts.map +1 -0
- package/dist/types/src/signal-client/signal-client.d.ts +25 -50
- package/dist/types/src/signal-client/signal-client.d.ts.map +1 -1
- package/dist/types/src/signal-client/signal-local-state.d.ts +46 -0
- package/dist/types/src/signal-client/signal-local-state.d.ts.map +1 -0
- package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts +6 -0
- package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts.map +1 -0
- package/dist/types/src/signal-client/signal-rpc-client.d.ts +4 -2
- package/dist/types/src/signal-client/signal-rpc-client.d.ts.map +1 -1
- package/dist/types/src/signal-manager/memory-signal-manager.d.ts +0 -2
- package/dist/types/src/signal-manager/memory-signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-manager/signal-manager.d.ts +0 -2
- package/dist/types/src/signal-manager/signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts +8 -0
- package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts.map +1 -0
- package/dist/types/src/signal-manager/websocket-signal-manager.d.ts +7 -3
- package/dist/types/src/signal-manager/websocket-signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-methods.d.ts +6 -4
- package/dist/types/src/signal-methods.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/messenger-monitor.ts +20 -0
- package/src/messenger.ts +16 -5
- package/src/signal-client/signal-client-monitor.ts +111 -0
- package/src/signal-client/signal-client.test.ts +111 -259
- package/src/signal-client/signal-client.ts +141 -252
- package/src/signal-client/signal-local-state.ts +156 -0
- package/src/signal-client/signal-rpc-client-monitor.ts +15 -0
- package/src/signal-client/signal-rpc-client.ts +38 -21
- package/src/signal-manager/memory-signal-manager.ts +0 -2
- package/src/signal-manager/signal-manager.ts +0 -3
- package/src/signal-manager/websocket-signal-manager-monitor.ts +20 -0
- package/src/signal-manager/websocket-signal-manager.ts +48 -26
- 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,
|
|
6
|
-
import { type Any
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
private readonly _instanceId = PublicKey.random().toHex();
|
|
55
|
+
readonly localState: SignalLocalState;
|
|
104
56
|
|
|
105
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
194
|
-
lastStateChange: this._lastStateChange,
|
|
158
|
+
...this._monitor.getRecordedTimestamps(),
|
|
195
159
|
};
|
|
196
160
|
}
|
|
197
161
|
|
|
198
|
-
async join(
|
|
199
|
-
log('joining', { topic, peerId });
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
202
|
-
this._reconcileTask
|
|
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(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
223
|
-
this._reconcileTask
|
|
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.
|
|
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.
|
|
202
|
+
this._monitor.recordConnectionStartTime();
|
|
255
203
|
|
|
256
204
|
// Create new context for each connection.
|
|
257
|
-
this._connectionCtx = this._ctx
|
|
205
|
+
this._connectionCtx = this._ctx.derive();
|
|
258
206
|
this._connectionCtx.onDispose(async () => {
|
|
259
207
|
log('connection context disposed');
|
|
260
|
-
await
|
|
261
|
-
|
|
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
|
-
|
|
213
|
+
const client = new SignalRPCClient({
|
|
268
214
|
url: this._host,
|
|
269
215
|
callbacks: {
|
|
270
216
|
onConnected: () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
362
|
-
|
|
281
|
+
private _onDisconnected(options?: { error: Error }) {
|
|
282
|
+
this._updateReconnectTimeout();
|
|
283
|
+
if (this._state === SignalState.CLOSED) {
|
|
284
|
+
return;
|
|
363
285
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
313
|
+
this._clientReady.reset();
|
|
314
|
+
await this._client?.close().catch(() => {});
|
|
315
|
+
this._client = undefined;
|
|
427
316
|
}
|
|
428
317
|
}
|