@dxos/messaging 0.9.1-staging.ee54ba693a → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/messaging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Messaging",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
".": {
|
|
17
17
|
"source": "./src/index.ts",
|
|
18
18
|
"types": "./dist/types/src/index.d.ts",
|
|
19
|
-
"
|
|
19
|
+
"import": "./dist/lib/index.mjs"
|
|
20
20
|
},
|
|
21
21
|
"./testing": {
|
|
22
22
|
"source": "./src/testing/index.ts",
|
|
23
23
|
"types": "./dist/types/src/testing/index.d.ts",
|
|
24
|
-
"
|
|
24
|
+
"import": "./dist/lib/testing.mjs"
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -31,26 +31,26 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"effect": "3.21.4",
|
|
34
35
|
"isomorphic-ws": "^5.0.0",
|
|
35
36
|
"ws": "^8.17.1",
|
|
36
|
-
"@dxos/async": "0.
|
|
37
|
-
"@dxos/codec-protobuf": "0.
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/log": "0.
|
|
44
|
-
"@dxos/node-std": "0.
|
|
45
|
-
"@dxos/protocols": "0.
|
|
46
|
-
"@dxos/rpc": "0.
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
37
|
+
"@dxos/async": "0.11.0",
|
|
38
|
+
"@dxos/codec-protobuf": "0.11.0",
|
|
39
|
+
"@dxos/context": "0.11.0",
|
|
40
|
+
"@dxos/credentials": "0.11.0",
|
|
41
|
+
"@dxos/edge-client": "0.11.0",
|
|
42
|
+
"@dxos/invariant": "0.11.0",
|
|
43
|
+
"@dxos/keys": "0.11.0",
|
|
44
|
+
"@dxos/log": "0.11.0",
|
|
45
|
+
"@dxos/node-std": "0.11.0",
|
|
46
|
+
"@dxos/protocols": "0.11.0",
|
|
47
|
+
"@dxos/rpc": "0.11.0",
|
|
48
|
+
"@dxos/tracing": "0.11.0",
|
|
49
|
+
"@dxos/util": "0.11.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/test-utils": "0.9.1-staging.ee54ba693a"
|
|
52
|
+
"@dxos/keyring": "0.11.0",
|
|
53
|
+
"@dxos/test-utils": "0.11.0"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,7 @@ import { describe, expect, onTestFinished, test } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { asyncTimeout, latch, sleep } from '@dxos/async';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
|
-
import { range } from '@dxos/util';
|
|
10
9
|
|
|
11
|
-
import { WebsocketSignalManager } from './signal-manager';
|
|
12
10
|
import { type Message } from './signal-methods';
|
|
13
11
|
import { PAYLOAD_1, PAYLOAD_2, PAYLOAD_3, TestBuilder, messageEqual } from './testing';
|
|
14
12
|
|
|
@@ -313,27 +311,4 @@ export const messengerTests = (signalManagerFactory: TestBuilder['createSignalMa
|
|
|
313
311
|
expect(count).toEqual(1);
|
|
314
312
|
});
|
|
315
313
|
});
|
|
316
|
-
|
|
317
|
-
describe.skip('load', () => {
|
|
318
|
-
test('many connections to KUBE', { timeout: 5_000 }, async () => {
|
|
319
|
-
const builder = new TestBuilder({
|
|
320
|
-
signalManagerFactory: async () =>
|
|
321
|
-
new WebsocketSignalManager([{ server: 'wss://dev.kube.dxos.org/.well-known/dx/signal' }]),
|
|
322
|
-
});
|
|
323
|
-
void range(100).map(async () => {
|
|
324
|
-
const peer = await builder.createPeer();
|
|
325
|
-
|
|
326
|
-
void peer.messenger.sendMessage(Context.default(), {
|
|
327
|
-
author: peer.peerInfo,
|
|
328
|
-
recipient: peer.peerInfo,
|
|
329
|
-
payload: {
|
|
330
|
-
type_url: 'dxos.test',
|
|
331
|
-
value: Buffer.from('TEST'),
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
await sleep(1000000);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
314
|
};
|
|
@@ -8,7 +8,7 @@ import { messengerTests } from './messenger.blueprint-test';
|
|
|
8
8
|
import { MemorySignalManager, MemorySignalManagerContext } from './signal-manager';
|
|
9
9
|
|
|
10
10
|
// TODO(mykola): Use EDGE signal server.
|
|
11
|
-
describe('Messenger with
|
|
11
|
+
describe('Messenger with MemorySignalManager', () => {
|
|
12
12
|
let context: MemorySignalManagerContext;
|
|
13
13
|
beforeEach(async () => {
|
|
14
14
|
context = new MemorySignalManagerContext();
|
package/src/messenger.ts
CHANGED
|
@@ -72,12 +72,6 @@ export class Messenger {
|
|
|
72
72
|
this._ctx = new Context({
|
|
73
73
|
onError: (err) => log.catch(err),
|
|
74
74
|
});
|
|
75
|
-
this._ctx.onDispose(
|
|
76
|
-
this._signalManager.onMessage.on(async (message) => {
|
|
77
|
-
log('received message', { from: message.author });
|
|
78
|
-
await this._handleMessage(message);
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
75
|
|
|
82
76
|
// Clear the map periodically.
|
|
83
77
|
scheduleTaskInterval(
|
|
@@ -97,11 +91,18 @@ export class Messenger {
|
|
|
97
91
|
return;
|
|
98
92
|
}
|
|
99
93
|
this._closed = true;
|
|
94
|
+
// Disposing the context tears down every still-active subscription — each `listen` registers its
|
|
95
|
+
// transport unsubscribe via `onDispose`, and a handle unsubscribed manually has already cleared
|
|
96
|
+
// its registration. The offline/online cycle keeps the messenger open (only signaling toggles),
|
|
97
|
+
// so subscriptions are torn down only on a real close.
|
|
100
98
|
await this._ctx.dispose();
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
async sendMessage(ctx: Context,
|
|
101
|
+
async sendMessage(ctx: Context, message: Message): Promise<void> {
|
|
104
102
|
invariant(!this._closed, 'Closed');
|
|
103
|
+
const { author, recipient, payload } = message;
|
|
104
|
+
// Messenger provides reliable point-to-point delivery; broadcasts are not routed here.
|
|
105
|
+
invariant(recipient, 'Recipient is required');
|
|
105
106
|
const messageContext = this._ctx.derive();
|
|
106
107
|
|
|
107
108
|
const reliablePayload: ReliablePayload = {
|
|
@@ -175,22 +176,39 @@ export class Messenger {
|
|
|
175
176
|
onMessage: OnMessage;
|
|
176
177
|
}): Promise<ListeningHandle> {
|
|
177
178
|
invariant(!this._closed, 'Closed');
|
|
178
|
-
|
|
179
|
-
await this._signalManager.subscribeMessages(peer);
|
|
180
|
-
let listeners: Set<OnMessage> | undefined;
|
|
181
179
|
invariant(peer.peerKey, 'Peer key is required');
|
|
180
|
+
const peerKey = peer.peerKey;
|
|
181
|
+
|
|
182
|
+
// Multiplexing is owned by the signal manager. The messenger keeps only reliable delivery
|
|
183
|
+
// (ACK/dedup) and payloadType routing, and passes the transport unsubscribe back through the
|
|
184
|
+
// handle. The unsubscribe is also registered on the messenger context so `close` tears every
|
|
185
|
+
// subscription down; a handle unsubscribed manually clears that registration first.
|
|
186
|
+
const unsubscribe = await this._signalManager.subscribeMessages({
|
|
187
|
+
peer,
|
|
188
|
+
onMessage: (message) => {
|
|
189
|
+
// Subscriptions can outlive a close (they are torn down on dispose, and survive offline/online
|
|
190
|
+
// cycles), so ignore late deliveries and never let a handler rejection escape as unhandled.
|
|
191
|
+
if (this._closed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
log('received message', { from: message.author });
|
|
195
|
+
void this._handleMessage(message).catch((err) => log.catch(err));
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const clearDispose = this._ctx.onDispose(unsubscribe);
|
|
182
199
|
|
|
200
|
+
let listeners: Set<OnMessage> | undefined;
|
|
183
201
|
if (!payloadType) {
|
|
184
|
-
listeners = this._defaultListeners.get(
|
|
202
|
+
listeners = this._defaultListeners.get(peerKey);
|
|
185
203
|
if (!listeners) {
|
|
186
204
|
listeners = new Set();
|
|
187
|
-
this._defaultListeners.set(
|
|
205
|
+
this._defaultListeners.set(peerKey, listeners);
|
|
188
206
|
}
|
|
189
207
|
} else {
|
|
190
|
-
listeners = this._listeners.get({ peerId:
|
|
208
|
+
listeners = this._listeners.get({ peerId: peerKey, payloadType });
|
|
191
209
|
if (!listeners) {
|
|
192
210
|
listeners = new Set();
|
|
193
|
-
this._listeners.set({ peerId:
|
|
211
|
+
this._listeners.set({ peerId: peerKey, payloadType }, listeners);
|
|
194
212
|
}
|
|
195
213
|
}
|
|
196
214
|
|
|
@@ -198,7 +216,9 @@ export class Messenger {
|
|
|
198
216
|
|
|
199
217
|
return {
|
|
200
218
|
unsubscribe: async () => {
|
|
219
|
+
clearDispose();
|
|
201
220
|
listeners!.delete(onMessage);
|
|
221
|
+
await unsubscribe();
|
|
202
222
|
},
|
|
203
223
|
};
|
|
204
224
|
}
|
|
@@ -238,8 +258,10 @@ export class Messenger {
|
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
|
|
241
|
-
private async _handleReliablePayload(
|
|
261
|
+
private async _handleReliablePayload(message: Message): Promise<void> {
|
|
262
|
+
const { author, recipient, payload } = message;
|
|
242
263
|
invariant(payload.type_url === 'dxos.mesh.messaging.ReliablePayload');
|
|
264
|
+
invariant(recipient, 'Recipient is required');
|
|
243
265
|
const reliablePayload: ReliablePayload = ReliablePayload.decode(payload.value, { preserveAny: true });
|
|
244
266
|
|
|
245
267
|
log('handling message', { messageId: reliablePayload.messageId });
|
|
@@ -299,9 +321,11 @@ export class Messenger {
|
|
|
299
321
|
}
|
|
300
322
|
|
|
301
323
|
private async _callListeners(message: Message): Promise<void> {
|
|
324
|
+
const { recipient } = message;
|
|
325
|
+
invariant(recipient?.peerKey, 'Peer key is required');
|
|
326
|
+
const peerKey = recipient.peerKey;
|
|
302
327
|
{
|
|
303
|
-
|
|
304
|
-
const defaultListenerMap = this._defaultListeners.get(message.recipient.peerKey);
|
|
328
|
+
const defaultListenerMap = this._defaultListeners.get(peerKey);
|
|
305
329
|
if (defaultListenerMap) {
|
|
306
330
|
for (const listener of defaultListenerMap) {
|
|
307
331
|
await listener(message);
|
|
@@ -311,7 +335,7 @@ export class Messenger {
|
|
|
311
335
|
|
|
312
336
|
{
|
|
313
337
|
const listenerMap = this._listeners.get({
|
|
314
|
-
peerId:
|
|
338
|
+
peerId: peerKey,
|
|
315
339
|
payloadType: message.payload.type_url,
|
|
316
340
|
});
|
|
317
341
|
if (listenerMap) {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Context } from '@dxos/context';
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
|
|
10
|
+
import { type Message, type PeerInfo } from '../signal-methods';
|
|
11
|
+
import { TestEdgeMesh } from '../testing';
|
|
12
|
+
import { EdgeSignalManager } from './edge-signal-manager';
|
|
13
|
+
|
|
14
|
+
// DX-1125: exercise the broadcast pub/sub paths of EdgeSignalManager against an in-memory edge.
|
|
15
|
+
|
|
16
|
+
const TRACE_TAG = 'type:status.update';
|
|
17
|
+
|
|
18
|
+
const payload = (value: number[]) => ({ type_url: 'dxos.compute.TraceMessage', value: new Uint8Array(value) });
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A capture sink for a single subscription. Delivered messages are classified by shape: a broadcast
|
|
22
|
+
* carries `tags` and no `recipient`; a point-to-point message carries a `recipient` (DX-1125).
|
|
23
|
+
*/
|
|
24
|
+
const capture = () => {
|
|
25
|
+
const messages: Message[] = [];
|
|
26
|
+
const broadcasts: Message[] = [];
|
|
27
|
+
const onMessage = (message: Message) => {
|
|
28
|
+
if (message.recipient != null) {
|
|
29
|
+
messages.push(message);
|
|
30
|
+
} else {
|
|
31
|
+
broadcasts.push(message);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return { messages, broadcasts, onMessage };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wire an EdgeSignalManager to a mesh connection and join `topic`. Returns the manager and its peer
|
|
39
|
+
* info; subscriptions (with their capture sinks) are created per-test.
|
|
40
|
+
*/
|
|
41
|
+
const setupPeer = async (mesh: TestEdgeMesh, topic: PublicKey, name: string) => {
|
|
42
|
+
const peer: PeerInfo = { peerKey: PublicKey.random().toHex(), identityDid: `did:test:${name}` };
|
|
43
|
+
const connection = mesh.createConnection({ peerKey: peer.peerKey!, identityDid: peer.identityDid! });
|
|
44
|
+
const manager = new EdgeSignalManager({ edgeConnection: connection as any });
|
|
45
|
+
await manager.open();
|
|
46
|
+
await manager.join(Context.default(), { topic, peer });
|
|
47
|
+
return { manager, peer };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// A broadcast addresses its target swarm via `author.swarmKey` (DX-1125).
|
|
51
|
+
const broadcast = (peer: PeerInfo, topic: PublicKey, tags: string[], value: number[]) => ({
|
|
52
|
+
author: { ...peer, swarmKey: topic.toHex() },
|
|
53
|
+
tags,
|
|
54
|
+
payload: payload(value),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('EdgeSignalManager broadcast (DX-1125)', () => {
|
|
58
|
+
test('delivers a tag broadcast to a subscriber whose tags intersect', async ({ expect }) => {
|
|
59
|
+
const mesh = new TestEdgeMesh();
|
|
60
|
+
const topic = PublicKey.random();
|
|
61
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
62
|
+
const subscriber = await setupPeer(mesh, topic, 'subscriber');
|
|
63
|
+
|
|
64
|
+
const sink = capture();
|
|
65
|
+
await subscriber.manager.subscribeMessages({ peer: subscriber.peer, tags: [TRACE_TAG], onMessage: sink.onMessage });
|
|
66
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [1, 2, 3]));
|
|
67
|
+
|
|
68
|
+
expect(sink.broadcasts).toHaveLength(1);
|
|
69
|
+
expect(sink.broadcasts[0].tags).toContain(TRACE_TAG);
|
|
70
|
+
expect(sink.broadcasts[0].author.peerKey).toBe(publisher.peer.peerKey);
|
|
71
|
+
expect([...sink.broadcasts[0].payload.value]).toEqual([1, 2, 3]);
|
|
72
|
+
// Broadcasts are not point-to-point messages.
|
|
73
|
+
expect(sink.messages).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('does not deliver broadcasts whose tags do not intersect the subscription', async ({ expect }) => {
|
|
77
|
+
const mesh = new TestEdgeMesh();
|
|
78
|
+
const topic = PublicKey.random();
|
|
79
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
80
|
+
const subscriber = await setupPeer(mesh, topic, 'subscriber');
|
|
81
|
+
|
|
82
|
+
const sink = capture();
|
|
83
|
+
await subscriber.manager.subscribeMessages({
|
|
84
|
+
peer: subscriber.peer,
|
|
85
|
+
tags: ['type:operation.input'],
|
|
86
|
+
onMessage: sink.onMessage,
|
|
87
|
+
});
|
|
88
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [9]));
|
|
89
|
+
|
|
90
|
+
expect(sink.broadcasts).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('excludes the author from its own broadcast', async ({ expect }) => {
|
|
94
|
+
const mesh = new TestEdgeMesh();
|
|
95
|
+
const topic = PublicKey.random();
|
|
96
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
97
|
+
|
|
98
|
+
// The publisher is also a subscriber for the same tag.
|
|
99
|
+
const sink = capture();
|
|
100
|
+
await publisher.manager.subscribeMessages({ peer: publisher.peer, tags: [TRACE_TAG], onMessage: sink.onMessage });
|
|
101
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [1]));
|
|
102
|
+
|
|
103
|
+
expect(sink.broadcasts).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('re-establishes the subscription across reconnect', async ({ expect }) => {
|
|
107
|
+
const mesh = new TestEdgeMesh();
|
|
108
|
+
const topic = PublicKey.random();
|
|
109
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
110
|
+
const subscriber = await setupPeer(mesh, topic, 'subscriber');
|
|
111
|
+
|
|
112
|
+
const sink = capture();
|
|
113
|
+
await subscriber.manager.subscribeMessages({ peer: subscriber.peer, tags: [TRACE_TAG], onMessage: sink.onMessage });
|
|
114
|
+
// Simulate a reconnect: the manager re-joins swarms and re-sends its subscription.
|
|
115
|
+
await (subscriber.manager as any)._rejoinAllSwarms();
|
|
116
|
+
|
|
117
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [7]));
|
|
118
|
+
|
|
119
|
+
expect(sink.broadcasts).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('one consumer unsubscribing does not clobber another consumer of the same tag', async ({ expect }) => {
|
|
123
|
+
const mesh = new TestEdgeMesh();
|
|
124
|
+
const topic = PublicKey.random();
|
|
125
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
126
|
+
const subscriber = await setupPeer(mesh, topic, 'subscriber');
|
|
127
|
+
|
|
128
|
+
// Two consumers on the same client share the manager's tag subscription (e.g. trace-progress and
|
|
129
|
+
// a story module both watching status updates). The bug (DX-1125): one consumer's teardown
|
|
130
|
+
// wholesale-cleared the shared tag set, so the other went silent. The refcounted per-subscription
|
|
131
|
+
// teardown releases only the tags that subscription added.
|
|
132
|
+
const first = capture();
|
|
133
|
+
const second = capture();
|
|
134
|
+
await subscriber.manager.subscribeMessages({
|
|
135
|
+
peer: subscriber.peer,
|
|
136
|
+
tags: [TRACE_TAG],
|
|
137
|
+
onMessage: first.onMessage,
|
|
138
|
+
});
|
|
139
|
+
const unsubscribeSecond = await subscriber.manager.subscribeMessages({
|
|
140
|
+
peer: subscriber.peer,
|
|
141
|
+
tags: [TRACE_TAG, 'space:test-space'],
|
|
142
|
+
onMessage: second.onMessage,
|
|
143
|
+
});
|
|
144
|
+
await unsubscribeSecond();
|
|
145
|
+
|
|
146
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [5]));
|
|
147
|
+
|
|
148
|
+
// The first consumer's registration of TRACE_TAG survives; the released space tag does not.
|
|
149
|
+
expect(first.broadcasts).toHaveLength(1);
|
|
150
|
+
expect([...first.broadcasts[0].payload.value]).toEqual([5]);
|
|
151
|
+
|
|
152
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, ['space:test-space'], [6]));
|
|
153
|
+
expect(first.broadcasts).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('releasing the last registration of a tag stops delivery', async ({ expect }) => {
|
|
157
|
+
const mesh = new TestEdgeMesh();
|
|
158
|
+
const topic = PublicKey.random();
|
|
159
|
+
const publisher = await setupPeer(mesh, topic, 'publisher');
|
|
160
|
+
const subscriber = await setupPeer(mesh, topic, 'subscriber');
|
|
161
|
+
|
|
162
|
+
const sink = capture();
|
|
163
|
+
const unsubscribe = await subscriber.manager.subscribeMessages({
|
|
164
|
+
peer: subscriber.peer,
|
|
165
|
+
tags: [TRACE_TAG],
|
|
166
|
+
onMessage: sink.onMessage,
|
|
167
|
+
});
|
|
168
|
+
await unsubscribe();
|
|
169
|
+
|
|
170
|
+
await publisher.manager.sendMessage(Context.default(), broadcast(publisher.peer, topic, [TRACE_TAG], [8]));
|
|
171
|
+
|
|
172
|
+
expect(sink.broadcasts).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('still delivers point-to-point messages', async ({ expect }) => {
|
|
176
|
+
const mesh = new TestEdgeMesh();
|
|
177
|
+
const topic = PublicKey.random();
|
|
178
|
+
const sender = await setupPeer(mesh, topic, 'sender');
|
|
179
|
+
const recipient = await setupPeer(mesh, topic, 'recipient');
|
|
180
|
+
|
|
181
|
+
const sink = capture();
|
|
182
|
+
await recipient.manager.subscribeMessages({ peer: recipient.peer, onMessage: sink.onMessage });
|
|
183
|
+
await sender.manager.sendMessage(Context.default(), {
|
|
184
|
+
author: sender.peer,
|
|
185
|
+
recipient: recipient.peer,
|
|
186
|
+
payload: payload([4, 2]),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(sink.messages).toHaveLength(1);
|
|
190
|
+
expect(sink.messages[0].author.peerKey).toBe(sender.peer.peerKey);
|
|
191
|
+
expect(sink.broadcasts).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -24,16 +24,38 @@ import {
|
|
|
24
24
|
import { type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
25
25
|
import { ComplexMap, ComplexSet } from '@dxos/util';
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type Message,
|
|
29
|
+
type PeerInfo,
|
|
30
|
+
PeerInfoHash,
|
|
31
|
+
type SubscribeMessagesParams,
|
|
32
|
+
type SwarmEvent,
|
|
33
|
+
type UnsubscribeCallback,
|
|
34
|
+
} from '../signal-methods';
|
|
28
35
|
import { type SignalManager } from './signal-manager';
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A single message subscription registered on an {@link EdgeSignalManager} (DX-1125). Point-to-point
|
|
39
|
+
* delivery matches `peerKey`; broadcast delivery matches any intersection with `tags`.
|
|
40
|
+
*/
|
|
41
|
+
type MessageSubscription = {
|
|
42
|
+
peerKey: string;
|
|
43
|
+
tags: Set<string>;
|
|
44
|
+
onMessage: (message: Message) => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
30
47
|
export class EdgeSignalManager extends Resource implements SignalManager {
|
|
31
48
|
/**
|
|
32
49
|
* @deprecated
|
|
33
50
|
*/
|
|
34
51
|
public swarmEvent = new Event<SwarmEvent>();
|
|
35
52
|
public swarmState = new Event<SwarmResponse>();
|
|
36
|
-
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Active message subscriptions. Routing is encapsulated here (DX-1125): each incoming message is
|
|
56
|
+
* dispatched to every subscription it matches, and each subscription owns its own teardown.
|
|
57
|
+
*/
|
|
58
|
+
private readonly _subscriptions = new Set<MessageSubscription>();
|
|
37
59
|
|
|
38
60
|
/**
|
|
39
61
|
* Swarm key -> { peer: <own state payload>, joinedPeers: <state of swarm> }.
|
|
@@ -44,6 +66,15 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
44
66
|
{ lastState?: Uint8Array; joinedPeers: ComplexSet<PeerInfo> }
|
|
45
67
|
>(PublicKey.hash);
|
|
46
68
|
|
|
69
|
+
/**
|
|
70
|
+
* OR-subscription tag refcounts for broadcast messages (DX-1125). One count per distinct tag across
|
|
71
|
+
* all subscribers, so one consumer's unsubscribe releases only its own registration rather than
|
|
72
|
+
* clobbering another consumer's identical subscription. The effective tag set (the keys) is shared
|
|
73
|
+
* across all joined swarms; the edge fans out any broadcast whose tags intersect it. Re-sent on
|
|
74
|
+
* reconnect and whenever a swarm is (re-)joined.
|
|
75
|
+
*/
|
|
76
|
+
private readonly _subscribedTags = new Map<string, number>();
|
|
77
|
+
|
|
47
78
|
private readonly _edgeConnection: EdgeConnection;
|
|
48
79
|
|
|
49
80
|
constructor({ edgeConnection }: { edgeConnection: EdgeConnection }) {
|
|
@@ -89,6 +120,11 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
89
120
|
payload: { action: SwarmRequestAction.JOIN, swarmKeys: [topic.toHex()] },
|
|
90
121
|
}),
|
|
91
122
|
);
|
|
123
|
+
|
|
124
|
+
// Re-establish any broadcast subscription on the newly-joined swarm (DX-1125).
|
|
125
|
+
if (this._subscribedTags.size > 0) {
|
|
126
|
+
await this._sendSubscription(ctx);
|
|
127
|
+
}
|
|
92
128
|
}
|
|
93
129
|
|
|
94
130
|
async leave(ctx: Context, { topic, peer }: { topic: PublicKey; peer: PeerInfo }): Promise<void> {
|
|
@@ -134,10 +170,16 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
async sendMessage(ctx: Context, message: Message): Promise<void> {
|
|
137
|
-
|
|
173
|
+
const { author, recipient, tags, payload } = message;
|
|
174
|
+
// Exactly one of point-to-point (`recipient`) or broadcast (`tags`) delivery (DX-1125). A broadcast
|
|
175
|
+
// carries its target swarm in `author.swarmKey` and is published with no `target`; the edge fans it
|
|
176
|
+
// out to every peer whose subscription tags intersect.
|
|
177
|
+
invariant((recipient == null) !== !tags?.length, 'Exactly one of `recipient` or `tags` must be set');
|
|
178
|
+
|
|
179
|
+
if (!this._matchSelfPeerInfo(author)) {
|
|
138
180
|
// NOTE: Could only join swarm with the same peer info as the edge connection.
|
|
139
181
|
log.warn('ignoring author on send request', {
|
|
140
|
-
author
|
|
182
|
+
author,
|
|
141
183
|
expected: { peerKey: this._edgeConnection.peerKey, identityDid: this._edgeConnection.identityDid },
|
|
142
184
|
});
|
|
143
185
|
}
|
|
@@ -146,19 +188,79 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
146
188
|
ctx,
|
|
147
189
|
protocol.createMessage(bufWkt.AnySchema, {
|
|
148
190
|
serviceId: EdgeService.SIGNAL,
|
|
149
|
-
source:
|
|
150
|
-
target: [
|
|
151
|
-
|
|
191
|
+
source: author,
|
|
192
|
+
target: recipient != null ? [recipient] : undefined,
|
|
193
|
+
tags,
|
|
194
|
+
payload: { typeUrl: payload.type_url, value: payload.value },
|
|
152
195
|
}),
|
|
153
196
|
);
|
|
154
197
|
}
|
|
155
198
|
|
|
156
|
-
async subscribeMessages(
|
|
157
|
-
|
|
199
|
+
async subscribeMessages({ peer, tags = [], onMessage }: SubscribeMessagesParams): Promise<UnsubscribeCallback> {
|
|
200
|
+
const subscription: MessageSubscription = { peerKey: peer.peerKey, tags: new Set(tags), onMessage };
|
|
201
|
+
this._subscriptions.add(subscription);
|
|
202
|
+
|
|
203
|
+
// Point-to-point delivery needs no edge registration (the edge relays targeted messages to this
|
|
204
|
+
// peer's socket). Only tag broadcasts require an OR-subscription registered on the swarm (DX-1125),
|
|
205
|
+
// refcounted so one subscriber's teardown does not clobber another's identical tags.
|
|
206
|
+
let changed = false;
|
|
207
|
+
for (const tag of subscription.tags) {
|
|
208
|
+
const count = this._subscribedTags.get(tag) ?? 0;
|
|
209
|
+
this._subscribedTags.set(tag, count + 1);
|
|
210
|
+
if (count === 0) {
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (changed) {
|
|
215
|
+
await this._sendSubscription(this._ctx);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return async () => {
|
|
219
|
+
this._subscriptions.delete(subscription);
|
|
220
|
+
// Release only this subscription's tag registrations; a tag stays live while another holds it.
|
|
221
|
+
let released = false;
|
|
222
|
+
for (const tag of subscription.tags) {
|
|
223
|
+
const count = this._subscribedTags.get(tag);
|
|
224
|
+
if (count === undefined) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (count > 1) {
|
|
228
|
+
this._subscribedTags.set(tag, count - 1);
|
|
229
|
+
} else {
|
|
230
|
+
this._subscribedTags.delete(tag);
|
|
231
|
+
released = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (released) {
|
|
235
|
+
await this._sendSubscription(this._ctx);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
158
238
|
}
|
|
159
239
|
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Send the current broadcast tag subscription to every joined swarm (DX-1125). An empty tag set
|
|
242
|
+
* clears the subscription on the edge.
|
|
243
|
+
*/
|
|
244
|
+
private async _sendSubscription(ctx: Context): Promise<void> {
|
|
245
|
+
const swarmKeys = Array.from(this._swarmPeers.keys()).map((topic) => topic.toHex());
|
|
246
|
+
if (swarmKeys.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await this._edgeConnection.send(
|
|
250
|
+
ctx,
|
|
251
|
+
protocol.createMessage(SwarmRequestSchema, {
|
|
252
|
+
serviceId: EdgeService.SWARM,
|
|
253
|
+
source: {
|
|
254
|
+
peerKey: this._edgeConnection.peerKey,
|
|
255
|
+
identityDid: this._edgeConnection.identityDid,
|
|
256
|
+
},
|
|
257
|
+
payload: {
|
|
258
|
+
action: SwarmRequestAction.SUBSCRIBE,
|
|
259
|
+
swarmKeys,
|
|
260
|
+
subscribeTags: Array.from(this._subscribedTags.keys()),
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
162
264
|
}
|
|
163
265
|
|
|
164
266
|
private _onMessage(message: EdgeMessage): void {
|
|
@@ -215,10 +317,21 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
215
317
|
invariant(protocol.getPayloadType(message) === bufWkt.AnySchema.typeName, 'Wrong payload type');
|
|
216
318
|
const payload = protocol.getPayload(message, bufWkt.AnySchema);
|
|
217
319
|
invariant(message.source, 'source is missing');
|
|
320
|
+
|
|
321
|
+
// Broadcasts (DX-1125) carry tags and no target; point-to-point messages carry exactly one target.
|
|
322
|
+
if ((message.tags?.length ?? 0) > 0 && (message.target?.length ?? 0) === 0) {
|
|
323
|
+
this._deliver({
|
|
324
|
+
author: message.source,
|
|
325
|
+
tags: message.tags ?? [],
|
|
326
|
+
payload: { type_url: payload.typeUrl, value: payload.value },
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
218
331
|
invariant(message.target, 'target is missing');
|
|
219
332
|
invariant(message.target.length === 1, 'target should have exactly one item');
|
|
220
333
|
|
|
221
|
-
this.
|
|
334
|
+
this._deliver({
|
|
222
335
|
author: message.source,
|
|
223
336
|
recipient: message.target[0],
|
|
224
337
|
payload: {
|
|
@@ -228,6 +341,22 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
228
341
|
});
|
|
229
342
|
}
|
|
230
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Route an incoming message to matching subscriptions (DX-1125): point-to-point by recipient
|
|
346
|
+
* `peerKey`, broadcasts by tag intersection.
|
|
347
|
+
*/
|
|
348
|
+
private _deliver(message: Message): void {
|
|
349
|
+
for (const subscription of this._subscriptions) {
|
|
350
|
+
if (message.recipient != null) {
|
|
351
|
+
if (subscription.peerKey === message.recipient.peerKey) {
|
|
352
|
+
subscription.onMessage(message);
|
|
353
|
+
}
|
|
354
|
+
} else if (message.tags?.some((tag) => subscription.tags.has(tag))) {
|
|
355
|
+
subscription.onMessage(message);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
231
360
|
private _matchSelfPeerInfo(peer: PeerInfo): boolean {
|
|
232
361
|
return Boolean(
|
|
233
362
|
peer && (peer.peerKey === this._edgeConnection.peerKey || peer.identityDid === this._edgeConnection.identityDid),
|
|
@@ -246,6 +375,10 @@ export class EdgeSignalManager extends Resource implements SignalManager {
|
|
|
246
375
|
},
|
|
247
376
|
});
|
|
248
377
|
}
|
|
378
|
+
// Re-establish the broadcast subscription across the rejoined swarms (DX-1125).
|
|
379
|
+
if (this._subscribedTags.size > 0) {
|
|
380
|
+
await this._sendSubscription(this._ctx);
|
|
381
|
+
}
|
|
249
382
|
}
|
|
250
383
|
}
|
|
251
384
|
|