@dxos/messaging 0.10.0 → 0.11.0
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/chunk-memory-signal-manager.mjs +652 -0
- package/dist/lib/chunk-memory-signal-manager.mjs.map +1 -0
- package/dist/lib/index.mjs +377 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +288 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/messenger.blueprint-test.d.ts.map +1 -1
- package/dist/types/src/messenger.d.ts +1 -1
- package/dist/types/src/messenger.d.ts.map +1 -1
- package/dist/types/src/signal-manager/edge-signal-manager.d.ts +21 -4
- package/dist/types/src/signal-manager/edge-signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-manager/edge-signal-manager.test.d.ts +2 -0
- package/dist/types/src/signal-manager/edge-signal-manager.test.d.ts.map +1 -0
- package/dist/types/src/signal-manager/index.d.ts +1 -2
- package/dist/types/src/signal-manager/index.d.ts.map +1 -1
- package/dist/types/src/signal-manager/memory-signal-manager.d.ts +12 -10
- package/dist/types/src/signal-manager/memory-signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-manager/memory-signal-manager.test.d.ts +2 -0
- package/dist/types/src/signal-manager/memory-signal-manager.test.d.ts.map +1 -0
- package/dist/types/src/signal-manager/signal-manager.d.ts +5 -0
- package/dist/types/src/signal-manager/signal-manager.d.ts.map +1 -1
- package/dist/types/src/signal-manager/utils.d.ts +6 -4
- package/dist/types/src/signal-manager/utils.d.ts.map +1 -1
- package/dist/types/src/signal-methods.d.ts +33 -22
- package/dist/types/src/signal-methods.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-edge-mesh.d.ts +61 -0
- package/dist/types/src/testing/test-edge-mesh.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -19
- package/src/index.ts +0 -1
- package/src/messenger.blueprint-test.ts +0 -25
- package/src/messenger.node.test.ts +1 -1
- package/src/messenger.ts +42 -18
- package/src/signal-manager/edge-signal-manager.test.ts +193 -0
- package/src/signal-manager/edge-signal-manager.ts +145 -12
- package/src/signal-manager/index.ts +1 -2
- package/src/signal-manager/memory-signal-manager.test.ts +199 -0
- package/src/signal-manager/memory-signal-manager.ts +87 -47
- package/src/signal-manager/signal-manager.ts +7 -0
- package/src/signal-manager/utils.ts +30 -21
- package/src/signal-methods.ts +37 -26
- package/src/testing/index.ts +1 -0
- package/src/testing/test-edge-mesh.ts +196 -0
- package/src/testing/utils.ts +2 -2
- package/dist/lib/neutral/chunk-EC4H56J5.mjs +0 -497
- package/dist/lib/neutral/chunk-EC4H56J5.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -1249
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/testing/index.mjs +0 -155
- package/dist/lib/neutral/testing/index.mjs.map +0 -7
- package/dist/types/src/signal-client/index.d.ts +0 -2
- package/dist/types/src/signal-client/index.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-client-monitor.d.ts +0 -30
- package/dist/types/src/signal-client/signal-client-monitor.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-client.d.ts +0 -59
- package/dist/types/src/signal-client/signal-client.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-client.node.test.d.ts +0 -2
- package/dist/types/src/signal-client/signal-client.node.test.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-local-state.d.ts +0 -38
- package/dist/types/src/signal-client/signal-local-state.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts +0 -6
- package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-rpc-client.d.ts +0 -47
- package/dist/types/src/signal-client/signal-rpc-client.d.ts.map +0 -1
- package/dist/types/src/signal-client/signal-rpc-client.node.test.d.ts +0 -2
- package/dist/types/src/signal-client/signal-rpc-client.node.test.d.ts.map +0 -1
- package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts +0 -8
- package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts.map +0 -1
- package/dist/types/src/signal-manager/websocket-signal-manager.d.ts +0 -41
- package/dist/types/src/signal-manager/websocket-signal-manager.d.ts.map +0 -1
- package/dist/types/src/signal-manager/websocket-signal-manager.node.test.d.ts +0 -2
- package/dist/types/src/signal-manager/websocket-signal-manager.node.test.d.ts.map +0 -1
- package/src/signal-client/index.ts +0 -5
- package/src/signal-client/signal-client-monitor.ts +0 -111
- package/src/signal-client/signal-client.node.test.ts +0 -185
- package/src/signal-client/signal-client.ts +0 -339
- package/src/signal-client/signal-local-state.ts +0 -174
- package/src/signal-client/signal-rpc-client-monitor.ts +0 -15
- package/src/signal-client/signal-rpc-client.node.test.ts +0 -102
- package/src/signal-client/signal-rpc-client.ts +0 -227
- package/src/signal-manager/websocket-signal-manager-monitor.ts +0 -20
- package/src/signal-manager/websocket-signal-manager.node.test.ts +0 -112
- package/src/signal-manager/websocket-signal-manager.ts +0 -188
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2020 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { DeferredTask, Event, Trigger, scheduleTask, scheduleTaskInterval, sleep } from '@dxos/async';
|
|
6
|
-
import { type Context, Resource, cancelWithContext } from '@dxos/context';
|
|
7
|
-
import { invariant } from '@dxos/invariant';
|
|
8
|
-
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import { log } from '@dxos/log';
|
|
10
|
-
import { type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
11
|
-
import { type JoinRequest, type LeaveRequest, type QueryRequest } from '@dxos/protocols/proto/dxos/edge/signal';
|
|
12
|
-
import { SignalState } from '@dxos/protocols/proto/dxos/mesh/signal';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
type Message,
|
|
16
|
-
type PeerInfo,
|
|
17
|
-
type SignalClientMethods,
|
|
18
|
-
type SignalStatus,
|
|
19
|
-
type SwarmEvent,
|
|
20
|
-
} from '../signal-methods';
|
|
21
|
-
import { SignalClientMonitor } from './signal-client-monitor';
|
|
22
|
-
import { SignalLocalState } from './signal-local-state';
|
|
23
|
-
import { SignalRPCClient } from './signal-rpc-client';
|
|
24
|
-
|
|
25
|
-
const DEFAULT_RECONNECT_TIMEOUT = 100;
|
|
26
|
-
const MAX_RECONNECT_TIMEOUT = 5_000;
|
|
27
|
-
const ERROR_RECONCILE_DELAY = 1_000;
|
|
28
|
-
const RECONCILE_INTERVAL = 5_000;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* KUBE-specific signaling client.
|
|
32
|
-
* Establishes a websocket connection to signal server and provides RPC methods.
|
|
33
|
-
* Subscription state updates are executed immediately against the local state which
|
|
34
|
-
* is reconciled periodically.
|
|
35
|
-
* TODO(mykola): Delete.
|
|
36
|
-
* @deprecated
|
|
37
|
-
*/
|
|
38
|
-
// TODO(burdon): Rename impl.
|
|
39
|
-
export class SignalClient extends Resource implements SignalClientMethods {
|
|
40
|
-
private readonly _monitor = new SignalClientMonitor();
|
|
41
|
-
|
|
42
|
-
private _state = SignalState.CLOSED;
|
|
43
|
-
private _lastError?: Error;
|
|
44
|
-
private _lastReconciliationFailed = false;
|
|
45
|
-
|
|
46
|
-
private readonly _clientReady = new Trigger();
|
|
47
|
-
private _connectionCtx?: Context;
|
|
48
|
-
private _client?: SignalRPCClient;
|
|
49
|
-
|
|
50
|
-
private _reconcileTask?: DeferredTask;
|
|
51
|
-
private _reconnectTask?: DeferredTask;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Number of milliseconds after which the connection will be attempted again in case of error.
|
|
55
|
-
*/
|
|
56
|
-
private _reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @internal
|
|
60
|
-
*/
|
|
61
|
-
readonly localState: SignalLocalState;
|
|
62
|
-
|
|
63
|
-
readonly statusChanged = new Event<SignalStatus>();
|
|
64
|
-
|
|
65
|
-
public readonly onMessage = new Event<Message>();
|
|
66
|
-
public readonly swarmEvent = new Event<SwarmEvent>();
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @param _host Signal server websocket URL.
|
|
70
|
-
* @param onMessage called when a new message is received.
|
|
71
|
-
* @param onSwarmEvent called when a new swarm event is received.
|
|
72
|
-
* @param _getMetadata signal-message metadata provider, called for every message.
|
|
73
|
-
*/
|
|
74
|
-
constructor(
|
|
75
|
-
private readonly _host: string,
|
|
76
|
-
private readonly _getMetadata?: () => any,
|
|
77
|
-
) {
|
|
78
|
-
super();
|
|
79
|
-
if (!this._host.startsWith('wss://') && !this._host.startsWith('ws://')) {
|
|
80
|
-
throw new Error(`Signal server requires a websocket URL. Provided: ${this._host}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.localState = new SignalLocalState(
|
|
84
|
-
async (message) => {
|
|
85
|
-
this._monitor.recordMessageReceived(message);
|
|
86
|
-
this.onMessage.emit(message);
|
|
87
|
-
},
|
|
88
|
-
async (event) => this.swarmEvent.emit(event),
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
protected override async _open(): Promise<void> {
|
|
93
|
-
log('opening signal client');
|
|
94
|
-
|
|
95
|
-
if ([SignalState.CONNECTED, SignalState.CONNECTING].includes(this._state)) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
this._setState(SignalState.CONNECTING);
|
|
99
|
-
|
|
100
|
-
this._reconcileTask = new DeferredTask(this._ctx, async () => {
|
|
101
|
-
try {
|
|
102
|
-
await cancelWithContext(this._connectionCtx!, this._clientReady.wait({ timeout: 5_000 }));
|
|
103
|
-
invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
|
|
104
|
-
await this.localState.reconcile(this._connectionCtx!, this._client!);
|
|
105
|
-
this._monitor.recordReconciliation({ success: true });
|
|
106
|
-
this._lastReconciliationFailed = false;
|
|
107
|
-
} catch (err) {
|
|
108
|
-
this._lastReconciliationFailed = true;
|
|
109
|
-
this._monitor.recordReconciliation({ success: false });
|
|
110
|
-
throw err;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Reconcile subscriptions periodically.
|
|
115
|
-
scheduleTaskInterval(
|
|
116
|
-
this._ctx,
|
|
117
|
-
async () => {
|
|
118
|
-
if (this._state === SignalState.CONNECTED) {
|
|
119
|
-
this._reconcileTask!.schedule();
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
RECONCILE_INTERVAL,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
this._reconnectTask = new DeferredTask(this._ctx, async () => {
|
|
126
|
-
try {
|
|
127
|
-
await this._reconnect();
|
|
128
|
-
this._monitor.recordReconnect({ success: true });
|
|
129
|
-
} catch (err) {
|
|
130
|
-
this._monitor.recordReconnect({ success: false });
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
this._createClient();
|
|
136
|
-
log('opened signal client');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
protected override async _catch(err: Error): Promise<void> {
|
|
140
|
-
if (this._state === SignalState.CLOSED || this._ctx.disposed) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
// Don't log consecutive reconciliation failures.
|
|
144
|
-
if (this._state === SignalState.CONNECTED && !this._lastReconciliationFailed) {
|
|
145
|
-
log.warn('SignalClient error:', err);
|
|
146
|
-
}
|
|
147
|
-
this._scheduleReconcileAfterError();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
protected override async _close(): Promise<void> {
|
|
151
|
-
log('closing...');
|
|
152
|
-
if ([SignalState.CLOSED].includes(this._state)) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this._setState(SignalState.CLOSED);
|
|
157
|
-
await this._safeResetClient();
|
|
158
|
-
|
|
159
|
-
log('closed');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
getStatus(): SignalStatus {
|
|
163
|
-
return {
|
|
164
|
-
host: this._host,
|
|
165
|
-
state: this._state,
|
|
166
|
-
error: this._lastError?.message,
|
|
167
|
-
reconnectIn: this._reconnectAfter,
|
|
168
|
-
...this._monitor.getRecordedTimestamps(),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async join(_ctx: Context, args: JoinRequest): Promise<void> {
|
|
173
|
-
log('joining', { topic: args.topic, peerId: args.peer.peerKey });
|
|
174
|
-
this._monitor.recordJoin();
|
|
175
|
-
this.localState.join({ topic: args.topic, peerId: PublicKey.from(args.peer.peerKey) });
|
|
176
|
-
this._reconcileTask?.schedule();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async leave(_ctx: Context, args: LeaveRequest): Promise<void> {
|
|
180
|
-
log('leaving', { topic: args.topic, peerId: args.peer.peerKey });
|
|
181
|
-
this._monitor.recordLeave();
|
|
182
|
-
this.localState.leave({ topic: args.topic, peerId: PublicKey.from(args.peer.peerKey) });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async query(_ctx: Context, params: QueryRequest): Promise<SwarmResponse> {
|
|
186
|
-
throw new Error('Not implemented');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async sendMessage(_ctx: Context, msg: Message): Promise<void> {
|
|
190
|
-
return this._monitor.recordMessageSending(msg, async () => {
|
|
191
|
-
await this._clientReady.wait();
|
|
192
|
-
invariant(this._state === SignalState.CONNECTED, 'Not connected to Signal Server');
|
|
193
|
-
invariant(msg.author.peerKey, 'Author key required');
|
|
194
|
-
invariant(msg.recipient.peerKey, 'Recipient key required');
|
|
195
|
-
await this._client!.sendMessage({
|
|
196
|
-
author: PublicKey.from(msg.author.peerKey),
|
|
197
|
-
recipient: PublicKey.from(msg.recipient.peerKey),
|
|
198
|
-
payload: msg.payload,
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async subscribeMessages(peer: PeerInfo): Promise<void> {
|
|
204
|
-
invariant(peer.peerKey, 'Peer key required');
|
|
205
|
-
log('subscribing to messages', { peer });
|
|
206
|
-
this.localState.subscribeMessages(PublicKey.from(peer.peerKey));
|
|
207
|
-
this._reconcileTask?.schedule();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async unsubscribeMessages(peer: PeerInfo): Promise<void> {
|
|
211
|
-
invariant(peer.peerKey, 'Peer key required');
|
|
212
|
-
log('unsubscribing from messages', { peer });
|
|
213
|
-
this.localState.unsubscribeMessages(PublicKey.from(peer.peerKey));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private _scheduleReconcileAfterError(): void {
|
|
217
|
-
scheduleTask(this._ctx, () => this._reconcileTask!.schedule(), ERROR_RECONCILE_DELAY);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private _createClient(): void {
|
|
221
|
-
log('creating client', { host: this._host, state: this._state });
|
|
222
|
-
invariant(!this._client, 'Client already created');
|
|
223
|
-
|
|
224
|
-
this._monitor.recordConnectionStartTime();
|
|
225
|
-
|
|
226
|
-
// Create new context for each connection.
|
|
227
|
-
this._connectionCtx = this._ctx.derive();
|
|
228
|
-
this._connectionCtx.onDispose(async () => {
|
|
229
|
-
log('connection context disposed');
|
|
230
|
-
const { failureCount } = await this.localState.safeCloseStreams();
|
|
231
|
-
this._monitor.recordStreamCloseErrors(failureCount);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const client = new SignalRPCClient({
|
|
236
|
-
url: this._host,
|
|
237
|
-
callbacks: {
|
|
238
|
-
onConnected: () => {
|
|
239
|
-
if (client === this._client) {
|
|
240
|
-
log('socket connected');
|
|
241
|
-
this._onConnected();
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
onDisconnected: () => {
|
|
246
|
-
if (client !== this._client) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
log('socket disconnected', { state: this._state });
|
|
250
|
-
if (this._state === SignalState.ERROR) {
|
|
251
|
-
// Ignore disconnects after error.
|
|
252
|
-
// Handled by error handler before disconnect handler.
|
|
253
|
-
this._setState(SignalState.DISCONNECTED);
|
|
254
|
-
} else {
|
|
255
|
-
this._onDisconnected();
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
onError: (error) => {
|
|
260
|
-
if (client === this._client) {
|
|
261
|
-
log('socket error', { error, state: this._state });
|
|
262
|
-
this._onDisconnected({ error });
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
getMetadata: this._getMetadata,
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
this._client = client;
|
|
269
|
-
} catch (error: any) {
|
|
270
|
-
this._client = undefined;
|
|
271
|
-
this._onDisconnected({ error });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private async _reconnect(): Promise<void> {
|
|
276
|
-
log(`reconnecting in ${this._reconnectAfter}ms`, { state: this._state });
|
|
277
|
-
|
|
278
|
-
if (this._state === SignalState.RECONNECTING) {
|
|
279
|
-
log.info('Signal api already reconnecting.');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (this._state === SignalState.CLOSED) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
this._setState(SignalState.RECONNECTING);
|
|
286
|
-
|
|
287
|
-
await this._safeResetClient();
|
|
288
|
-
|
|
289
|
-
await cancelWithContext(this._ctx!, sleep(this._reconnectAfter));
|
|
290
|
-
|
|
291
|
-
this._createClient();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private _onConnected(): void {
|
|
295
|
-
this._lastError = undefined;
|
|
296
|
-
this._lastReconciliationFailed = false;
|
|
297
|
-
this._reconnectAfter = DEFAULT_RECONNECT_TIMEOUT;
|
|
298
|
-
this._setState(SignalState.CONNECTED);
|
|
299
|
-
this._clientReady.wake();
|
|
300
|
-
this._reconcileTask!.schedule();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private _onDisconnected(options?: { error: Error }): void {
|
|
304
|
-
this._updateReconnectTimeout();
|
|
305
|
-
if (this._state === SignalState.CLOSED) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (options?.error) {
|
|
309
|
-
this._lastError = options.error;
|
|
310
|
-
this._setState(SignalState.ERROR);
|
|
311
|
-
} else {
|
|
312
|
-
this._setState(SignalState.DISCONNECTED);
|
|
313
|
-
}
|
|
314
|
-
this._reconnectTask!.schedule();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private _setState(newState: SignalState): void {
|
|
318
|
-
this._state = newState;
|
|
319
|
-
this._monitor.recordStateChangeTime();
|
|
320
|
-
log('signal state changed', { status: this.getStatus() });
|
|
321
|
-
this.statusChanged.emit(this.getStatus());
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private _updateReconnectTimeout(): void {
|
|
325
|
-
if (this._state !== SignalState.CONNECTED && this._state !== SignalState.CONNECTING) {
|
|
326
|
-
this._reconnectAfter *= 2;
|
|
327
|
-
this._reconnectAfter = Math.min(this._reconnectAfter, MAX_RECONNECT_TIMEOUT);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private async _safeResetClient(): Promise<void> {
|
|
332
|
-
await this._connectionCtx?.dispose();
|
|
333
|
-
this._connectionCtx = undefined;
|
|
334
|
-
|
|
335
|
-
this._clientReady.reset();
|
|
336
|
-
await this._client?.close().catch(() => {});
|
|
337
|
-
this._client = undefined;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Event, asyncTimeout } from '@dxos/async';
|
|
6
|
-
import type { Stream } from '@dxos/codec-protobuf/stream';
|
|
7
|
-
import { type Context, cancelWithContext } from '@dxos/context';
|
|
8
|
-
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import { log } from '@dxos/log';
|
|
10
|
-
import {
|
|
11
|
-
type Message as SignalMessage,
|
|
12
|
-
type SwarmEvent as SwarmEventProto,
|
|
13
|
-
} from '@dxos/protocols/proto/dxos/mesh/signal';
|
|
14
|
-
import { ComplexMap, ComplexSet, safeAwaitAll } from '@dxos/util';
|
|
15
|
-
|
|
16
|
-
import type { Message, SwarmEvent } from '../signal-methods';
|
|
17
|
-
import { type SignalRPCClient } from './signal-rpc-client';
|
|
18
|
-
|
|
19
|
-
export class SignalLocalState {
|
|
20
|
-
/**
|
|
21
|
-
* Swarm events streams. Keys represent actually joined topic and peerId.
|
|
22
|
-
*/
|
|
23
|
-
private readonly _swarmStreams = new ComplexMap<{ topic: PublicKey; peerId: PublicKey }, Stream<SwarmEventProto>>(
|
|
24
|
-
({ topic, peerId }) => topic.toHex() + peerId.toHex(),
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Represent desired joined topic and peerId.
|
|
29
|
-
*/
|
|
30
|
-
private readonly _joinedTopics = new ComplexSet<{ topic: PublicKey; peerId: PublicKey }>(
|
|
31
|
-
({ topic, peerId }) => topic.toHex() + peerId.toHex(),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Represent desired message subscriptions.
|
|
36
|
-
*/
|
|
37
|
-
private readonly _subscribedMessages = new ComplexSet<{ peerId: PublicKey }>(({ peerId }) => peerId.toHex());
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Message streams. Keys represents actually subscribed peers.
|
|
41
|
-
* @internal
|
|
42
|
-
*/
|
|
43
|
-
readonly messageStreams = new ComplexMap<PublicKey, Stream<SignalMessage>>((key) => key.toHex());
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Event to use in tests to wait till subscription is successfully established.
|
|
47
|
-
* @internal
|
|
48
|
-
*/
|
|
49
|
-
readonly reconciled = new Event();
|
|
50
|
-
|
|
51
|
-
constructor(
|
|
52
|
-
private readonly _onMessage: (params: Message) => Promise<void>,
|
|
53
|
-
private readonly _onSwarmEvent: (params: SwarmEvent) => Promise<void>,
|
|
54
|
-
) {}
|
|
55
|
-
|
|
56
|
-
async safeCloseStreams(): Promise<{ failureCount: number }> {
|
|
57
|
-
const streams = ([...this._swarmStreams.values()] as Stream<any>[]).concat([...this.messageStreams.values()]);
|
|
58
|
-
this._swarmStreams.clear();
|
|
59
|
-
this.messageStreams.clear();
|
|
60
|
-
const failureCount = (await safeAwaitAll(streams, (s) => s.close())).length;
|
|
61
|
-
return { failureCount };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
join({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }): void {
|
|
65
|
-
this._joinedTopics.add({ topic, peerId });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
leave({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }): void {
|
|
69
|
-
void this._swarmStreams.get({ topic, peerId })?.close();
|
|
70
|
-
this._swarmStreams.delete({ topic, peerId });
|
|
71
|
-
this._joinedTopics.delete({ topic, peerId });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
subscribeMessages(peerId: PublicKey): void {
|
|
75
|
-
this._subscribedMessages.add({ peerId });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
unsubscribeMessages(peerId: PublicKey): void {
|
|
79
|
-
log('unsubscribing from messages', { peerId });
|
|
80
|
-
this._subscribedMessages.delete({ peerId });
|
|
81
|
-
void this.messageStreams.get(peerId)?.close();
|
|
82
|
-
this.messageStreams.delete(peerId);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public async reconcile(ctx: Context, client: SignalRPCClient): Promise<void> {
|
|
86
|
-
await this._reconcileSwarmSubscriptions(ctx, client);
|
|
87
|
-
await this._reconcileMessageSubscriptions(ctx, client);
|
|
88
|
-
this.reconciled.emit();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private async _reconcileSwarmSubscriptions(ctx: Context, client: SignalRPCClient): Promise<void> {
|
|
92
|
-
// Unsubscribe from topics that are no longer needed.
|
|
93
|
-
for (const { topic, peerId } of this._swarmStreams.keys()) {
|
|
94
|
-
// Join desired topics.
|
|
95
|
-
if (this._joinedTopics.has({ topic, peerId })) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
void this._swarmStreams.get({ topic, peerId })?.close();
|
|
100
|
-
this._swarmStreams.delete({ topic, peerId });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Subscribe to topics that are needed.
|
|
104
|
-
for (const { topic, peerId } of this._joinedTopics.values()) {
|
|
105
|
-
// Join desired topics.
|
|
106
|
-
if (this._swarmStreams.has({ topic, peerId })) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const swarmStream = await asyncTimeout(cancelWithContext(ctx, client.join({ topic, peerId })), 5_000);
|
|
111
|
-
// Subscribing to swarm events.
|
|
112
|
-
// TODO(mykola): What happens when the swarm stream is closed? Maybe send leave event for each peer?
|
|
113
|
-
swarmStream.subscribe(async (swarmEvent: SwarmEventProto) => {
|
|
114
|
-
if (this._joinedTopics.has({ topic, peerId })) {
|
|
115
|
-
log('swarm event', { swarmEvent });
|
|
116
|
-
const event: SwarmEvent = swarmEvent.peerAvailable
|
|
117
|
-
? {
|
|
118
|
-
topic,
|
|
119
|
-
peerAvailable: {
|
|
120
|
-
...swarmEvent.peerAvailable,
|
|
121
|
-
peer: { peerKey: PublicKey.from(swarmEvent.peerAvailable.peer).toHex() },
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
: {
|
|
125
|
-
topic,
|
|
126
|
-
peerLeft: {
|
|
127
|
-
...swarmEvent.peerLeft,
|
|
128
|
-
peer: { peerKey: PublicKey.from(swarmEvent.peerLeft!.peer).toHex() },
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
await this._onSwarmEvent(event);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Saving swarm stream.
|
|
136
|
-
this._swarmStreams.set({ topic, peerId }, swarmStream);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private async _reconcileMessageSubscriptions(ctx: Context, client: SignalRPCClient): Promise<void> {
|
|
141
|
-
// Unsubscribe from messages that are no longer needed.
|
|
142
|
-
for (const peerId of this.messageStreams.keys()) {
|
|
143
|
-
// Join desired topics.
|
|
144
|
-
if (this._subscribedMessages.has({ peerId })) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
void this.messageStreams.get(peerId)?.close();
|
|
149
|
-
this.messageStreams.delete(peerId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Subscribe to messages that are needed.
|
|
153
|
-
for (const { peerId } of this._subscribedMessages.values()) {
|
|
154
|
-
if (this.messageStreams.has(peerId)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const messageStream = await asyncTimeout(cancelWithContext(ctx, client.receiveMessages(peerId)), 5_000);
|
|
159
|
-
messageStream.subscribe(async (signalMessage: SignalMessage) => {
|
|
160
|
-
if (this._subscribedMessages.has({ peerId })) {
|
|
161
|
-
const message: Message = {
|
|
162
|
-
author: { peerKey: PublicKey.from(signalMessage.author).toHex() },
|
|
163
|
-
recipient: { peerKey: PublicKey.from(signalMessage.recipient).toHex() },
|
|
164
|
-
payload: signalMessage.payload,
|
|
165
|
-
};
|
|
166
|
-
await this._onMessage(message);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// Saving message stream.
|
|
171
|
-
this.messageStreams.set(peerId, messageStream);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { trace } from '@dxos/tracing';
|
|
6
|
-
|
|
7
|
-
export class SignalRpcClientMonitor {
|
|
8
|
-
public recordClientCloseFailure(params: { failureReason: string }): void {
|
|
9
|
-
trace.metrics.increment('dxos.mesh.signal.signal-rpc-client.close-failure', 1, {
|
|
10
|
-
tags: {
|
|
11
|
-
reason: params.failureReason,
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2022 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, onTestFinished, test } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { type Any } from '@dxos/codec-protobuf';
|
|
8
|
-
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import { log } from '@dxos/log';
|
|
10
|
-
import { schema } from '@dxos/protocols/proto';
|
|
11
|
-
import { type Message as SignalMessage, type SwarmEvent } from '@dxos/protocols/proto/dxos/mesh/signal';
|
|
12
|
-
import { type SignalServerRunner, runTestSignalServer } from '@dxos/signal';
|
|
13
|
-
|
|
14
|
-
import { SignalRPCClient } from './signal-rpc-client';
|
|
15
|
-
|
|
16
|
-
describe('SignalRPCClient', () => {
|
|
17
|
-
let broker: SignalServerRunner;
|
|
18
|
-
|
|
19
|
-
beforeAll(async () => {
|
|
20
|
-
broker = await runTestSignalServer();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterAll(() => {
|
|
24
|
-
void broker.stop();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// TODO(burdon): Convert to TestBuilder pattern.
|
|
28
|
-
const setupClient = async () => {
|
|
29
|
-
const client = new SignalRPCClient({ url: broker.url() });
|
|
30
|
-
onTestFinished(async () => await client.close());
|
|
31
|
-
return client;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
test('signal between 2 peers', { timeout: 2_000 }, async () => {
|
|
35
|
-
const client1 = await setupClient();
|
|
36
|
-
const client2 = await setupClient();
|
|
37
|
-
|
|
38
|
-
const peerId1 = PublicKey.random();
|
|
39
|
-
const peerId2 = PublicKey.random();
|
|
40
|
-
|
|
41
|
-
const stream1 = await client1.receiveMessages(peerId1);
|
|
42
|
-
const payload: Any = {
|
|
43
|
-
type_url: 'example.testing.data.TestPayload',
|
|
44
|
-
value: schema.getCodecForType('example.testing.data.TestPayload').encode({ data: 'Some payload' }),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const received: Promise<SignalMessage> = new Promise((resolve) => {
|
|
48
|
-
stream1.subscribe(
|
|
49
|
-
(message) => {
|
|
50
|
-
resolve(message);
|
|
51
|
-
},
|
|
52
|
-
(error) => {
|
|
53
|
-
if (error) {
|
|
54
|
-
log.catch(error);
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await client2.sendMessage({
|
|
62
|
-
author: peerId2,
|
|
63
|
-
recipient: peerId1,
|
|
64
|
-
payload,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect((await received).author).toEqual(peerId2.asBuffer());
|
|
68
|
-
expect((await received).payload).toEqual(expect.objectContaining(payload));
|
|
69
|
-
void stream1.close();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('join', { timeout: 2_000 }, async () => {
|
|
73
|
-
const client1 = await setupClient();
|
|
74
|
-
const client2 = await setupClient();
|
|
75
|
-
|
|
76
|
-
const peerId1 = PublicKey.random();
|
|
77
|
-
const peerId2 = PublicKey.random();
|
|
78
|
-
const topic = PublicKey.random();
|
|
79
|
-
|
|
80
|
-
const stream1 = await client1.join({ topic, peerId: peerId1 });
|
|
81
|
-
const promise = new Promise<SwarmEvent>((resolve) => {
|
|
82
|
-
stream1.subscribe(
|
|
83
|
-
(event: SwarmEvent) => {
|
|
84
|
-
if (event.peerAvailable && peerId2.equals(event.peerAvailable.peer!)) {
|
|
85
|
-
resolve(event);
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
(error: any) => {
|
|
89
|
-
if (error) {
|
|
90
|
-
log.error(error);
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
const stream2 = await client2.join({ topic, peerId: peerId2 });
|
|
97
|
-
|
|
98
|
-
expect((await promise).peerAvailable?.peer).toEqual(peerId2.asBuffer());
|
|
99
|
-
void stream1.close();
|
|
100
|
-
void stream2.close();
|
|
101
|
-
});
|
|
102
|
-
});
|