@dxos/network-manager 2.33.9-dev.d70ac9ee → 2.33.9-dev.d7113edd

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 (73) hide show
  1. package/dist/browser-mocha/bundle.js +119276 -0
  2. package/dist/browser-mocha/main.js +27 -0
  3. package/dist/src/network-manager.d.ts.map +1 -1
  4. package/dist/src/network-manager.js +7 -6
  5. package/dist/src/network-manager.js.map +1 -1
  6. package/dist/src/proto/gen/dxos/mesh/signal.d.ts +21 -0
  7. package/dist/src/proto/gen/dxos/mesh/signal.d.ts.map +1 -1
  8. package/dist/src/proto/gen/index.d.ts +1 -0
  9. package/dist/src/proto/gen/index.d.ts.map +1 -1
  10. package/dist/src/proto/gen/index.js +1 -1
  11. package/dist/src/proto/gen/index.js.map +1 -1
  12. package/dist/src/protocol-factory.js +3 -3
  13. package/dist/src/protocol-factory.js.map +1 -1
  14. package/dist/src/signal/in-memory-signal-manager.js +5 -5
  15. package/dist/src/signal/in-memory-signal-manager.js.map +1 -1
  16. package/dist/src/signal/message-router.d.ts +16 -4
  17. package/dist/src/signal/message-router.d.ts.map +1 -1
  18. package/dist/src/signal/message-router.js +91 -17
  19. package/dist/src/signal/message-router.js.map +1 -1
  20. package/dist/src/signal/message-router.test.js +120 -13
  21. package/dist/src/signal/message-router.test.js.map +1 -1
  22. package/dist/src/signal/signal-client.d.ts +2 -1
  23. package/dist/src/signal/signal-client.d.ts.map +1 -1
  24. package/dist/src/signal/signal-client.js +26 -16
  25. package/dist/src/signal/signal-client.js.map +1 -1
  26. package/dist/src/signal/signal-client.test.js +4 -2
  27. package/dist/src/signal/signal-client.test.js.map +1 -1
  28. package/dist/src/signal/websocket-rpc.js +3 -3
  29. package/dist/src/signal/websocket-rpc.js.map +1 -1
  30. package/dist/src/signal/websocket-signal-manager.js +2 -2
  31. package/dist/src/signal/websocket-signal-manager.js.map +1 -1
  32. package/dist/src/swarm/connection.js +8 -8
  33. package/dist/src/swarm/connection.js.map +1 -1
  34. package/dist/src/swarm/swarm.js +10 -10
  35. package/dist/src/swarm/swarm.js.map +1 -1
  36. package/dist/src/swarm/swarm.test.js +2 -0
  37. package/dist/src/swarm/swarm.test.js.map +1 -1
  38. package/dist/src/testing/test-protocol.d.ts.map +1 -1
  39. package/dist/src/testing/test-protocol.js +3 -3
  40. package/dist/src/testing/test-protocol.js.map +1 -1
  41. package/dist/src/topology/fully-connected-topology.js +3 -3
  42. package/dist/src/topology/fully-connected-topology.js.map +1 -1
  43. package/dist/src/topology/mmst-topology.js +5 -5
  44. package/dist/src/topology/mmst-topology.js.map +1 -1
  45. package/dist/src/topology/star-topology.js +4 -4
  46. package/dist/src/topology/star-topology.js.map +1 -1
  47. package/dist/src/transport/in-memory-transport.js +2 -2
  48. package/dist/src/transport/in-memory-transport.js.map +1 -1
  49. package/dist/src/transport/webrtc-transport.js +3 -3
  50. package/dist/src/transport/webrtc-transport.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +14 -11
  53. package/src/network-manager.ts +2 -1
  54. package/src/proto/defs/dxos/mesh/signal.proto +13 -0
  55. package/src/proto/gen/dxos/mesh/signal.ts +21 -0
  56. package/src/proto/gen/index.ts +2 -1
  57. package/src/protocol-factory.ts +1 -1
  58. package/src/signal/in-memory-signal-manager.ts +1 -1
  59. package/src/signal/message-router.test.ts +154 -38
  60. package/src/signal/message-router.ts +107 -18
  61. package/src/signal/signal-client.test.ts +4 -2
  62. package/src/signal/signal-client.ts +42 -15
  63. package/src/signal/websocket-rpc.ts +1 -1
  64. package/src/signal/websocket-signal-manager.ts +1 -1
  65. package/src/swarm/connection.ts +1 -1
  66. package/src/swarm/swarm.test.ts +2 -0
  67. package/src/swarm/swarm.ts +2 -2
  68. package/src/testing/test-protocol.ts +1 -1
  69. package/src/topology/fully-connected-topology.ts +1 -1
  70. package/src/topology/mmst-topology.ts +1 -1
  71. package/src/topology/star-topology.ts +1 -1
  72. package/src/transport/in-memory-transport.ts +1 -1
  73. package/src/transport/webrtc-transport.ts +1 -1
