@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,227 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2022 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import WebSocket from 'isomorphic-ws';
|
|
6
|
-
|
|
7
|
-
import { TimeoutError, Trigger, scheduleTaskInterval } from '@dxos/async';
|
|
8
|
-
import { type Any, type Stream } from '@dxos/codec-protobuf';
|
|
9
|
-
import { Context } from '@dxos/context';
|
|
10
|
-
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { PublicKey } from '@dxos/keys';
|
|
12
|
-
import { log } from '@dxos/log';
|
|
13
|
-
import { schema } from '@dxos/protocols/proto';
|
|
14
|
-
import { type Signal, type Message as SignalMessage } from '@dxos/protocols/proto/dxos/mesh/signal';
|
|
15
|
-
import { type ProtoRpcPeer, createProtoRpcPeer } from '@dxos/rpc';
|
|
16
|
-
|
|
17
|
-
import { SignalRpcClientMonitor } from './signal-rpc-client-monitor';
|
|
18
|
-
|
|
19
|
-
const SIGNAL_KEEPALIVE_INTERVAL = 10000;
|
|
20
|
-
|
|
21
|
-
interface Services {
|
|
22
|
-
Signal: Signal;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type SignalCallbacks = {
|
|
26
|
-
onConnected?: () => void;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Called on disconnect.
|
|
30
|
-
* In case of error, `onError` will be called first and then `onDisconnected`.
|
|
31
|
-
*/
|
|
32
|
-
onDisconnected?: () => void;
|
|
33
|
-
|
|
34
|
-
onError?: (error: Error) => void;
|
|
35
|
-
getMetadata?: () => any;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type SignalRPCClientProps = {
|
|
39
|
-
url: string;
|
|
40
|
-
callbacks?: SignalCallbacks;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Signal RPC client.
|
|
45
|
-
* TODO(mykola): Delete.
|
|
46
|
-
* @deprecated
|
|
47
|
-
*/
|
|
48
|
-
export class SignalRPCClient {
|
|
49
|
-
private readonly _socket: WebSocket;
|
|
50
|
-
private readonly _rpc: ProtoRpcPeer<Services>;
|
|
51
|
-
private readonly _connectTrigger = new Trigger();
|
|
52
|
-
|
|
53
|
-
private _keepaliveCtx?: Context;
|
|
54
|
-
|
|
55
|
-
private _closed = false;
|
|
56
|
-
|
|
57
|
-
private readonly _url: string;
|
|
58
|
-
private readonly _callbacks: SignalCallbacks;
|
|
59
|
-
private readonly _closeComplete = new Trigger();
|
|
60
|
-
|
|
61
|
-
private readonly _monitor = new SignalRpcClientMonitor();
|
|
62
|
-
|
|
63
|
-
constructor({ url, callbacks = {} }: SignalRPCClientProps) {
|
|
64
|
-
log('creating signal rpc client', { url });
|
|
65
|
-
this._url = url;
|
|
66
|
-
this._callbacks = callbacks;
|
|
67
|
-
this._socket = new WebSocket(this._url);
|
|
68
|
-
|
|
69
|
-
this._rpc = createProtoRpcPeer({
|
|
70
|
-
requested: {
|
|
71
|
-
Signal: schema.getService('dxos.mesh.signal.Signal'),
|
|
72
|
-
},
|
|
73
|
-
noHandshake: true,
|
|
74
|
-
port: {
|
|
75
|
-
send: (msg) => {
|
|
76
|
-
if (this._closed) {
|
|
77
|
-
// Do not send messages after close.
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
this._socket!.send(msg);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
log.warn('send error', err);
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
subscribe: (cb) => {
|
|
87
|
-
this._socket!.onmessage = async (msg: WebSocket.MessageEvent) => {
|
|
88
|
-
if (typeof Blob !== 'undefined' && msg.data instanceof Blob) {
|
|
89
|
-
cb(Buffer.from(await msg.data.arrayBuffer()));
|
|
90
|
-
} else {
|
|
91
|
-
cb(msg.data as any);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
encodingOptions: {
|
|
97
|
-
preserveAny: true,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
this._socket.onopen = async () => {
|
|
102
|
-
try {
|
|
103
|
-
await this._rpc!.open();
|
|
104
|
-
if (this._closed) {
|
|
105
|
-
await this._safeCloseRpc();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
log(`RPC open ${this._url}`);
|
|
109
|
-
this._callbacks.onConnected?.();
|
|
110
|
-
this._connectTrigger.wake();
|
|
111
|
-
this._keepaliveCtx = new Context();
|
|
112
|
-
scheduleTaskInterval(
|
|
113
|
-
this._keepaliveCtx,
|
|
114
|
-
async () => {
|
|
115
|
-
// TODO(nf): use RFC6455 ping/pong once implemented in the browser?
|
|
116
|
-
// TODO(nf): check for pong response from server (once implemented)
|
|
117
|
-
// Current implementation of signal server ignores all text data messages, and does not send a response.
|
|
118
|
-
// However this is enough to detect breakages in the connection as TCP will reset the connection if ACKs are not received.
|
|
119
|
-
this._socket?.send('__ping__');
|
|
120
|
-
},
|
|
121
|
-
SIGNAL_KEEPALIVE_INTERVAL,
|
|
122
|
-
);
|
|
123
|
-
} catch (err: any) {
|
|
124
|
-
this._callbacks.onError?.(err);
|
|
125
|
-
this._socket.close();
|
|
126
|
-
this._closed = true;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
this._socket.onclose = async () => {
|
|
131
|
-
log(`Disconnected ${this._url}`);
|
|
132
|
-
this._callbacks.onDisconnected?.();
|
|
133
|
-
this._closeComplete.wake();
|
|
134
|
-
await this.close();
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
this._socket.onerror = async (event: WebSocket.ErrorEvent) => {
|
|
138
|
-
if (this._closed) {
|
|
139
|
-
this._socket.close();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
this._closed = true;
|
|
143
|
-
|
|
144
|
-
this._callbacks.onError?.(event.error ?? new Error(event.message));
|
|
145
|
-
await this._safeCloseRpc();
|
|
146
|
-
|
|
147
|
-
log.warn(`Socket ${event.type ?? 'unknown'} error`, { message: event.message, url: this._url });
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
log('created signal rpc client', { url });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async close(): Promise<void> {
|
|
154
|
-
if (this._closed) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
this._closed = true;
|
|
158
|
-
|
|
159
|
-
await this._keepaliveCtx?.dispose();
|
|
160
|
-
try {
|
|
161
|
-
await this._safeCloseRpc();
|
|
162
|
-
|
|
163
|
-
if (this._socket.readyState === WebSocket.OPEN || this._socket.readyState === WebSocket.CONNECTING) {
|
|
164
|
-
// close() only starts the closing handshake.
|
|
165
|
-
this._socket.close();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
await this._closeComplete.wait({ timeout: 1_000 });
|
|
169
|
-
} catch (err) {
|
|
170
|
-
const failureReason = err instanceof TimeoutError ? 'timeout' : (err?.constructor?.name ?? 'unknown');
|
|
171
|
-
this._monitor.recordClientCloseFailure({ failureReason });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async join({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }) {
|
|
176
|
-
log('join', { topic, peerId, metadata: this._callbacks?.getMetadata?.() });
|
|
177
|
-
invariant(!this._closed, 'SignalRPCClient is closed');
|
|
178
|
-
await this._connectTrigger.wait();
|
|
179
|
-
const swarmStream = this._rpc.rpc.Signal.join({
|
|
180
|
-
swarm: topic.asUint8Array(),
|
|
181
|
-
peer: peerId.asUint8Array(),
|
|
182
|
-
metadata: this._callbacks?.getMetadata?.(),
|
|
183
|
-
});
|
|
184
|
-
await swarmStream.waitUntilReady();
|
|
185
|
-
return swarmStream;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async receiveMessages(peerId: PublicKey): Promise<Stream<SignalMessage>> {
|
|
189
|
-
log('receiveMessages', { peerId });
|
|
190
|
-
invariant(!this._closed, 'SignalRPCClient is closed');
|
|
191
|
-
await this._connectTrigger.wait();
|
|
192
|
-
const messageStream = this._rpc.rpc.Signal.receiveMessages({
|
|
193
|
-
peer: peerId.asUint8Array(),
|
|
194
|
-
});
|
|
195
|
-
await messageStream.waitUntilReady();
|
|
196
|
-
return messageStream;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async sendMessage({
|
|
200
|
-
author,
|
|
201
|
-
recipient,
|
|
202
|
-
payload,
|
|
203
|
-
}: {
|
|
204
|
-
author: PublicKey;
|
|
205
|
-
recipient: PublicKey;
|
|
206
|
-
payload: Any;
|
|
207
|
-
}): Promise<void> {
|
|
208
|
-
log('sendMessage', { author, recipient, payload, metadata: this._callbacks?.getMetadata?.() });
|
|
209
|
-
invariant(!this._closed, 'SignalRPCClient is closed');
|
|
210
|
-
await this._connectTrigger.wait();
|
|
211
|
-
await this._rpc.rpc.Signal.sendMessage({
|
|
212
|
-
author: author.asUint8Array(),
|
|
213
|
-
recipient: recipient.asUint8Array(),
|
|
214
|
-
payload,
|
|
215
|
-
metadata: this._callbacks?.getMetadata?.(),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private async _safeCloseRpc(): Promise<void> {
|
|
220
|
-
try {
|
|
221
|
-
this._connectTrigger.reset();
|
|
222
|
-
await this._rpc.close();
|
|
223
|
-
} catch (err) {
|
|
224
|
-
log.catch(err);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { trace } from '@dxos/tracing';
|
|
6
|
-
|
|
7
|
-
export class WebsocketSignalManagerMonitor {
|
|
8
|
-
public recordRateLimitExceeded(): void {
|
|
9
|
-
trace.metrics.increment('dxos.mesh.signal.signal-manager.rate-limit-hit', 1);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
public recordServerFailure(params: { serverName: string; willRestart: boolean }): void {
|
|
13
|
-
trace.metrics.increment('dxos.mesh.signal.signal-manager.server-failure', 1, {
|
|
14
|
-
tags: {
|
|
15
|
-
server: params.serverName,
|
|
16
|
-
restarted: params.willRestart,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { afterAll, beforeAll, describe, test } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { asyncTimeout, sleep } from '@dxos/async';
|
|
8
|
-
import { Context } from '@dxos/context';
|
|
9
|
-
import { PublicKey } from '@dxos/keys';
|
|
10
|
-
import { type SignalServerRunner, runTestSignalServer } from '@dxos/signal';
|
|
11
|
-
import { openAndClose } from '@dxos/test-utils';
|
|
12
|
-
|
|
13
|
-
import { createMessage, expectPeerAvailable, expectReceivedMessage } from '../testing';
|
|
14
|
-
import { WebsocketSignalManager } from './websocket-signal-manager';
|
|
15
|
-
|
|
16
|
-
describe.skip('WebSocketSignalManager', () => {
|
|
17
|
-
let broker1: SignalServerRunner;
|
|
18
|
-
let broker2: SignalServerRunner;
|
|
19
|
-
|
|
20
|
-
beforeAll(async () => {
|
|
21
|
-
broker1 = await runTestSignalServer({ port: 5001 });
|
|
22
|
-
broker2 = await runTestSignalServer({ port: 5002 });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterAll(() => {
|
|
26
|
-
void broker1.stop();
|
|
27
|
-
void broker2.stop();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('join swarm with two brokers', { timeout: 1_000 }, async () => {
|
|
31
|
-
const client1 = new WebsocketSignalManager([{ server: broker1.url() }, { server: broker2.url() }]);
|
|
32
|
-
const client2 = new WebsocketSignalManager([{ server: broker1.url() }]);
|
|
33
|
-
const client3 = new WebsocketSignalManager([{ server: broker2.url() }]);
|
|
34
|
-
await openAndClose(client1, client2, client3);
|
|
35
|
-
|
|
36
|
-
const [topic, peer1, peer2, peer3] = PublicKey.randomSequence();
|
|
37
|
-
|
|
38
|
-
const joined12 = expectPeerAvailable(client1, topic, { peerKey: peer2.toHex() });
|
|
39
|
-
const joined13 = expectPeerAvailable(client1, topic, { peerKey: peer3.toHex() });
|
|
40
|
-
const joined21 = expectPeerAvailable(client2, topic, { peerKey: peer1.toHex() });
|
|
41
|
-
const joined31 = expectPeerAvailable(client3, topic, { peerKey: peer1.toHex() });
|
|
42
|
-
|
|
43
|
-
await client1.join(Context.default(), { topic, peer: { peerKey: peer1.toHex() } });
|
|
44
|
-
await client2.join(Context.default(), { topic, peer: { peerKey: peer2.toHex() } });
|
|
45
|
-
await client3.join(Context.default(), { topic, peer: { peerKey: peer3.toHex() } });
|
|
46
|
-
|
|
47
|
-
await Promise.all([joined12, joined13, joined21, joined31]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('join single swarm with doubled brokers', { timeout: 1_000 }, async () => {
|
|
51
|
-
const client1 = new WebsocketSignalManager([{ server: broker1.url() }, { server: broker2.url() }]);
|
|
52
|
-
const client2 = new WebsocketSignalManager([{ server: broker1.url() }, { server: broker2.url() }]);
|
|
53
|
-
await openAndClose(client1, client2);
|
|
54
|
-
|
|
55
|
-
const [topic, peer1, peer2] = PublicKey.randomSequence();
|
|
56
|
-
|
|
57
|
-
const joined12 = expectPeerAvailable(client1, topic, { peerKey: peer2.toHex() });
|
|
58
|
-
const joined21 = expectPeerAvailable(client2, topic, { peerKey: peer1.toHex() });
|
|
59
|
-
|
|
60
|
-
await client1.join(Context.default(), { topic, peer: { peerKey: peer1.toHex() } });
|
|
61
|
-
await client2.join(Context.default(), { topic, peer: { peerKey: peer2.toHex() } });
|
|
62
|
-
|
|
63
|
-
await asyncTimeout(Promise.all([joined12, joined21]), 1_000);
|
|
64
|
-
|
|
65
|
-
const message = createMessage({ peerKey: peer1.toHex() }, { peerKey: peer2.toHex() });
|
|
66
|
-
|
|
67
|
-
const received = expectReceivedMessage(client2.onMessage, message);
|
|
68
|
-
await client2.subscribeMessages({ peerKey: peer2.toHex() });
|
|
69
|
-
await sleep(50);
|
|
70
|
-
await client1.sendMessage(Context.default(), message);
|
|
71
|
-
|
|
72
|
-
await asyncTimeout(received, 1_000);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('works with one broken server', { timeout: 1_000 }, async () => {
|
|
76
|
-
const client1 = new WebsocketSignalManager([{ server: 'ws://broken.server/signal' }, { server: broker1.url() }]);
|
|
77
|
-
const client2 = new WebsocketSignalManager([{ server: 'ws://broken.server/signal' }, { server: broker1.url() }]);
|
|
78
|
-
await openAndClose(client1, client2);
|
|
79
|
-
|
|
80
|
-
const [topic, peer1, peer2] = PublicKey.randomSequence();
|
|
81
|
-
|
|
82
|
-
const joined12 = expectPeerAvailable(client1, topic, { peerKey: peer2.toHex() });
|
|
83
|
-
const joined21 = expectPeerAvailable(client2, topic, { peerKey: peer1.toHex() });
|
|
84
|
-
|
|
85
|
-
await client1.join(Context.default(), { topic, peer: { peerKey: peer1.toHex() } });
|
|
86
|
-
await client2.join(Context.default(), { topic, peer: { peerKey: peer2.toHex() } });
|
|
87
|
-
|
|
88
|
-
await Promise.all([joined12, joined21]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('join two swarms with a broken signal server', { timeout: 1_000 }, async () => {
|
|
92
|
-
const client1 = new WebsocketSignalManager([{ server: 'ws://broken.server/signal' }, { server: broker1.url() }]);
|
|
93
|
-
const client2 = new WebsocketSignalManager([{ server: 'ws://broken.server/signal' }, { server: broker1.url() }]);
|
|
94
|
-
await openAndClose(client1, client2);
|
|
95
|
-
|
|
96
|
-
const [topic1, topic2, peer1, peer2] = PublicKey.randomSequence();
|
|
97
|
-
|
|
98
|
-
const joined112 = expectPeerAvailable(client1, topic1, { peerKey: peer2.toHex() });
|
|
99
|
-
const joined121 = expectPeerAvailable(client2, topic1, { peerKey: peer1.toHex() });
|
|
100
|
-
|
|
101
|
-
await client1.join(Context.default(), { topic: topic1, peer: { peerKey: peer1.toHex() } });
|
|
102
|
-
await client2.join(Context.default(), { topic: topic1, peer: { peerKey: peer2.toHex() } });
|
|
103
|
-
await Promise.all([joined112, joined121]);
|
|
104
|
-
|
|
105
|
-
const joined212 = expectPeerAvailable(client1, topic2, { peerKey: peer2.toHex() });
|
|
106
|
-
const joined221 = expectPeerAvailable(client2, topic2, { peerKey: peer1.toHex() });
|
|
107
|
-
|
|
108
|
-
await client1.join(Context.default(), { topic: topic2, peer: { peerKey: peer1.toHex() } });
|
|
109
|
-
await client2.join(Context.default(), { topic: topic2, peer: { peerKey: peer2.toHex() } });
|
|
110
|
-
await Promise.all([joined212, joined221]);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2020 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { Event, sleep, synchronized } from '@dxos/async';
|
|
6
|
-
import { type Context, LifecycleState, Resource } from '@dxos/context';
|
|
7
|
-
import { invariant } from '@dxos/invariant';
|
|
8
|
-
import { log } from '@dxos/log';
|
|
9
|
-
import { RateLimitExceededError, TimeoutError } from '@dxos/protocols';
|
|
10
|
-
import { type Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
11
|
-
import { type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
12
|
-
import { type JoinRequest, type LeaveRequest, type QueryRequest } from '@dxos/protocols/proto/dxos/edge/signal';
|
|
13
|
-
import { BitField, safeAwaitAll } from '@dxos/util';
|
|
14
|
-
|
|
15
|
-
import { SignalClient } from '../signal-client';
|
|
16
|
-
import {
|
|
17
|
-
type Message,
|
|
18
|
-
type PeerInfo,
|
|
19
|
-
type SignalClientMethods,
|
|
20
|
-
type SignalMethods,
|
|
21
|
-
type SignalStatus,
|
|
22
|
-
type SwarmEvent,
|
|
23
|
-
} from '../signal-methods';
|
|
24
|
-
import { type SignalManager } from './signal-manager';
|
|
25
|
-
import { WebsocketSignalManagerMonitor } from './websocket-signal-manager-monitor';
|
|
26
|
-
|
|
27
|
-
const MAX_SERVER_FAILURES = 5;
|
|
28
|
-
const WSS_SIGNAL_SERVER_REBOOT_DELAY = 3_000;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Manages connection to multiple Signal Servers over WebSocket
|
|
32
|
-
* TODO(mykola): Delete.
|
|
33
|
-
* @deprecated
|
|
34
|
-
*/
|
|
35
|
-
export class WebsocketSignalManager extends Resource implements SignalManager {
|
|
36
|
-
private readonly _servers = new Map<string, SignalClientMethods>();
|
|
37
|
-
private readonly _monitor = new WebsocketSignalManagerMonitor();
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Used to avoid logging failed server restarts more than once until the server actually recovers.
|
|
41
|
-
*/
|
|
42
|
-
private readonly _failedServersBitfield: Uint8Array;
|
|
43
|
-
|
|
44
|
-
readonly failureCount = new Map<string, number>();
|
|
45
|
-
readonly statusChanged = new Event<SignalStatus[]>();
|
|
46
|
-
readonly swarmEvent = new Event<SwarmEvent>();
|
|
47
|
-
|
|
48
|
-
readonly onMessage = new Event<Message>();
|
|
49
|
-
|
|
50
|
-
constructor(
|
|
51
|
-
private readonly _hosts: Runtime.Services.Signal[],
|
|
52
|
-
private readonly _getMetadata?: () => any,
|
|
53
|
-
) {
|
|
54
|
-
super();
|
|
55
|
-
log('Created WebsocketSignalManager', { hosts: this._hosts });
|
|
56
|
-
for (const host of this._hosts) {
|
|
57
|
-
if (this._servers.has(host.server)) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// TODO(burdon): Create factory to support different variants.
|
|
62
|
-
const server = new SignalClient(host.server, this._getMetadata);
|
|
63
|
-
server.swarmEvent.on((data) => this.swarmEvent.emit(data));
|
|
64
|
-
server.onMessage.on((data) => this.onMessage.emit(data));
|
|
65
|
-
|
|
66
|
-
server.statusChanged.on(() => this.statusChanged.emit(this.getStatus()));
|
|
67
|
-
|
|
68
|
-
this._servers.set(host.server, server);
|
|
69
|
-
this.failureCount.set(host.server, 0);
|
|
70
|
-
}
|
|
71
|
-
this._failedServersBitfield = BitField.zeros(this._hosts.length);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected override async _open(): Promise<void> {
|
|
75
|
-
log('open signal manager', { hosts: this._hosts });
|
|
76
|
-
|
|
77
|
-
await safeAwaitAll(this._servers.values(), (server) => server.open());
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
protected override async _close(): Promise<void> {
|
|
81
|
-
await safeAwaitAll(this._servers.values(), (server) => server.close());
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async restartServer(serverName: string): Promise<void> {
|
|
85
|
-
log('restarting server', { serverName });
|
|
86
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
87
|
-
|
|
88
|
-
const server = this._servers.get(serverName);
|
|
89
|
-
invariant(server, 'server not found');
|
|
90
|
-
|
|
91
|
-
await server.close();
|
|
92
|
-
await sleep(WSS_SIGNAL_SERVER_REBOOT_DELAY);
|
|
93
|
-
await server.open();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
getStatus(): SignalStatus[] {
|
|
97
|
-
return Array.from(this._servers.values()).map((server) => server.getStatus());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@synchronized
|
|
101
|
-
async join(_ctx: Context, { topic, peer }: JoinRequest): Promise<void> {
|
|
102
|
-
log('join', { topic, peer });
|
|
103
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
104
|
-
await this._forEachServer((server) => server.join(_ctx, { topic, peer }));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@synchronized
|
|
108
|
-
async leave(_ctx: Context, { topic, peer }: LeaveRequest): Promise<void> {
|
|
109
|
-
log('leaving', { topic, peer });
|
|
110
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
111
|
-
await this._forEachServer((server) => server.leave(_ctx, { topic, peer }));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async query(_ctx: Context, { topic }: QueryRequest): Promise<SwarmResponse> {
|
|
115
|
-
throw new Error('Not implemented');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async sendMessage(_ctx: Context, { author, recipient, payload }: Message): Promise<void> {
|
|
119
|
-
log('signal', { recipient });
|
|
120
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
121
|
-
|
|
122
|
-
void this._forEachServer(async (server, serverName, index) => {
|
|
123
|
-
void server
|
|
124
|
-
.sendMessage(_ctx, { author, recipient, payload })
|
|
125
|
-
.then(() => this._clearServerFailedFlag(serverName, index))
|
|
126
|
-
.catch((err) => {
|
|
127
|
-
if (err instanceof RateLimitExceededError) {
|
|
128
|
-
log.info('WSS rate limit exceeded', { err });
|
|
129
|
-
this._monitor.recordRateLimitExceeded();
|
|
130
|
-
} else if (err instanceof TimeoutError || err.constructor.name === 'TimeoutError') {
|
|
131
|
-
log.info('WSS sendMessage timeout', { err });
|
|
132
|
-
void this.checkServerFailure(serverName, index);
|
|
133
|
-
} else {
|
|
134
|
-
log.warn(`error sending to ${serverName}`, { err });
|
|
135
|
-
void this.checkServerFailure(serverName, index);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
@synchronized
|
|
142
|
-
async checkServerFailure(serverName: string, index: number): Promise<void> {
|
|
143
|
-
const failureCount = this.failureCount.get(serverName!) ?? 0;
|
|
144
|
-
const isRestartRequired = failureCount > MAX_SERVER_FAILURES;
|
|
145
|
-
this._monitor.recordServerFailure({ serverName, willRestart: isRestartRequired });
|
|
146
|
-
if (isRestartRequired) {
|
|
147
|
-
if (!BitField.get(this._failedServersBitfield, index)) {
|
|
148
|
-
log.warn('too many failures for ws-server, restarting', { serverName, failureCount });
|
|
149
|
-
BitField.set(this._failedServersBitfield, index, true);
|
|
150
|
-
}
|
|
151
|
-
await this.restartServer(serverName!);
|
|
152
|
-
this.failureCount.set(serverName!, 0);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.failureCount.set(serverName!, (this.failureCount.get(serverName!) ?? 0) + 1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private _clearServerFailedFlag(serverName: string, index: number): void {
|
|
160
|
-
if (BitField.get(this._failedServersBitfield, index)) {
|
|
161
|
-
log.info('server connection restored', { serverName });
|
|
162
|
-
BitField.set(this._failedServersBitfield, index, false);
|
|
163
|
-
this.failureCount.set(serverName!, 0);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async subscribeMessages(peer: PeerInfo): Promise<void> {
|
|
168
|
-
log('subscribed for message stream', { peer });
|
|
169
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
170
|
-
|
|
171
|
-
await this._forEachServer(async (server) => server.subscribeMessages(peer));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async unsubscribeMessages(peer: PeerInfo): Promise<void> {
|
|
175
|
-
log('subscribed for message stream', { peer });
|
|
176
|
-
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
177
|
-
|
|
178
|
-
await this._forEachServer(async (server) => server.unsubscribeMessages(peer));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private async _forEachServer<ReturnType>(
|
|
182
|
-
fn: (server: SignalMethods, serverName: string, index: number) => Promise<ReturnType>,
|
|
183
|
-
): Promise<ReturnType[]> {
|
|
184
|
-
return Promise.all(
|
|
185
|
-
Array.from(this._servers.entries()).map(([serverName, server], idx) => fn(server, serverName, idx)),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|