@dxos/messaging 0.10.0 → 0.11.1

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
@@ -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 { type Message, type PeerInfo, PeerInfoHash, type SignalStatus, type SwarmEvent } from '../signal-methods';
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
- readonly onMessage = new Event<Message>();
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
- const remote = this._context.connections.get(recipient);
167
- if (!remote) {
168
- log.warn('recipient is not subscribed for messages', { author, recipient });
169
- return;
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
- if (remote._ctx.disposed) {
173
- log.warn('recipient is disposed', { author, recipient });
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
- remote._freezeTrigger
228
+ this._freezeTrigger
178
229
  .wait()
179
230
  .then(() => {
180
- if (remote._ctx.disposed) {
181
- log.warn('recipient is disposed', { author, recipient });
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
- remote.onMessage.emit({ author, recipient, payload });
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 { type DevicesService, type IdentityService } from '@dxos/protocols/proto/dxos/client/services';
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().subscribe((idqr) => {
20
- if (!idqr?.identity?.identityKey) {
21
- log('empty response from identity service', { idqr });
22
- return;
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
- setTag('identityKey', idqr.identity.identityKey.truncate());
31
+ setTag('identityKey', idqr.identity.identityKey.truncate());
32
+ },
26
33
  });
27
34
 
28
- devicesService.queryDevices().subscribe((dqr) => {
29
- if (!dqr || !dqr.devices || dqr.devices.length === 0) {
30
- log('empty response from device service', { device: dqr });
31
- return;
32
- }
33
- invariant(dqr, 'empty response from device service');
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
- const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);
36
- if (!thisDevice) {
37
- log('no current device', { device: dqr });
38
- return;
39
- }
40
- setTag('deviceKey', thisDevice.deviceKey.truncate());
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
  };
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Event } from '@dxos/async';
6
- import { type Context, type Lifecycle } from '@dxos/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 peer.
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 from peer.
73
- * @deprecated
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
- // TODO(burdon): Return unsubscribe function. Encapsulate callback/routing here.
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
  }
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './test-builder';
6
+ export * from './test-edge-mesh';
6
7
  export * from './test-peer';
7
8
  export * from './test-messages';
8
9
  export * from './utils';