@@ -13,9 +13,14 @@ message Message {
13
13
  PubKey id = 1;
14
14
  /// Receiver`s public key.
15
15
  PubKey remoteId = 2;
16
+ /// Swarm identefier.
16
17
  PubKey topic = 3;
18
+ /// Unique connection identifier.
17
19
  PubKey sessionId = 4;
20
+ /// Message payload.
18
21
  MessageData data = 5;
22
+ /// Unique message identifier. Used for Acknolegment and matching Anwers to Offers.
23
+ PubKey messageId = 6;
19
24
  }
20
25
 
21
26
  message MessageData {
@@ -23,6 +28,7 @@ message MessageData {
23
28
  Offer offer = 1;
24
29
  Answer answer = 2;
25
30
  Signal signal = 3;
31
+ Acknowledgement ack = 4;
26
32
  }
27
33
  }
28
34
 
@@ -31,8 +37,15 @@ message Offer {
31
37
 
32
38
  message Answer {
33
39
  bool accept = 1;
40
+ /// MessageId of the Offer being answered.
41
+ PubKey offerMessageId = 2;
34
42
  }
35
43
 
36
44
  message Signal {
37
45
  string json = 1;
38
46
  }
47
+
48
+ message Acknowledgement {
49
+ // MessageId of the Message being acknowledged.
50
+ PubKey messageId = 1;
51
+ }
@@ -12,20 +12,41 @@ export interface Message {
12
12
  * Receiver`s public key.
13
13
  */
14
14
  remoteId?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
15
+ /**
16
+ * Swarm identefier.
17
+ */
15
18
  topic?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
19
+ /**
20
+ * Unique connection identifier.
21
+ */
16
22
  sessionId?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
23
+ /**
24
+ * Message payload.
25
+ */
17
26
  data?: MessageData;
27
+ /**
28
+ * Unique message identifier. Used for Acknolegment and matching Anwers to Offers.
29
+ */
30
+ messageId?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
18
31
  }
19
32
  export interface MessageData {
20
33
  offer?: Offer;
21
34
  answer?: Answer;
22
35
  signal?: Signal;
36
+ ack?: Acknowledgement;
23
37
  }
24
38
  export interface Offer {
25
39
  }
26
40
  export interface Answer {
27
41
  accept?: boolean;
42
+ /**
43
+ * MessageId of the Offer being answered.
44
+ */
45
+ offerMessageId?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
28
46
  }
29
47
  export interface Signal {
30
48
  json?: string;
31
49
  }
50
+ export interface Acknowledgement {
51
+ messageId?: ReturnType<(typeof substitutions)["dxos.halo.keys.PubKey"]["decode"]>;
52
+ }
@@ -15,6 +15,7 @@ export interface TYPES {
15
15
  "dxos.halo.keys.KeyType": dxos_halo_keys.KeyType;
16
16
  "dxos.halo.keys.PrivKey": dxos_halo_keys.PrivKey;
17
17
  "dxos.halo.keys.PubKey": dxos_halo_keys.PubKey;
18
+ "dxos.mesh.signal.Acknowledgement": dxos_mesh_signal.Acknowledgement;
18
19
  "dxos.mesh.signal.Answer": dxos_mesh_signal.Answer;
19
20
  "dxos.mesh.signal.Message": dxos_mesh_signal.Message;
20
21
  "dxos.mesh.signal.MessageData": dxos_mesh_signal.MessageData;
@@ -24,5 +25,5 @@ export interface TYPES {
24
25
  }
25
26
  export interface SERVICES {
26
27
  }
27
- export const schemaJson = JSON.parse("{\"nested\":{\"dxos\":{\"nested\":{\"credentials\":{\"nested\":{\"Message\":{\"fields\":{\"payload\":{\"rule\":\"required\",\"type\":\"google.protobuf.Any\",\"id\":1,\"comment\":null}},\"comment\":null},\"SignedMessage\":{\"fields\":{\"signed\":{\"rule\":\"required\",\"type\":\"Signed\",\"id\":1,\"comment\":null},\"signatures\":{\"rule\":\"repeated\",\"type\":\"Signature\",\"id\":2,\"comment\":null}},\"nested\":{\"Signature\":{\"fields\":{\"key\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":1,\"comment\":null},\"signature\":{\"rule\":\"required\",\"type\":\"bytes\",\"id\":2,\"comment\":null},\"keyChain\":{\"type\":\"KeyChain\",\"id\":3,\"comment\":null}},\"comment\":null},\"Signed\":{\"fields\":{\"created\":{\"rule\":\"required\",\"type\":\"string\",\"id\":1,\"comment\":null},\"nonce\":{\"rule\":\"required\",\"type\":\"bytes\",\"id\":2,\"comment\":null},\"payload\":{\"rule\":\"required\",\"type\":\"google.protobuf.Any\",\"id\":10,\"comment\":null}},\"comment\":null}},\"comment\":\"A generic container message used whenever messages are signed (e.g. PartyCredential)\"}}},\"halo\":{\"nested\":{\"keys\":{\"nested\":{\"KeyChain\":{\"fields\":{\"publicKey\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":1,\"comment\":null},\"message\":{\"rule\":\"required\",\"type\":\"SignedMessage\",\"id\":2,\"comment\":null},\"parents\":{\"rule\":\"repeated\",\"type\":\"KeyChain\",\"id\":3,\"comment\":null}},\"comment\":null},\"KeyRecord\":{\"fields\":{\"type\":{\"rule\":\"required\",\"type\":\"KeyType\",\"id\":1,\"comment\":\"The `KeyType` type of the key. This is often unknown for keys from other sources.\"},\"publicKey\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":2,\"comment\":\"The public key as a Buffer (required).\"},\"secretKey\":{\"type\":\"PrivKey\",\"id\":3,\"comment\":\"The secret key as a Buffer (this will never be visible outside the Keyring).\"},\"hint\":{\"type\":\"bool\",\"id\":4,\"comment\":\"Is this key from a Greeting \\\"hint\\\"?\"},\"own\":{\"type\":\"bool\",\"id\":5,\"comment\":\"Determines if this is our key?\\nUsually true if `secretKey` is present; may be false for \\\"inception keys\\\" such as the Party key.\"},\"trusted\":{\"type\":\"bool\",\"id\":6,\"comment\":\"Is this key to be trusted?\"},\"added\":{\"type\":\"string\",\"id\":7,\"comment\":\"An RFC-3339 date/time string for when the key was added to the Keyring.\"},\"created\":{\"type\":\"string\",\"id\":8,\"comment\":\"An RFC-3339 date/time string for when the key was created.\"}},\"comment\":null},\"KeyRecordList\":{\"fields\":{\"keys\":{\"rule\":\"repeated\",\"type\":\"KeyRecord\",\"id\":1,\"comment\":null}},\"comment\":null},\"KeyType\":{\"values\":{\"UNKNOWN\":0,\"IDENTITY\":1,\"DEVICE\":2,\"PARTY\":3,\"FEED\":4,\"DXNS_ADDRESS\":5},\"comment\":null,\"comments\":{\"UNKNOWN\":null,\"IDENTITY\":null,\"DEVICE\":null,\"PARTY\":null,\"FEED\":null,\"DXNS_ADDRESS\":null}},\"PrivKey\":{\"fields\":{\"data\":{\"type\":\"bytes\",\"id\":1,\"comment\":null}},\"comment\":null},\"PubKey\":{\"fields\":{\"data\":{\"type\":\"bytes\",\"id\":1,\"comment\":null}},\"comment\":null}}}}},\"mesh\":{\"nested\":{\"signal\":{\"nested\":{\"Answer\":{\"fields\":{\"accept\":{\"type\":\"bool\",\"id\":1,\"comment\":null}},\"comment\":null},\"Message\":{\"fields\":{\"id\":{\"type\":\"PubKey\",\"id\":1,\"comment\":\"Sender's public key.\"},\"remoteId\":{\"type\":\"PubKey\",\"id\":2,\"comment\":\"Receiver`s public key.\"},\"topic\":{\"type\":\"PubKey\",\"id\":3,\"comment\":null},\"sessionId\":{\"type\":\"PubKey\",\"id\":4,\"comment\":null},\"data\":{\"type\":\"MessageData\",\"id\":5,\"comment\":null}},\"comment\":null},\"MessageData\":{\"oneofs\":{\"payload\":{\"oneof\":[\"offer\",\"answer\",\"signal\"],\"comment\":null}},\"fields\":{\"offer\":{\"type\":\"Offer\",\"id\":1,\"comment\":null},\"answer\":{\"type\":\"Answer\",\"id\":2,\"comment\":null},\"signal\":{\"type\":\"Signal\",\"id\":3,\"comment\":null}},\"comment\":null},\"Offer\":{\"fields\":{},\"comment\":null},\"Signal\":{\"fields\":{\"json\":{\"type\":\"string\",\"id\":1,\"comment\":null}},\"comment\":null}}}}}}},\"google\":{\"nested\":{\"protobuf\":{\"nested\":{\"Any\":{\"fields\":{\"type_url\":{\"type\":\"string\",\"id\":1},\"value\":{\"type\":\"bytes\",\"id\":2}},\"comment\":null}}}}}}}");
28
+ export const schemaJson = JSON.parse("{\"nested\":{\"dxos\":{\"nested\":{\"credentials\":{\"nested\":{\"Message\":{\"fields\":{\"payload\":{\"rule\":\"required\",\"type\":\"google.protobuf.Any\",\"id\":1,\"comment\":null}},\"comment\":null},\"SignedMessage\":{\"fields\":{\"signed\":{\"rule\":\"required\",\"type\":\"Signed\",\"id\":1,\"comment\":null},\"signatures\":{\"rule\":\"repeated\",\"type\":\"Signature\",\"id\":2,\"comment\":null}},\"nested\":{\"Signature\":{\"fields\":{\"key\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":1,\"comment\":null},\"signature\":{\"rule\":\"required\",\"type\":\"bytes\",\"id\":2,\"comment\":null},\"keyChain\":{\"type\":\"KeyChain\",\"id\":3,\"comment\":null}},\"comment\":null},\"Signed\":{\"fields\":{\"created\":{\"rule\":\"required\",\"type\":\"string\",\"id\":1,\"comment\":null},\"nonce\":{\"rule\":\"required\",\"type\":\"bytes\",\"id\":2,\"comment\":null},\"payload\":{\"rule\":\"required\",\"type\":\"google.protobuf.Any\",\"id\":10,\"comment\":null}},\"comment\":null}},\"comment\":\"A generic container message used whenever messages are signed (e.g. PartyCredential)\"}}},\"halo\":{\"nested\":{\"keys\":{\"nested\":{\"KeyChain\":{\"fields\":{\"publicKey\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":1,\"comment\":null},\"message\":{\"rule\":\"required\",\"type\":\"SignedMessage\",\"id\":2,\"comment\":null},\"parents\":{\"rule\":\"repeated\",\"type\":\"KeyChain\",\"id\":3,\"comment\":null}},\"comment\":null},\"KeyRecord\":{\"fields\":{\"type\":{\"rule\":\"required\",\"type\":\"KeyType\",\"id\":1,\"comment\":\"The `KeyType` type of the key. This is often unknown for keys from other sources.\"},\"publicKey\":{\"rule\":\"required\",\"type\":\"PubKey\",\"id\":2,\"comment\":\"The public key as a Buffer (required).\"},\"secretKey\":{\"type\":\"PrivKey\",\"id\":3,\"comment\":\"The secret key as a Buffer (this will never be visible outside the Keyring).\"},\"hint\":{\"type\":\"bool\",\"id\":4,\"comment\":\"Is this key from a Greeting \\\"hint\\\"?\"},\"own\":{\"type\":\"bool\",\"id\":5,\"comment\":\"Determines if this is our key?\\nUsually true if `secretKey` is present; may be false for \\\"inception keys\\\" such as the Party key.\"},\"trusted\":{\"type\":\"bool\",\"id\":6,\"comment\":\"Is this key to be trusted?\"},\"added\":{\"type\":\"string\",\"id\":7,\"comment\":\"An RFC-3339 date/time string for when the key was added to the Keyring.\"},\"created\":{\"type\":\"string\",\"id\":8,\"comment\":\"An RFC-3339 date/time string for when the key was created.\"}},\"comment\":null},\"KeyRecordList\":{\"fields\":{\"keys\":{\"rule\":\"repeated\",\"type\":\"KeyRecord\",\"id\":1,\"comment\":null}},\"comment\":null},\"KeyType\":{\"values\":{\"UNKNOWN\":0,\"IDENTITY\":1,\"DEVICE\":2,\"PARTY\":3,\"FEED\":4,\"DXNS_ADDRESS\":5},\"comment\":null,\"comments\":{\"UNKNOWN\":null,\"IDENTITY\":null,\"DEVICE\":null,\"PARTY\":null,\"FEED\":null,\"DXNS_ADDRESS\":null}},\"PrivKey\":{\"fields\":{\"data\":{\"type\":\"bytes\",\"id\":1,\"comment\":null}},\"comment\":null},\"PubKey\":{\"fields\":{\"data\":{\"type\":\"bytes\",\"id\":1,\"comment\":null}},\"comment\":null}}}}},\"mesh\":{\"nested\":{\"signal\":{\"nested\":{\"Acknowledgement\":{\"fields\":{\"messageId\":{\"type\":\"PubKey\",\"id\":1,\"comment\":null}},\"comment\":null},\"Answer\":{\"fields\":{\"accept\":{\"type\":\"bool\",\"id\":1,\"comment\":null},\"offerMessageId\":{\"type\":\"PubKey\",\"id\":2,\"comment\":\"MessageId of the Offer being answered.\"}},\"comment\":null},\"Message\":{\"fields\":{\"id\":{\"type\":\"PubKey\",\"id\":1,\"comment\":\"Sender's public key.\"},\"remoteId\":{\"type\":\"PubKey\",\"id\":2,\"comment\":\"Receiver`s public key.\"},\"topic\":{\"type\":\"PubKey\",\"id\":3,\"comment\":\"Swarm identefier.\"},\"sessionId\":{\"type\":\"PubKey\",\"id\":4,\"comment\":\"Unique connection identifier.\"},\"data\":{\"type\":\"MessageData\",\"id\":5,\"comment\":\"Message payload.\"},\"messageId\":{\"type\":\"PubKey\",\"id\":6,\"comment\":\"Unique message identifier. Used for Acknolegment and matching Anwers to Offers.\"}},\"comment\":null},\"MessageData\":{\"oneofs\":{\"payload\":{\"oneof\":[\"offer\",\"answer\",\"signal\",\"ack\"],\"comment\":null}},\"fields\":{\"offer\":{\"type\":\"Offer\",\"id\":1,\"comment\":null},\"answer\":{\"type\":\"Answer\",\"id\":2,\"comment\":null},\"signal\":{\"type\":\"Signal\",\"id\":3,\"comment\":null},\"ack\":{\"type\":\"Acknowledgement\",\"id\":4,\"comment\":null}},\"comment\":null},\"Offer\":{\"fields\":{},\"comment\":null},\"Signal\":{\"fields\":{\"json\":{\"type\":\"string\",\"id\":1,\"comment\":null}},\"comment\":null}}}}}}},\"google\":{\"nested\":{\"protobuf\":{\"nested\":{\"Any\":{\"fields\":{\"type_url\":{\"type\":\"string\",\"id\":1},\"value\":{\"type\":\"bytes\",\"id\":2}},\"comment\":null}}}}}}}");
28
29
  export const schema = Schema.fromJson<TYPES, SERVICES>(schemaJson, substitutions);
@@ -2,8 +2,8 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import assert from 'assert';
6
5
  import debug from 'debug';
6
+ import assert from 'node:assert';
7
7
 
8
8
  import { discoveryKey } from '@dxos/crypto';
9
9
  import { Extension, Protocol } from '@dxos/mesh-protocol';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import assert from 'assert';
5
+ import assert from 'node:assert';
6
6
 
7
7
  import { Event } from '@dxos/async';
8
8
  import { PublicKey } from '@dxos/protocols';
@@ -40,11 +40,15 @@ describe('MessageRouter', () => {
40
40
  await broker1.stop();
41
41
  });
42
42
 
43
- const createSignalClientAndMessageRouter = async (
44
- signalApiUrl: string,
45
- onSignal: (msg: Message) => Promise<void> = (async () => {}) as any,
46
- onOffer: (msg: Message) => Promise<Answer> = async () => ({ accept: true })
47
- ) => {
43
+ const createSignalClientAndMessageRouter = async ({
44
+ signalApiUrl,
45
+ onSignal = (async () => { }) as any,
46
+ onOffer = async () => ({ accept: true })
47
+ }: {
48
+ signalApiUrl: string;
49
+ onSignal?: (msg: Message) => Promise<void>;
50
+ onOffer?: (msg: Message) => Promise<Answer>;
51
+ }) => {
48
52
 
49
53
  // eslint-disable-next-line prefer-const
50
54
  let api: SignalClient;
@@ -54,6 +58,7 @@ describe('MessageRouter', () => {
54
58
  onSignal: onSignal,
55
59
  onOffer: onOffer
56
60
  });
61
+ afterTest(() => router.destroy());
57
62
 
58
63
  api = new SignalClient(
59
64
  signalApiUrl,
@@ -70,8 +75,8 @@ describe('MessageRouter', () => {
70
75
 
71
76
  test('signaling between 2 clients', async () => {
72
77
  const signalMock1 = mockFn<(msg: Message) => Promise<void>>().resolvesTo();
73
- const { api: api1 } = await createSignalClientAndMessageRouter(signalApiUrl1, signalMock1);
74
- const { api: api2, router: router2 } = await createSignalClientAndMessageRouter(signalApiUrl1);
78
+ const { api: api1 } = await createSignalClientAndMessageRouter({ signalApiUrl: signalApiUrl1, onSignal: signalMock1 });
79
+ const { api: api2, router: router2 } = await createSignalClientAndMessageRouter({ signalApiUrl: signalApiUrl1 });
75
80
 
76
81
  await api1.join(topic, peer1);
77
82
  await api2.join(topic, peer2);
@@ -92,15 +97,19 @@ describe('MessageRouter', () => {
92
97
 
93
98
  test('offer/answer', async () => {
94
99
  const { api: api1, router: router1 } = await createSignalClientAndMessageRouter(
95
- signalApiUrl1,
96
- (async () => {}) as any,
97
- async () => ({ accept: true })
98
- );
100
+ {
101
+ signalApiUrl: signalApiUrl1,
102
+ onSignal: (async () => { }) as any,
103
+ onOffer:
104
+ async () => ({ accept: true })
105
+ });
99
106
  const { api: api2 } = await createSignalClientAndMessageRouter(
100
- signalApiUrl1,
101
- (async () => {}) as any,
102
- async () => ({ accept: true })
103
- );
107
+ {
108
+ signalApiUrl: signalApiUrl1,
109
+ onSignal: (async () => { }) as any,
110
+ onOffer:
111
+ async () => ({ accept: true })
112
+ });
104
113
 
105
114
  await api1.join(topic, peer1);
106
115
  await api2.join(topic, peer2);
@@ -112,28 +121,34 @@ describe('MessageRouter', () => {
112
121
  topic,
113
122
  data: { offer: { } }
114
123
  });
115
- expect(answer).toEqual({ accept: true });
124
+ expect(answer.accept).toEqual(true);
116
125
  }).timeout(5_000);
117
126
 
118
127
  test('signaling between 3 clients', async () => {
119
128
  const signalMock1 = mockFn<(msg: Message) => Promise<void>>().resolvesTo();
120
129
  const { api: api1, router: router1 } = await createSignalClientAndMessageRouter(
121
- signalApiUrl1,
122
- signalMock1,
123
- async () => ({ accept: true })
124
- );
130
+ {
131
+ signalApiUrl: signalApiUrl1,
132
+ onSignal: signalMock1,
133
+ onOffer:
134
+ async () => ({ accept: true })
135
+ });
125
136
  const signalMock2 = mockFn<(msg: Message) => Promise<void>>().resolvesTo();
126
137
  const { api: api2, router: router2 } = await createSignalClientAndMessageRouter(
127
- signalApiUrl1,
128
- signalMock2,
129
- async () => ({ accept: true })
130
- );
138
+ {
139
+ signalApiUrl: signalApiUrl1,
140
+ onSignal: signalMock2,
141
+ onOffer:
142
+ async () => ({ accept: true })
143
+ });
131
144
  const signalMock3 = mockFn<(msg: Message) => Promise<void>>().resolvesTo();
132
145
  const { api: api3, router: router3 } = await createSignalClientAndMessageRouter(
133
- signalApiUrl1,
134
- signalMock3,
135
- async () => ({ accept: true })
136
- );
146
+ {
147
+ signalApiUrl: signalApiUrl1,
148
+ onSignal: signalMock3,
149
+ onOffer:
150
+ async () => ({ accept: true })
151
+ });
137
152
 
138
153
  await api1.join(topic, peer1);
139
154
  await api2.join(topic, peer2);
@@ -182,15 +197,19 @@ describe('MessageRouter', () => {
182
197
 
183
198
  test('two offers', async () => {
184
199
  const { api: api1, router: router1 } = await createSignalClientAndMessageRouter(
185
- signalApiUrl1,
186
- (async () => {}) as any,
187
- async () => ({ accept: true })
188
- );
200
+ {
201
+ signalApiUrl: signalApiUrl1,
202
+ onSignal: (async () => { }) as any,
203
+ onOffer:
204
+ async () => ({ accept: true })
205
+ });
189
206
  const { api: api2, router: router2 } = await createSignalClientAndMessageRouter(
190
- signalApiUrl1,
191
- (async () => {}) as any,
192
- async () => ({ accept: true })
193
- );
207
+ {
208
+ signalApiUrl: signalApiUrl1,
209
+ onSignal: (async () => { }) as any,
210
+ onOffer:
211
+ async () => ({ accept: true })
212
+ });
194
213
 
195
214
  await api1.join(topic, peer1);
196
215
  await api2.join(topic, peer2);
@@ -203,7 +222,7 @@ describe('MessageRouter', () => {
203
222
  topic,
204
223
  data: { offer: {} }
205
224
  });
206
- expect(answer1).toEqual({ accept: true });
225
+ expect(answer1.accept).toEqual(true);
207
226
 
208
227
  // sending offer from peer2 to peer1.
209
228
  const answer2 = await router2.offer({
@@ -213,6 +232,103 @@ describe('MessageRouter', () => {
213
232
  topic,
214
233
  data: { offer: {} }
215
234
  });
216
- expect(answer2).toEqual({ accept: true });
235
+ expect(answer2.accept).toEqual(true);
217
236
  }).timeout(5_000);
237
+
238
+ describe('Reliability', () => {
239
+ const setup = ({
240
+ onSignal1 = async () => { },
241
+ onSignal2 = async () => { },
242
+ // Imitates signal network disruptions (e. g. message doubling, ).
243
+ messageDisruption = msg => [msg]
244
+ }: {
245
+ onSignal1?: (msg: Message) => Promise<void>;
246
+ onSignal2?: (msg: Message) => Promise<void>;
247
+ messageDisruption?: (msg: Message) => Message[];
248
+ }): {mr1: MessageRouter; mr2: MessageRouter} => {
249
+
250
+ const mr1: MessageRouter = new MessageRouter({
251
+ sendMessage: async msg => messageDisruption(msg).forEach(msg => mr2.receiveMessage(msg)),
252
+ onOffer: async () => ({ accept: true }),
253
+ onSignal: onSignal1
254
+ });
255
+ afterTest(() => mr1.destroy());
256
+
257
+ const mr2: MessageRouter = new MessageRouter({
258
+ sendMessage: async msg => messageDisruption(msg).forEach(msg => mr1.receiveMessage(msg)),
259
+ onOffer: async () => ({ accept: true }),
260
+ onSignal: onSignal2
261
+ });
262
+ afterTest(() => mr1.destroy());
263
+
264
+ return { mr1, mr2 };
265
+ };
266
+
267
+ test('signaling with non reliable connection', async () => {
268
+ // Simulate unreliable connection.
269
+ // Only each 3rd message is sent.
270
+ let i = 0;
271
+ const unreliableConnection = (msg: Message): Message[] => {
272
+ i++;
273
+ if (i % 3 !== 0) {
274
+ return [msg];
275
+ }
276
+ return [];
277
+ };
278
+
279
+ const received: Message[] = [];
280
+ const signalMock1 = async (msg: Message) => {
281
+ received.push(msg);
282
+ };
283
+
284
+ const { mr2 } = await setup({
285
+ onSignal1: signalMock1,
286
+ messageDisruption: unreliableConnection
287
+ });
288
+
289
+ // Sending 3 messages.
290
+ // Setup sends messages directly to between. So we don`t need to specify any ids.
291
+ Array(3).fill(0).forEach(async () => {
292
+ await mr2.signal({
293
+ id: PublicKey.random(),
294
+ remoteId: PublicKey.random(),
295
+ sessionId: PublicKey.random(),
296
+ topic: PublicKey.random(),
297
+ data: { signal: { json: 'asd' } }
298
+ });
299
+ });
300
+ // expect to receive 3 messages.
301
+ await waitForExpect(() => {
302
+ expect(received.length).toEqual(3);
303
+ }, 4_000);
304
+ }).timeout(5_000);
305
+
306
+ test('ignoring doubled messages', async () => {
307
+ // Message got doubled going through signal network.
308
+ const doublingMessage = (msg: Message) => [msg, msg];
309
+
310
+ const received: Message[] = [];
311
+ const signalMock1 = async (msg: Message) => {
312
+ received.push(msg);
313
+ };
314
+
315
+ const { mr2 } = setup({
316
+ onSignal1: signalMock1,
317
+ messageDisruption: doublingMessage
318
+ });
319
+
320
+ // sending message.
321
+ await mr2.signal({
322
+ id: PublicKey.random(),
323
+ remoteId: PublicKey.random(),
324
+ sessionId: PublicKey.random(),
325
+ topic: PublicKey.random(),
326
+ data: { signal: { json: 'asd' } }
327
+ });
328
+ // expect to receive 1 message.
329
+ await waitForExpect(() => {
330
+ expect(received.length).toEqual(1);
331
+ }, 4_000);
332
+ }).timeout(5_000);
333
+ });
218
334
  });
@@ -3,9 +3,10 @@
3
3
  //
4
4
 
5
5
  import assert from 'assert';
6
+ import debug from 'debug';
6
7
 
7
8
  import { PublicKey } from '@dxos/protocols';
8
- import { ComplexMap } from '@dxos/util';
9
+ import { ComplexMap, ComplexSet, exponentialBackoffInterval, SubscriptionGroup } from '@dxos/util';
9
10
 
10
11
  import { Answer, Message } from '../proto/gen/dxos/mesh/signal';
11
12
  import { SignalMessaging } from './signal-manager';
@@ -16,11 +17,14 @@ interface OfferRecord {
16
17
  }
17
18
 
18
19
  interface MessageRouterOptions {
19
- onSignal: (message: Message) => Promise<void>;
20
- sendMessage: (message: Message) => Promise<void>;
21
- onOffer: (message: Message) => Promise<Answer>;
20
+ onSignal?: (message: Message) => Promise<void>;
21
+ sendMessage?: (message: Message) => Promise<void>;
22
+ onOffer?: (message: Message) => Promise<Answer>;
23
+ retryDelay?: number;
24
+ timeout?: number;
22
25
  }
23
26
 
27
+ const log = debug('dxos:network-manager:message-router');
24
28
  /**
25
29
  * Adds offer/answer RPC and reliable messaging.
26
30
  */
@@ -31,59 +35,144 @@ export class MessageRouter implements SignalMessaging {
31
35
  private readonly _sendMessage: (message: Message) => Promise<void>;
32
36
  private readonly _onOffer: (message: Message) => Promise<Answer>;
33
37
 
38
+ private readonly _onAckCallbacks = new ComplexMap<PublicKey, () => void>(key => key.toHex());
39
+ private readonly _receivedMessages = new ComplexSet<PublicKey>(key => key.toHex());
40
+ private readonly _retryDelay: number;
41
+ private readonly _timeout: number;
42
+
43
+ private readonly _subscriptions = new SubscriptionGroup();
44
+
34
45
  constructor ({
35
46
  sendMessage,
36
47
  onSignal,
37
- onOffer
38
- }: MessageRouterOptions) {
48
+ onOffer,
49
+ retryDelay = 100,
50
+ timeout = 3000
51
+ }: MessageRouterOptions = {}) {
52
+ assert(sendMessage);
39
53
  this._sendMessage = sendMessage;
54
+ assert(onSignal);
40
55
  this._onSignal = onSignal;
56
+ assert(onOffer);
41
57
  this._onOffer = onOffer;
58
+ this._retryDelay = retryDelay;
59
+ this._timeout = timeout;
42
60
  }
43
61
 
44
62
  async receiveMessage (message: Message): Promise<void> {
63
+ log(`receive message: ${JSON.stringify(message)}`);
64
+ if (!message.data?.ack) {
65
+ if (this._receivedMessages.has(message.messageId!)) {
66
+ return;
67
+ }
68
+
69
+ this._receivedMessages.add(message.messageId!);
70
+ await this._sendAcknowledgement(message);
71
+ }
72
+
45
73
  if (message.data?.offer) {
46
74
  await this._handleOffer(message);
47
75
  } else if (message.data?.answer) {
48
76
  await this._resolveAnswers(message);
49
77
  } else if (message.data?.signal) {
50
- await this._onSignal(message);
78
+ await this._handleSignal(message);
79
+ } else if (message.data?.ack) {
80
+ await this._handleAcknowledgement(message);
51
81
  }
52
82
  }
53
83
 
54
84
  async signal (message: Message): Promise<void> {
55
85
  assert(message.data?.signal);
56
- await this._sendMessage(message);
86
+ await this._sendReliableMessage(message);
57
87
  }
58
88
 
59
89
  async offer (message: Message): Promise<Answer> {
90
+ message.messageId = PublicKey.random();
60
91
  const promise = new Promise<Answer>((resolve, reject) => {
61
- assert(message.sessionId);
62
- this._offerRecords.set(message.sessionId, { resolve, reject });
92
+ this._offerRecords.set(message.messageId!, { resolve, reject });
93
+ return this._sendReliableMessage(message);
63
94
  });
64
- await this._sendMessage(message);
65
95
  return promise;
66
96
  }
67
97
 
98
+ private async _sendReliableMessage (message: Message): Promise<PublicKey> {
99
+ // Setting unique messageId if it not specified yet.
100
+ message.messageId = message.messageId ?? PublicKey.random();
101
+ log(`sent message: ${JSON.stringify(message)}`);
102
+
103
+ // Setting retry interval if signal was not acknowledged.
104
+ const cancelRetry = exponentialBackoffInterval(async () => {
105
+ log(`retrying message: ${JSON.stringify(message)}`);
106
+ await this._sendMessage(message);
107
+ }, this._retryDelay);
108
+
109
+ const timeout = setTimeout(() => {
110
+ log('Signal was not delivered!');
111
+ this._onAckCallbacks.delete(message.messageId!);
112
+ cancelRetry();
113
+ }, this._timeout);
114
+
115
+ assert(!this._onAckCallbacks.has(message.messageId!));
116
+ this._onAckCallbacks.set(message.messageId, () => {
117
+ this._onAckCallbacks.delete(message.messageId!);
118
+ cancelRetry();
119
+ clearTimeout(timeout);
120
+ });
121
+ this._subscriptions.push(cancelRetry);
122
+ this._subscriptions.push(() => clearTimeout(timeout));
123
+
124
+ await this._sendMessage(message);
125
+ return message.messageId;
126
+ }
127
+
68
128
  private async _resolveAnswers (message: Message): Promise<void> {
69
- assert(message.sessionId);
70
- const offerRecord = this._offerRecords.get(message.sessionId);
129
+ assert(message.data?.answer?.offerMessageId, 'No offerMessageId');
130
+ const offerRecord = this._offerRecords.get(message.data.answer.offerMessageId);
71
131
  if (offerRecord) {
72
- this._offerRecords.delete(message.sessionId);
73
- assert(message.data?.answer);
132
+ this._offerRecords.delete(message.data.answer.offerMessageId);
133
+ assert(message.data?.answer, 'No Answer');
134
+ log(`resolving answer with ${message.data.answer}`);
74
135
  offerRecord.resolve(message.data.answer);
75
136
  }
76
137
  }
77
138
 
78
139
  private async _handleOffer (message: Message): Promise<void> {
79
140
  const answer = await this._onOffer(message);
80
- const answerMessage = {
141
+ answer.offerMessageId = message.messageId;
142
+ const answerMessage: Message = {
143
+ id: message.remoteId,
144
+ remoteId: message.id,
145
+ topic: message.topic,
146
+ sessionId: message.sessionId,
147
+ data: { answer }
148
+ };
149
+ await this._sendReliableMessage(answerMessage);
150
+ }
151
+
152
+ private async _handleSignal (message: Message): Promise<void> {
153
+ assert(message.messageId);
154
+ await this._onSignal(message);
155
+ }
156
+
157
+ private async _handleAcknowledgement (message: Message): Promise<void> {
158
+ assert(message.data?.ack?.messageId);
159
+ this._onAckCallbacks.get(message.data.ack.messageId)?.();
160
+ }
161
+
162
+ private async _sendAcknowledgement (message: Message): Promise<void> {
163
+ assert(message.messageId);
164
+ const ackMessage = {
81
165
  id: message.remoteId,
82
166
  remoteId: message.id,
83
167
  topic: message.topic,
84
168
  sessionId: message.sessionId,
85
- data: { answer: answer }
169
+ data: { ack: { messageId: message.messageId } }
86
170
  };
87
- await this._sendMessage(answerMessage);
171
+ log(`sent ack: ${JSON.stringify(ackMessage)}`);
172
+ await this._sendMessage(ackMessage);
173
+ }
174
+
175
+ destroy (): void {
176
+ this._subscriptions.unsubscribe();
88
177
  }
89
178
  }
@@ -61,7 +61,8 @@ describe('SignalApi', () => {
61
61
  remoteId: peer1,
62
62
  sessionId: PublicKey.random(),
63
63
  topic,
64
- data: { signal: { json: "foo: 'bar'" } }
64
+ data: { signal: { json: "foo: 'bar'" } },
65
+ messageId: undefined
65
66
  };
66
67
  await api2.signal(msg);
67
68
 
@@ -111,7 +112,8 @@ describe('SignalApi', () => {
111
112
  remoteId: peer1,
112
113
  sessionId: PublicKey.random(),
113
114
  topic,
114
- data: { signal: { json: 'bar' } }
115
+ data: { signal: { json: 'bar' } },
116
+ messageId: undefined
115
117
  };
116
118
  await api1.signal(msg);
117
119