@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { sleep } from '@dxos/async';
|
|
8
|
+
import { Context } from '@dxos/context';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
|
|
11
|
+
import { type Message, type PeerInfo } from '../signal-methods';
|
|
12
|
+
import { MemorySignalManager, MemorySignalManagerContext } from './memory-signal-manager';
|
|
13
|
+
|
|
14
|
+
// Exercises the subscription/routing behavior of the in-memory signal manager: point-to-point routing
|
|
15
|
+
// by recipient, broadcast routing by tag intersection (DX-1125), fan-out, and unsubscribe lifecycle.
|
|
16
|
+
|
|
17
|
+
const payload = (value: number[]) => ({ type_url: 'dxos.test.Message', value: new Uint8Array(value) });
|
|
18
|
+
|
|
19
|
+
const randomPeer = (name: string): PeerInfo => ({
|
|
20
|
+
peerKey: PublicKey.random().toHex(),
|
|
21
|
+
identityDid: `did:test:${name}`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A capture sink for a single subscription. Delivered messages are classified by shape: a
|
|
26
|
+
* point-to-point message carries a `recipient`; a broadcast carries `tags` and no `recipient`.
|
|
27
|
+
*/
|
|
28
|
+
const capture = () => {
|
|
29
|
+
const messages: Message[] = [];
|
|
30
|
+
const broadcasts: Message[] = [];
|
|
31
|
+
const onMessage = (message: Message) => {
|
|
32
|
+
if (message.recipient != null) {
|
|
33
|
+
messages.push(message);
|
|
34
|
+
} else {
|
|
35
|
+
broadcasts.push(message);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return { messages, broadcasts, onMessage };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const openManager = async (context: MemorySignalManagerContext): Promise<MemorySignalManager> => {
|
|
42
|
+
const manager = new MemorySignalManager(context);
|
|
43
|
+
await manager.open();
|
|
44
|
+
onTestFinished(() => manager.close());
|
|
45
|
+
return manager;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('MemorySignalManager', () => {
|
|
49
|
+
test('delivers a point-to-point message to the recipient subscription', async () => {
|
|
50
|
+
const context = new MemorySignalManagerContext();
|
|
51
|
+
const sender = await openManager(context);
|
|
52
|
+
const receiver = await openManager(context);
|
|
53
|
+
const senderPeer = randomPeer('sender');
|
|
54
|
+
const receiverPeer = randomPeer('receiver');
|
|
55
|
+
|
|
56
|
+
const sink = capture();
|
|
57
|
+
await receiver.subscribeMessages({ peer: receiverPeer, onMessage: sink.onMessage });
|
|
58
|
+
await sender.sendMessage(Context.default(), {
|
|
59
|
+
author: senderPeer,
|
|
60
|
+
recipient: receiverPeer,
|
|
61
|
+
payload: payload([1, 2, 3]),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect.poll(() => sink.messages.length).toBe(1);
|
|
65
|
+
expect(sink.messages[0].author.peerKey).toBe(senderPeer.peerKey);
|
|
66
|
+
expect([...sink.messages[0].payload.value]).toEqual([1, 2, 3]);
|
|
67
|
+
expect(sink.broadcasts).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('routes point-to-point messages by recipient', async () => {
|
|
71
|
+
const context = new MemorySignalManagerContext();
|
|
72
|
+
const sender = await openManager(context);
|
|
73
|
+
const receiverA = await openManager(context);
|
|
74
|
+
const receiverB = await openManager(context);
|
|
75
|
+
const peerA = randomPeer('a');
|
|
76
|
+
const peerB = randomPeer('b');
|
|
77
|
+
|
|
78
|
+
const sinkA = capture();
|
|
79
|
+
const sinkB = capture();
|
|
80
|
+
await receiverA.subscribeMessages({ peer: peerA, onMessage: sinkA.onMessage });
|
|
81
|
+
await receiverB.subscribeMessages({ peer: peerB, onMessage: sinkB.onMessage });
|
|
82
|
+
|
|
83
|
+
await sender.sendMessage(Context.default(), {
|
|
84
|
+
author: randomPeer('sender'),
|
|
85
|
+
recipient: peerB,
|
|
86
|
+
payload: payload([2]),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await expect.poll(() => sinkB.messages.length).toBe(1);
|
|
90
|
+
expect([...sinkB.messages[0].payload.value]).toEqual([2]);
|
|
91
|
+
// The message addressed to peerB is not delivered to peerA's subscription.
|
|
92
|
+
await sleep(20);
|
|
93
|
+
expect(sinkA.messages).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('fans a point-to-point message out to every subscription for the peer', async () => {
|
|
97
|
+
const context = new MemorySignalManagerContext();
|
|
98
|
+
const sender = await openManager(context);
|
|
99
|
+
const receiver = await openManager(context);
|
|
100
|
+
const receiverPeer = randomPeer('receiver');
|
|
101
|
+
|
|
102
|
+
const first = capture();
|
|
103
|
+
const second = capture();
|
|
104
|
+
await receiver.subscribeMessages({ peer: receiverPeer, onMessage: first.onMessage });
|
|
105
|
+
await receiver.subscribeMessages({ peer: receiverPeer, onMessage: second.onMessage });
|
|
106
|
+
await sender.sendMessage(Context.default(), {
|
|
107
|
+
author: randomPeer('sender'),
|
|
108
|
+
recipient: receiverPeer,
|
|
109
|
+
payload: payload([7]),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect.poll(() => first.messages.length).toBe(1);
|
|
113
|
+
await expect.poll(() => second.messages.length).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('stops delivery after unsubscribe', async () => {
|
|
117
|
+
const context = new MemorySignalManagerContext();
|
|
118
|
+
const sender = await openManager(context);
|
|
119
|
+
const receiver = await openManager(context);
|
|
120
|
+
const senderPeer = randomPeer('sender');
|
|
121
|
+
const receiverPeer = randomPeer('receiver');
|
|
122
|
+
|
|
123
|
+
const sink = capture();
|
|
124
|
+
const unsubscribe = await receiver.subscribeMessages({ peer: receiverPeer, onMessage: sink.onMessage });
|
|
125
|
+
await sender.sendMessage(Context.default(), { author: senderPeer, recipient: receiverPeer, payload: payload([1]) });
|
|
126
|
+
await expect.poll(() => sink.messages.length).toBe(1);
|
|
127
|
+
|
|
128
|
+
await unsubscribe();
|
|
129
|
+
await sender.sendMessage(Context.default(), { author: senderPeer, recipient: receiverPeer, payload: payload([2]) });
|
|
130
|
+
await sleep(20);
|
|
131
|
+
expect(sink.messages).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('keeps delivering to remaining subscriptions after one unsubscribes', async () => {
|
|
135
|
+
const context = new MemorySignalManagerContext();
|
|
136
|
+
const sender = await openManager(context);
|
|
137
|
+
const receiver = await openManager(context);
|
|
138
|
+
const receiverPeer = randomPeer('receiver');
|
|
139
|
+
|
|
140
|
+
const first = capture();
|
|
141
|
+
const second = capture();
|
|
142
|
+
const unsubscribeFirst = await receiver.subscribeMessages({ peer: receiverPeer, onMessage: first.onMessage });
|
|
143
|
+
await receiver.subscribeMessages({ peer: receiverPeer, onMessage: second.onMessage });
|
|
144
|
+
|
|
145
|
+
// Releasing one subscription keeps the shared-context connection alive for the other.
|
|
146
|
+
await unsubscribeFirst();
|
|
147
|
+
await sender.sendMessage(Context.default(), {
|
|
148
|
+
author: randomPeer('sender'),
|
|
149
|
+
recipient: receiverPeer,
|
|
150
|
+
payload: payload([5]),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await expect.poll(() => second.messages.length).toBe(1);
|
|
154
|
+
expect(first.messages).toHaveLength(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('delivers a broadcast only to subscriptions whose tags intersect', async () => {
|
|
158
|
+
const context = new MemorySignalManagerContext();
|
|
159
|
+
const sender = await openManager(context);
|
|
160
|
+
const receiver = await openManager(context);
|
|
161
|
+
|
|
162
|
+
const matching = capture();
|
|
163
|
+
const nonMatching = capture();
|
|
164
|
+
await receiver.subscribeMessages({ peer: randomPeer('a'), tags: ['type:a'], onMessage: matching.onMessage });
|
|
165
|
+
await receiver.subscribeMessages({ peer: randomPeer('b'), tags: ['type:b'], onMessage: nonMatching.onMessage });
|
|
166
|
+
|
|
167
|
+
await sender.sendMessage(Context.default(), {
|
|
168
|
+
author: randomPeer('sender'),
|
|
169
|
+
tags: ['type:a'],
|
|
170
|
+
payload: payload([4, 2]),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect.poll(() => matching.broadcasts.length).toBe(1);
|
|
174
|
+
expect(matching.broadcasts[0].tags).toContain('type:a');
|
|
175
|
+
expect([...matching.broadcasts[0].payload.value]).toEqual([4, 2]);
|
|
176
|
+
// Broadcasts are not point-to-point, and the non-intersecting subscription gets nothing.
|
|
177
|
+
expect(matching.messages).toHaveLength(0);
|
|
178
|
+
await sleep(20);
|
|
179
|
+
expect(nonMatching.broadcasts).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('rejects a message that sets neither or both of recipient and tags', async () => {
|
|
183
|
+
const context = new MemorySignalManagerContext();
|
|
184
|
+
const sender = await openManager(context);
|
|
185
|
+
const author = randomPeer('sender');
|
|
186
|
+
|
|
187
|
+
// Neither recipient nor tags.
|
|
188
|
+
await expect(sender.sendMessage(Context.default(), { author, payload: payload([1]) })).rejects.toThrow();
|
|
189
|
+
// Both recipient and tags.
|
|
190
|
+
await expect(
|
|
191
|
+
sender.sendMessage(Context.default(), {
|
|
192
|
+
author,
|
|
193
|
+
recipient: randomPeer('receiver'),
|
|
194
|
+
tags: ['type:a'],
|
|
195
|
+
payload: payload([1]),
|
|
196
|
+
}),
|
|
197
|
+
).rejects.toThrow();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -13,9 +13,27 @@ import { type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
|
13
13
|
import { type QueryRequest } from '@dxos/protocols/proto/dxos/edge/signal';
|
|
14
14
|
import { ComplexMap, ComplexSet } from '@dxos/util';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type Message,
|
|
18
|
+
type PeerInfo,
|
|
19
|
+
PeerInfoHash,
|
|
20
|
+
type SignalStatus,
|
|
21
|
+
type SubscribeMessagesParams,
|
|
22
|
+
type SwarmEvent,
|
|
23
|
+
type UnsubscribeCallback,
|
|
24
|
+
} from '../signal-methods';
|
|
17
25
|
import { type SignalManager } from './signal-manager';
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A single message subscription registered on a {@link MemorySignalManager} (DX-1125). Point-to-point
|
|
29
|
+
* delivery matches `peerKey`; broadcast delivery matches any intersection with `tags`.
|
|
30
|
+
*/
|
|
31
|
+
type MessageSubscription = {
|
|
32
|
+
peerKey: string;
|
|
33
|
+
tags: Set<string>;
|
|
34
|
+
onMessage: (message: Message) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
19
37
|
/**
|
|
20
38
|
* Common signaling context that connects multiple MemorySignalManager instances.
|
|
21
39
|
*/
|
|
@@ -37,7 +55,11 @@ export class MemorySignalManager implements SignalManager {
|
|
|
37
55
|
readonly statusChanged = new Event<SignalStatus[]>();
|
|
38
56
|
readonly swarmEvent = new Event<SwarmEvent>();
|
|
39
57
|
|
|
40
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Active message subscriptions on this manager. Routing is encapsulated here (DX-1125): a delivered
|
|
60
|
+
* message is dispatched to every subscription it matches.
|
|
61
|
+
*/
|
|
62
|
+
private readonly _subscriptions = new Set<MessageSubscription>();
|
|
41
63
|
|
|
42
64
|
/** Will be used to emit SwarmEvents on .open() and .close() */
|
|
43
65
|
private _joinedSwarms = new ComplexSet<{ topic: PublicKey; peer: PeerInfo }>(
|
|
@@ -144,70 +166,88 @@ export class MemorySignalManager implements SignalManager {
|
|
|
144
166
|
throw new Error('Not implemented');
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
async sendMessage(
|
|
148
|
-
_ctx: Context,
|
|
149
|
-
{
|
|
150
|
-
author,
|
|
151
|
-
recipient,
|
|
152
|
-
payload,
|
|
153
|
-
}: {
|
|
154
|
-
author: PeerInfo;
|
|
155
|
-
recipient: PeerInfo;
|
|
156
|
-
payload: Any;
|
|
157
|
-
},
|
|
158
|
-
): Promise<void> {
|
|
159
|
-
log('send message', { author, recipient, ...dec(payload) });
|
|
160
|
-
|
|
161
|
-
invariant(recipient);
|
|
169
|
+
async sendMessage(_ctx: Context, message: Message): Promise<void> {
|
|
162
170
|
invariant(!this._ctx.disposed, 'Closed');
|
|
171
|
+
const { author, recipient, tags, payload } = message;
|
|
172
|
+
// Exactly one of point-to-point (`recipient`) or broadcast (`tags`) delivery (DX-1125).
|
|
173
|
+
invariant((recipient == null) !== !tags?.length, 'Exactly one of `recipient` or `tags` must be set');
|
|
163
174
|
|
|
164
175
|
await this._freezeTrigger.wait();
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
if (recipient != null) {
|
|
178
|
+
log('send message', { author, recipient, ...dec(payload) });
|
|
179
|
+
const remote = this._context.connections.get(recipient);
|
|
180
|
+
if (!remote) {
|
|
181
|
+
log.warn('recipient is not subscribed for messages', { author, recipient });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
remote._deliver(message);
|
|
185
|
+
} else {
|
|
186
|
+
// Broadcast: fan out to every subscriber in the shared context whose tags intersect.
|
|
187
|
+
log('broadcast message', { author, tags, ...dec(payload) });
|
|
188
|
+
for (const manager of new Set(this._context.connections.values())) {
|
|
189
|
+
manager._deliver(message);
|
|
190
|
+
}
|
|
170
191
|
}
|
|
192
|
+
}
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
async subscribeMessages({ peer, tags = [], onMessage }: SubscribeMessagesParams): Promise<UnsubscribeCallback> {
|
|
195
|
+
invariant(!this._ctx.disposed, 'Closed');
|
|
196
|
+
log('subscribing', { peer, tags });
|
|
197
|
+
const subscription: MessageSubscription = { peerKey: peer.peerKey, tags: new Set(tags), onMessage };
|
|
198
|
+
this._subscriptions.add(subscription);
|
|
199
|
+
this._context.connections.set(peer, this);
|
|
200
|
+
|
|
201
|
+
return async () => {
|
|
202
|
+
log('unsubscribing', { peer, tags });
|
|
203
|
+
this._subscriptions.delete(subscription);
|
|
204
|
+
// Drop the shared-context connection entry only once no subscription for this peer remains.
|
|
205
|
+
if (![...this._subscriptions].some((sub) => sub.peerKey === peer.peerKey)) {
|
|
206
|
+
this._context.connections.delete(peer);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
freeze(): void {
|
|
212
|
+
this._freezeTrigger.reset();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
unfreeze(): void {
|
|
216
|
+
this._freezeTrigger.wake();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Route a delivered message to this manager's matching subscriptions once it is unfrozen and open.
|
|
221
|
+
*/
|
|
222
|
+
private _deliver(message: Message): void {
|
|
223
|
+
if (this._ctx.disposed) {
|
|
224
|
+
log.warn('recipient is disposed', { message });
|
|
174
225
|
return;
|
|
175
226
|
}
|
|
176
227
|
|
|
177
|
-
|
|
228
|
+
this._freezeTrigger
|
|
178
229
|
.wait()
|
|
179
230
|
.then(() => {
|
|
180
|
-
if (
|
|
181
|
-
log.warn('recipient is disposed', {
|
|
231
|
+
if (this._ctx.disposed) {
|
|
232
|
+
log.warn('recipient is disposed', { message });
|
|
182
233
|
return;
|
|
183
234
|
}
|
|
184
235
|
|
|
185
|
-
log('receive message', { author, recipient, ...dec(payload) });
|
|
186
|
-
|
|
187
|
-
|
|
236
|
+
log('receive message', { author: message.author, recipient: message.recipient, ...dec(message.payload) });
|
|
237
|
+
for (const subscription of this._subscriptions) {
|
|
238
|
+
if (message.recipient != null) {
|
|
239
|
+
if (subscription.peerKey === message.recipient.peerKey) {
|
|
240
|
+
subscription.onMessage(message);
|
|
241
|
+
}
|
|
242
|
+
} else if (message.tags?.some((tag) => subscription.tags.has(tag))) {
|
|
243
|
+
subscription.onMessage(message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
188
246
|
})
|
|
189
247
|
.catch((err) => {
|
|
190
248
|
log.error('error while waiting for freeze', { err });
|
|
191
249
|
});
|
|
192
250
|
}
|
|
193
|
-
|
|
194
|
-
async subscribeMessages(peerInfo: PeerInfo): Promise<void> {
|
|
195
|
-
log('subscribing', { peerInfo });
|
|
196
|
-
this._context.connections.set(peerInfo, this);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async unsubscribeMessages(peerInfo: PeerInfo): Promise<void> {
|
|
200
|
-
log('unsubscribing', { peerInfo });
|
|
201
|
-
this._context.connections.delete(peerInfo);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
freeze(): void {
|
|
205
|
-
this._freezeTrigger.reset();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
unfreeze(): void {
|
|
209
|
-
this._freezeTrigger.wake();
|
|
210
|
-
}
|
|
211
251
|
}
|
|
212
252
|
const dec = (payload: Any) => {
|
|
213
253
|
if (!payload.type_url.endsWith('ReliablePayload')) {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Copyright 2020 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Context from 'effect/Context';
|
|
6
|
+
|
|
5
7
|
import { type Event } from '@dxos/async';
|
|
6
8
|
import { type Lifecycle } from '@dxos/context';
|
|
7
9
|
|
|
@@ -14,3 +16,8 @@ export interface SignalManager extends SignalMethods, Required<Lifecycle> {
|
|
|
14
16
|
statusChanged?: Event<SignalStatus[]>;
|
|
15
17
|
getStatus?: () => SignalStatus[];
|
|
16
18
|
}
|
|
19
|
+
|
|
20
|
+
export class SignalManagerService extends Context.Tag('@dxos/messaging/SignalManager')<
|
|
21
|
+
SignalManagerService,
|
|
22
|
+
SignalManager
|
|
23
|
+
>() {}
|
|
@@ -2,41 +2,50 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Runtime from 'effect/Runtime';
|
|
6
|
+
|
|
5
7
|
import { invariant } from '@dxos/invariant';
|
|
6
8
|
import { log } from '@dxos/log';
|
|
7
|
-
import {
|
|
9
|
+
import { subscribeStream } from '@dxos/protocols';
|
|
8
10
|
import { DeviceKind } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
+
import { type DevicesService, type IdentityService } from '@dxos/protocols/rpc';
|
|
9
12
|
|
|
10
13
|
export const setIdentityTags = ({
|
|
11
14
|
identityService,
|
|
12
15
|
devicesService,
|
|
16
|
+
runtime = Runtime.defaultRuntime,
|
|
13
17
|
setTag,
|
|
14
18
|
}: {
|
|
15
|
-
identityService: IdentityService;
|
|
16
|
-
devicesService: DevicesService;
|
|
19
|
+
identityService: IdentityService.Client;
|
|
20
|
+
devicesService: DevicesService.Client;
|
|
21
|
+
runtime?: Runtime.Runtime<never>;
|
|
17
22
|
setTag: (k: string, v: string) => void;
|
|
18
23
|
}) => {
|
|
19
|
-
identityService.queryIdentity()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
subscribeStream(runtime, identityService.IdentityService.queryIdentity(undefined), {
|
|
25
|
+
onData: (idqr) => {
|
|
26
|
+
if (!idqr?.identity?.identityKey) {
|
|
27
|
+
log('empty response from identity service', { idqr });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
setTag('identityKey', idqr.identity.identityKey.truncate());
|
|
32
|
+
},
|
|
26
33
|
});
|
|
27
34
|
|
|
28
|
-
devicesService.queryDevices()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
subscribeStream(runtime, devicesService.DevicesService.queryDevices(undefined), {
|
|
36
|
+
onData: (dqr) => {
|
|
37
|
+
if (!dqr || !dqr.devices || dqr.devices.length === 0) {
|
|
38
|
+
log('empty response from device service', { device: dqr });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
invariant(dqr, 'empty response from device service');
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);
|
|
44
|
+
if (!thisDevice) {
|
|
45
|
+
log('no current device', { device: dqr });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
setTag('deviceKey', thisDevice.deviceKey.truncate());
|
|
49
|
+
},
|
|
41
50
|
});
|
|
42
51
|
};
|
package/src/signal-methods.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Event } from '@dxos/async';
|
|
6
|
-
import { type Context
|
|
6
|
+
import { type Context } from '@dxos/context';
|
|
7
7
|
import { type Peer, type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
8
8
|
import {
|
|
9
9
|
type JoinRequest,
|
|
@@ -27,6 +27,33 @@ export type SignalStatus = {
|
|
|
27
27
|
lastStateChange: Date;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Parameters for {@link SignalMethods.subscribeMessages}.
|
|
32
|
+
*/
|
|
33
|
+
export type SubscribeMessagesParams = {
|
|
34
|
+
/**
|
|
35
|
+
* The subscribing peer. Point-to-point messages addressed to this `peerKey` are delivered.
|
|
36
|
+
*/
|
|
37
|
+
peer: PeerInfo;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OR-subscription tags (DX-1125). When provided, swarm broadcasts whose tags intersect this set are
|
|
41
|
+
* also delivered. Only meaningful for edge signaling; other transports ignore them.
|
|
42
|
+
*/
|
|
43
|
+
tags?: string[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Invoked for every message delivered to this subscription (point-to-point or matching broadcast).
|
|
47
|
+
*/
|
|
48
|
+
onMessage: (message: Message) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tears down a subscription created by {@link SignalMethods.subscribeMessages} and releases its
|
|
53
|
+
* transport resources (edge tag registration / receive stream). The subscriber owns this lifecycle.
|
|
54
|
+
*/
|
|
55
|
+
export type UnsubscribeCallback = () => Promise<void>;
|
|
56
|
+
|
|
30
57
|
/**
|
|
31
58
|
* Message routing interface.
|
|
32
59
|
*/
|
|
@@ -38,11 +65,6 @@ export interface SignalMethods {
|
|
|
38
65
|
*/
|
|
39
66
|
swarmEvent: Event<SwarmEvent>;
|
|
40
67
|
|
|
41
|
-
/**
|
|
42
|
-
* Emits when a message is received.
|
|
43
|
-
*/
|
|
44
|
-
onMessage: Event<Message>;
|
|
45
|
-
|
|
46
68
|
/**
|
|
47
69
|
* Emits when the swarm state changes.
|
|
48
70
|
*/
|
|
@@ -64,29 +86,18 @@ export interface SignalMethods {
|
|
|
64
86
|
query: (ctx: Context, params: QueryRequest) => Promise<SwarmResponse>;
|
|
65
87
|
|
|
66
88
|
/**
|
|
67
|
-
* Send message to
|
|
89
|
+
* Send a message. Point-to-point when `recipient` is set; a swarm broadcast (DX-1125) when `tags`
|
|
90
|
+
* are set — fanned out to every peer whose tag subscription intersects, with the target swarm taken
|
|
91
|
+
* from `author.swarmKey`. Exactly one of `recipient` / `tags` must be present. Broadcasts are only
|
|
92
|
+
* supported by edge signaling.
|
|
68
93
|
*/
|
|
69
94
|
sendMessage: (ctx: Context, message: Message) => Promise<void>;
|
|
70
95
|
|
|
71
96
|
/**
|
|
72
|
-
* Start receiving messages
|
|
73
|
-
*
|
|
97
|
+
* Start receiving messages for `peer`: its point-to-point messages, plus — when `tags` are provided
|
|
98
|
+
* (DX-1125) — swarm broadcasts whose tags intersect. Matching messages are routed to
|
|
99
|
+
* `params.onMessage`; routing and the transport stream are owned here. Returns a callback that tears
|
|
100
|
+
* the subscription down; the subscriber owns that lifecycle.
|
|
74
101
|
*/
|
|
75
|
-
|
|
76
|
-
subscribeMessages: (peer: PeerInfo) => Promise<void>;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Stop receiving messages from peer.
|
|
80
|
-
* @deprecated
|
|
81
|
-
*/
|
|
82
|
-
unsubscribeMessages: (peer: PeerInfo) => Promise<void>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Signaling client.
|
|
87
|
-
* TODO(mykola): Delete.
|
|
88
|
-
* @deprecated
|
|
89
|
-
*/
|
|
90
|
-
export interface SignalClientMethods extends SignalMethods, Required<Lifecycle> {
|
|
91
|
-
getStatus(): SignalStatus;
|
|
102
|
+
subscribeMessages: (params: SubscribeMessagesParams) => Promise<UnsubscribeCallback>;
|
|
92
103
|
}
|