@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.
Files changed (88) hide show
  1. package/dist/lib/chunk-memory-signal-manager.mjs +652 -0
  2. package/dist/lib/chunk-memory-signal-manager.mjs.map +1 -0
  3. package/dist/lib/index.mjs +377 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/testing.mjs +288 -0
  6. package/dist/lib/testing.mjs.map +1 -0
  7. package/dist/types/src/index.d.ts +0 -1
  8. package/dist/types/src/index.d.ts.map +1 -1
  9. package/dist/types/src/messenger.blueprint-test.d.ts.map +1 -1
  10. package/dist/types/src/messenger.d.ts +1 -1
  11. package/dist/types/src/messenger.d.ts.map +1 -1
  12. package/dist/types/src/signal-manager/edge-signal-manager.d.ts +21 -4
  13. package/dist/types/src/signal-manager/edge-signal-manager.d.ts.map +1 -1
  14. package/dist/types/src/signal-manager/edge-signal-manager.test.d.ts +2 -0
  15. package/dist/types/src/signal-manager/edge-signal-manager.test.d.ts.map +1 -0
  16. package/dist/types/src/signal-manager/index.d.ts +1 -2
  17. package/dist/types/src/signal-manager/index.d.ts.map +1 -1
  18. package/dist/types/src/signal-manager/memory-signal-manager.d.ts +12 -10
  19. package/dist/types/src/signal-manager/memory-signal-manager.d.ts.map +1 -1
  20. package/dist/types/src/signal-manager/memory-signal-manager.test.d.ts +2 -0
  21. package/dist/types/src/signal-manager/memory-signal-manager.test.d.ts.map +1 -0
  22. package/dist/types/src/signal-manager/signal-manager.d.ts +5 -0
  23. package/dist/types/src/signal-manager/signal-manager.d.ts.map +1 -1
  24. package/dist/types/src/signal-manager/utils.d.ts +6 -4
  25. package/dist/types/src/signal-manager/utils.d.ts.map +1 -1
  26. package/dist/types/src/signal-methods.d.ts +33 -22
  27. package/dist/types/src/signal-methods.d.ts.map +1 -1
  28. package/dist/types/src/testing/index.d.ts +1 -0
  29. package/dist/types/src/testing/index.d.ts.map +1 -1
  30. package/dist/types/src/testing/test-edge-mesh.d.ts +61 -0
  31. package/dist/types/src/testing/test-edge-mesh.d.ts.map +1 -0
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +19 -19
  34. package/src/index.ts +0 -1
  35. package/src/messenger.blueprint-test.ts +0 -25
  36. package/src/messenger.node.test.ts +1 -1
  37. package/src/messenger.ts +42 -18
  38. package/src/signal-manager/edge-signal-manager.test.ts +193 -0
  39. package/src/signal-manager/edge-signal-manager.ts +145 -12
  40. package/src/signal-manager/index.ts +1 -2
  41. package/src/signal-manager/memory-signal-manager.test.ts +199 -0
  42. package/src/signal-manager/memory-signal-manager.ts +87 -47
  43. package/src/signal-manager/signal-manager.ts +7 -0
  44. package/src/signal-manager/utils.ts +30 -21
  45. package/src/signal-methods.ts +37 -26
  46. package/src/testing/index.ts +1 -0
  47. package/src/testing/test-edge-mesh.ts +196 -0
  48. package/src/testing/utils.ts +2 -2
  49. package/dist/lib/neutral/chunk-EC4H56J5.mjs +0 -497
  50. package/dist/lib/neutral/chunk-EC4H56J5.mjs.map +0 -7
  51. package/dist/lib/neutral/index.mjs +0 -1249
  52. package/dist/lib/neutral/index.mjs.map +0 -7
  53. package/dist/lib/neutral/meta.json +0 -1
  54. package/dist/lib/neutral/testing/index.mjs +0 -155
  55. package/dist/lib/neutral/testing/index.mjs.map +0 -7
  56. package/dist/types/src/signal-client/index.d.ts +0 -2
  57. package/dist/types/src/signal-client/index.d.ts.map +0 -1
  58. package/dist/types/src/signal-client/signal-client-monitor.d.ts +0 -30
  59. package/dist/types/src/signal-client/signal-client-monitor.d.ts.map +0 -1
  60. package/dist/types/src/signal-client/signal-client.d.ts +0 -59
  61. package/dist/types/src/signal-client/signal-client.d.ts.map +0 -1
  62. package/dist/types/src/signal-client/signal-client.node.test.d.ts +0 -2
  63. package/dist/types/src/signal-client/signal-client.node.test.d.ts.map +0 -1
  64. package/dist/types/src/signal-client/signal-local-state.d.ts +0 -38
  65. package/dist/types/src/signal-client/signal-local-state.d.ts.map +0 -1
  66. package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts +0 -6
  67. package/dist/types/src/signal-client/signal-rpc-client-monitor.d.ts.map +0 -1
  68. package/dist/types/src/signal-client/signal-rpc-client.d.ts +0 -47
  69. package/dist/types/src/signal-client/signal-rpc-client.d.ts.map +0 -1
  70. package/dist/types/src/signal-client/signal-rpc-client.node.test.d.ts +0 -2
  71. package/dist/types/src/signal-client/signal-rpc-client.node.test.d.ts.map +0 -1
  72. package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts +0 -8
  73. package/dist/types/src/signal-manager/websocket-signal-manager-monitor.d.ts.map +0 -1
  74. package/dist/types/src/signal-manager/websocket-signal-manager.d.ts +0 -41
  75. package/dist/types/src/signal-manager/websocket-signal-manager.d.ts.map +0 -1
  76. package/dist/types/src/signal-manager/websocket-signal-manager.node.test.d.ts +0 -2
  77. package/dist/types/src/signal-manager/websocket-signal-manager.node.test.d.ts.map +0 -1
  78. package/src/signal-client/index.ts +0 -5
  79. package/src/signal-client/signal-client-monitor.ts +0 -111
  80. package/src/signal-client/signal-client.node.test.ts +0 -185
  81. package/src/signal-client/signal-client.ts +0 -339
  82. package/src/signal-client/signal-local-state.ts +0 -174
  83. package/src/signal-client/signal-rpc-client-monitor.ts +0 -15
  84. package/src/signal-client/signal-rpc-client.node.test.ts +0 -102
  85. package/src/signal-client/signal-rpc-client.ts +0 -227
  86. package/src/signal-manager/websocket-signal-manager-monitor.ts +0 -20
  87. package/src/signal-manager/websocket-signal-manager.node.test.ts +0 -112
  88. 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
- }