@dxos/network-manager 0.1.55 → 0.1.56-main.09ca29d

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 (52) hide show
  1. package/dist/lib/browser/{chunk-IBLQYMBY.mjs → chunk-FC6CWDES.mjs} +823 -294
  2. package/dist/lib/browser/chunk-FC6CWDES.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +5 -1
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +2 -2
  6. package/dist/lib/node/index.cjs +843 -312
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +772 -301
  10. package/dist/lib/node/testing/index.cjs.map +4 -4
  11. package/dist/types/src/network-manager.d.ts +1 -1
  12. package/dist/types/src/network-manager.d.ts.map +1 -1
  13. package/dist/types/src/signal/swarm-messenger.d.ts.map +1 -1
  14. package/dist/types/src/swarm/connection-limiter.d.ts +28 -0
  15. package/dist/types/src/swarm/connection-limiter.d.ts.map +1 -0
  16. package/dist/types/src/swarm/connection-limiter.test.d.ts +2 -0
  17. package/dist/types/src/swarm/connection-limiter.test.d.ts.map +1 -0
  18. package/dist/types/src/swarm/connection.d.ts +1 -1
  19. package/dist/types/src/swarm/connection.d.ts.map +1 -1
  20. package/dist/types/src/swarm/connection.test.d.ts +2 -0
  21. package/dist/types/src/swarm/connection.test.d.ts.map +1 -0
  22. package/dist/types/src/swarm/index.d.ts +1 -0
  23. package/dist/types/src/swarm/index.d.ts.map +1 -1
  24. package/dist/types/src/swarm/peer.d.ts +3 -1
  25. package/dist/types/src/swarm/peer.d.ts.map +1 -1
  26. package/dist/types/src/swarm/swarm.d.ts +3 -1
  27. package/dist/types/src/swarm/swarm.d.ts.map +1 -1
  28. package/dist/types/src/topology/fully-connected-topology.d.ts.map +1 -1
  29. package/dist/types/src/topology/star-topology.d.ts.map +1 -1
  30. package/dist/types/src/transport/memory-transport.d.ts.map +1 -1
  31. package/dist/types/src/transport/webrtc-transport-proxy.d.ts.map +1 -1
  32. package/dist/types/src/transport/webrtc-transport-service.d.ts.map +1 -1
  33. package/dist/types/src/transport/webrtc-transport.d.ts.map +1 -1
  34. package/package.json +18 -18
  35. package/src/network-manager.ts +19 -6
  36. package/src/signal/swarm-messenger.ts +1 -2
  37. package/src/swarm/connection-limiter.test.ts +44 -0
  38. package/src/swarm/connection-limiter.ts +72 -0
  39. package/src/swarm/connection.test.ts +72 -0
  40. package/src/swarm/connection.ts +18 -5
  41. package/src/swarm/index.ts +1 -0
  42. package/src/swarm/peer.ts +46 -9
  43. package/src/swarm/swarm.test.ts +140 -90
  44. package/src/swarm/swarm.ts +4 -3
  45. package/src/topology/fully-connected-topology.ts +1 -2
  46. package/src/topology/mmst-topology.ts +1 -1
  47. package/src/topology/star-topology.ts +1 -2
  48. package/src/transport/memory-transport.ts +1 -1
  49. package/src/transport/webrtc-transport-proxy.ts +14 -12
  50. package/src/transport/webrtc-transport-service.ts +31 -8
  51. package/src/transport/webrtc-transport.ts +1 -1
  52. package/dist/lib/browser/chunk-IBLQYMBY.mjs.map +0 -7
package/src/swarm/peer.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { scheduleTask, synchronized } from '@dxos/async';
8
6
  import { Context } from '@dxos/context';
7
+ import { CancelledError } from '@dxos/errors';
8
+ import { invariant } from '@dxos/invariant';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { log } from '@dxos/log';
11
11
  import { Answer } from '@dxos/protocols/proto/dxos/mesh/swarm';
@@ -14,6 +14,7 @@ import { OfferMessage, SignalMessage, SignalMessenger } from '../signal';
14
14
  import { TransportFactory } from '../transport';
15
15
  import { WireProtocolProvider } from '../wire-protocol';
16
16
  import { Connection, ConnectionState } from './connection';
17
+ import { ConnectionLimiter } from './connection-limiter';
17
18
 
18
19
  interface PeerCallbacks {
19
20
  /**
@@ -80,7 +81,6 @@ export class Peer {
80
81
  public advertizing = false;
81
82
 
82
83
  public initiating = false;
83
-
84
84
  constructor(
85
85
  public readonly id: PublicKey,
86
86
  public readonly topic: PublicKey,
@@ -88,6 +88,7 @@ export class Peer {
88
88
  private readonly _signalMessaging: SignalMessenger,
89
89
  private readonly _protocolProvider: WireProtocolProvider,
90
90
  private readonly _transportFactory: TransportFactory,
91
+ private readonly _connectionLimiter: ConnectionLimiter,
91
92
  private readonly _callbacks: PeerCallbacks,
92
93
  ) {}
93
94
 
@@ -102,10 +103,11 @@ export class Peer {
102
103
  // Peer with the highest Id closes its connection, and accepts remote peer's offer.
103
104
  if (remoteId.toHex() < this.localPeerId.toHex()) {
104
105
  // TODO(burdon): Too verbose.
105
- log("closing local connection and accepting remote peer's offer", {
106
- id: this.id,
106
+ log('close local connection', {
107
+ localPeerId: this.id,
107
108
  topic: this.topic,
108
- peerId: this.localPeerId,
109
+ remotePeerId: this.localPeerId,
110
+ sessionId: this.connection?.sessionId,
109
111
  });
110
112
 
111
113
  if (this.connection) {
@@ -123,10 +125,15 @@ export class Peer {
123
125
  // Connection might have been already established.
124
126
  invariant(message.sessionId);
125
127
  const connection = this._createConnection(false, message.sessionId);
128
+
126
129
  try {
127
- connection.openConnection();
130
+ await this._connectionLimiter.connecting(message.sessionId);
131
+ await connection.openConnection();
128
132
  } catch (err: any) {
129
- log.warn('connection error', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, err });
133
+ if (!(err instanceof CancelledError)) {
134
+ log.warn('connection error', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, err });
135
+ }
136
+
130
137
  // Calls `onStateChange` with CLOSED state.
131
138
  await this.closeConnection();
132
139
  }
@@ -145,10 +152,13 @@ export class Peer {
145
152
  invariant(!this.connection, 'Already connected.');
146
153
  const sessionId = PublicKey.random();
147
154
  log('initiating...', { id: this.id, topic: this.topic, peerId: this.id, sessionId });
155
+
148
156
  const connection = this._createConnection(true, sessionId);
149
157
  this.initiating = true;
150
158
 
151
159
  try {
160
+ await this._connectionLimiter.connecting(sessionId);
161
+
152
162
  const answer = await this._signalMessaging.offer({
153
163
  author: this.localPeerId,
154
164
  recipient: this.id,
@@ -166,7 +176,7 @@ export class Peer {
166
176
  this._callbacks.onRejected();
167
177
  return;
168
178
  }
169
- connection.openConnection();
179
+ await connection.openConnection();
170
180
  this._callbacks.onAccepted();
171
181
  } catch (err: any) {
172
182
  log('initiation error', { err, topic: this.topic, peerId: this.localPeerId, remoteId: this.id });
@@ -214,6 +224,15 @@ export class Peer {
214
224
  this.availableToConnect = true;
215
225
  this._lastConnectionTime = Date.now();
216
226
  this._callbacks.onConnected();
227
+
228
+ this._connectionLimiter.doneConnecting(sessionId);
229
+ log.trace('dxos.mesh.connection.connected', {
230
+ topic: this.topic,
231
+ localPeerId: this.localPeerId,
232
+ remotePeerId: this.id,
233
+ sessionId,
234
+ initiator,
235
+ });
217
236
  break;
218
237
  }
219
238
 
@@ -221,6 +240,14 @@ export class Peer {
221
240
  log('connection closed', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, initiator });
222
241
  invariant(this.connection === connection, 'Connection mismatch (race condition).');
223
242
 
243
+ log.trace('dxos.mesh.connection.closed', {
244
+ topic: this.topic,
245
+ localPeerId: this.localPeerId,
246
+ remotePeerId: this.id,
247
+ sessionId,
248
+ initiator,
249
+ });
250
+
224
251
  if (this._lastConnectionTime && this._lastConnectionTime + CONNECTION_COUNTS_STABLE_AFTER < Date.now()) {
225
252
  // If we're closing the connection, and it has been connected for a while, reset the backoff.
226
253
  this._availableAfter = 0;
@@ -231,6 +258,7 @@ export class Peer {
231
258
 
232
259
  this.connection = undefined;
233
260
  this._callbacks.onDisconnected();
261
+ this._connectionLimiter.doneConnecting(sessionId);
234
262
 
235
263
  scheduleTask(
236
264
  this._connectionCtx!,
@@ -247,6 +275,14 @@ export class Peer {
247
275
  });
248
276
  connection.errors.handle((err) => {
249
277
  log.warn('connection error', { topic: this.topic, peerId: this.localPeerId, remoteId: this.id, initiator, err });
278
+ log.trace('dxos.mesh.connection.error', {
279
+ topic: this.topic,
280
+ localPeerId: this.localPeerId,
281
+ remotePeerId: this.id,
282
+ sessionId,
283
+ initiator,
284
+ err,
285
+ });
250
286
 
251
287
  // Calls `onStateChange` with CLOSED state.
252
288
  void this.closeConnection();
@@ -276,6 +312,7 @@ export class Peer {
276
312
  log('dropping signal message for non-existent connection', { message });
277
313
  return;
278
314
  }
315
+
279
316
  await this.connection.signal(message);
280
317
  }
281
318
 
@@ -2,31 +2,43 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- // @dxos/test platform=nodejs
6
-
7
- import { expect } from 'earljs';
5
+ import { expect } from 'chai';
8
6
 
9
7
  import { asyncTimeout, sleep } from '@dxos/async';
10
8
  import { PublicKey } from '@dxos/keys';
11
- import { log } from '@dxos/log';
12
- import { MemorySignalManager, MemorySignalManagerContext, Messenger } from '@dxos/messaging';
13
- import { afterTest, beforeEach, describe, test } from '@dxos/test';
9
+ import { MemorySignalManager, MemorySignalManagerContext, Messenger, SignalManager } from '@dxos/messaging';
10
+ import { afterTest, describe, test } from '@dxos/test';
11
+ import { ComplexSet } from '@dxos/util';
14
12
 
15
13
  import { TestWireProtocol } from '../testing/test-wire-protocol';
16
14
  import { FullyConnectedTopology } from '../topology';
17
15
  import { createWebRTCTransportFactory } from '../transport';
16
+ import { ConnectionState } from './connection';
17
+ import { ConnectionLimiter } from './connection-limiter';
18
18
  import { Swarm } from './swarm';
19
19
 
20
+ type TestPeer = {
21
+ swarm: Swarm;
22
+ peerId: PublicKey;
23
+ protocol: TestWireProtocol;
24
+ topic: PublicKey;
25
+ signalManager: SignalManager;
26
+ };
27
+
20
28
  describe('Swarm', () => {
21
29
  const context = new MemorySignalManagerContext();
22
- let signalManager: MemorySignalManager;
23
-
24
- beforeEach(() => {
25
- signalManager = new MemorySignalManager(context);
26
- afterTest(() => signalManager.close());
27
- });
28
30
 
29
- const setupSwarm = async ({ topic, peerId }: { topic: PublicKey; peerId: PublicKey }) => {
31
+ const setupSwarm = async ({
32
+ topic = PublicKey.random(),
33
+ peerId = PublicKey.random(),
34
+ connectionLimiter = new ConnectionLimiter(),
35
+ signalManager = new MemorySignalManager(context),
36
+ }: {
37
+ topic?: PublicKey;
38
+ peerId?: PublicKey;
39
+ connectionLimiter?: ConnectionLimiter;
40
+ signalManager?: SignalManager;
41
+ }): Promise<TestPeer> => {
30
42
  const protocol = new TestWireProtocol(peerId);
31
43
  const swarm = new Swarm(
32
44
  topic,
@@ -36,116 +48,154 @@ describe('Swarm', () => {
36
48
  new Messenger({ signalManager }),
37
49
  createWebRTCTransportFactory(),
38
50
  undefined,
51
+ connectionLimiter,
39
52
  );
40
53
 
41
54
  afterTest(async () => {
42
55
  await swarm.destroy();
56
+ await signalManager.close();
43
57
  });
44
58
 
45
59
  await swarm.open();
46
60
 
47
- return { swarm, protocol };
61
+ return { swarm, protocol, topic, peerId, signalManager };
48
62
  };
49
63
 
50
64
  test('connects two peers in a swarm', async () => {
51
65
  const topic = PublicKey.random();
52
- const peerId1 = PublicKey.random();
53
- const peerId2 = PublicKey.random();
54
66
 
55
- const { swarm: swarm1, protocol: protocol1 } = await setupSwarm({ topic, peerId: peerId1 });
67
+ const peer1 = await setupSwarm({ topic });
68
+ const peer2 = await setupSwarm({ topic });
56
69
 
57
- const { swarm: swarm2 } = await setupSwarm({ topic, peerId: peerId2 });
70
+ expect(peer1.swarm.connections.length).to.equal(0);
71
+ expect(peer2.swarm.connections.length).to.equal(0);
58
72
 
59
- expect(swarm1.connections.length).toEqual(0);
60
- expect(swarm2.connections.length).toEqual(0);
61
-
62
- const promise = Promise.all([
63
- asyncTimeout(swarm1.connected.waitForCount(1), 3000, new Error('Swarm1 connect timeout.')),
64
- asyncTimeout(swarm2.connected.waitForCount(1), 3000, new Error('Swarm2 connect timeout.')),
65
- ]);
73
+ await connectSwarms(peer1, peer2);
74
+ }).timeout(5_000);
66
75
 
67
- // Behavior of the Signal Server.
68
- swarm1.onSwarmEvent({
69
- peerAvailable: {
70
- peer: peerId1.asUint8Array(),
71
- since: new Date(),
72
- },
73
- });
76
+ test('two peers try to originate connections to each other simultaneously', async () => {
77
+ const topic = PublicKey.random();
74
78
 
75
- swarm1.onSwarmEvent({
76
- peerAvailable: {
77
- peer: peerId2.asUint8Array(),
78
- since: new Date(),
79
- },
80
- });
79
+ const peer1 = await setupSwarm({ topic });
80
+ const peer2 = await setupSwarm({ topic });
81
81
 
82
- log('Candidates changed');
83
- await promise;
84
- log('Swarms connected');
82
+ expect(peer1.swarm.connections.length).to.equal(0);
83
+ expect(peer2.swarm.connections.length).to.equal(0);
85
84
 
86
- await protocol1.testConnection(peerId2);
85
+ await connectSwarms(peer1, peer2);
87
86
  }).timeout(5_000);
88
87
 
89
- test('two peers try to originate connections to each other simultaneously', async () => {
88
+ test('second peer discovered after delay', async () => {
90
89
  const topic = PublicKey.random();
91
- const peerId1 = PublicKey.random();
92
- const peerId2 = PublicKey.random();
93
90
 
94
- const { swarm: swarm1 } = await setupSwarm({ topic, peerId: peerId1 });
95
- const { swarm: swarm2 } = await setupSwarm({ topic, peerId: peerId2 });
91
+ const peer1 = await setupSwarm({ topic });
92
+ const peer2 = await setupSwarm({ topic });
96
93
 
97
- expect(swarm1.connections.length).toEqual(0);
98
- expect(swarm2.connections.length).toEqual(0);
94
+ expect(peer1.swarm.connections.length).to.equal(0);
95
+ expect(peer2.swarm.connections.length).to.equal(0);
99
96
 
100
- const connectPromises = Promise.all([swarm1.connected.waitForCount(1), swarm2.connected.waitForCount(1)]);
97
+ expect(peer1.swarm.connections.length).to.equal(0);
98
+ expect(peer2.swarm.connections.length).to.equal(0);
101
99
 
102
- swarm1.onSwarmEvent({
103
- peerAvailable: {
104
- peer: peerId2.asUint8Array(),
105
- since: new Date(),
106
- },
107
- });
100
+ await connectSwarms(peer1, peer2, () => sleep(15));
101
+ }).timeout(10_000);
108
102
 
109
- swarm2.onSwarmEvent({
110
- peerAvailable: {
111
- peer: peerId1.asUint8Array(),
112
- since: new Date(),
113
- },
114
- });
103
+ test('connection limiter', async () => {
104
+ // remotePeer1 <--> peer (connectionLimiter: max = 1) <--> remotePeer2
115
105
 
116
- await connectPromises;
117
- }).timeout(5_000);
106
+ const peerId = PublicKey.fromHex('7701dc2d');
107
+ const remotePeerId1 = PublicKey.fromHex('7d2bc6aa');
108
+ const remotePeerId2 = PublicKey.fromHex('39ba0e41');
118
109
 
119
- test('second peer discovered after delay', async () => {
120
110
  const topic = PublicKey.random();
121
- const peerId1 = PublicKey.random();
122
- const peerId2 = PublicKey.random();
111
+ const connectionLimiter = new ConnectionLimiter({ maxConcurrentInitConnections: 1 });
123
112
 
124
- const { swarm: swarm1, protocol: protocol1 } = await setupSwarm({ topic, peerId: peerId1 });
125
-
126
- const { swarm: swarm2 } = await setupSwarm({ topic, peerId: peerId2 });
113
+ const signalManager = new MemorySignalManager(context);
114
+ const sendOriginal = signalManager.sendMessage.bind(signalManager);
115
+ const messages = new ComplexSet<{ author: PublicKey; recipient: PublicKey }>(
116
+ ({ author, recipient }) => author.toHex() + recipient.toHex(),
117
+ );
118
+ signalManager.sendMessage = async (message) => {
119
+ messages.add({ author: message.author, recipient: message.recipient });
120
+ return sendOriginal(message);
121
+ };
122
+ // Stop signaling to stop connection in initiation state.
123
+ signalManager.freeze();
124
+
125
+ const peer = await setupSwarm({
126
+ topic,
127
+ peerId,
128
+ connectionLimiter,
129
+ signalManager,
130
+ });
131
+ const remotePeer1 = await setupSwarm({ peerId: remotePeerId1, topic });
132
+ const remotePeer2 = await setupSwarm({ peerId: remotePeerId2, topic });
133
+
134
+ let connected1: Promise<void>;
135
+ {
136
+ // Connection limiter allow only one connection to be started.
137
+ const connectionInit = peer.swarm.connectionAdded.waitForCount(1);
138
+ connected1 = connectSwarms(peer, remotePeer1);
139
+ await asyncTimeout(connectionInit, 1000);
140
+ }
141
+
142
+ let connected2: Promise<void>;
143
+ {
144
+ // Connection limiter should prevent second connection from being started (only created).
145
+ const connectionInit = peer.swarm.connectionAdded.waitForCount(1);
146
+ connected2 = connectSwarms(peer, remotePeer2);
147
+ await asyncTimeout(connectionInit, 1000);
148
+ }
149
+
150
+ {
151
+ // Peer sent messages only to first remote peer.
152
+ expect(messages.has({ author: peer.peerId, recipient: remotePeer1.peerId })).to.equal(true);
153
+ expect(messages.has({ author: peer.peerId, recipient: remotePeer2.peerId })).to.equal(false);
154
+ }
155
+
156
+ signalManager.unfreeze();
157
+
158
+ await Promise.all([connected1, connected2]);
159
+ });
160
+ });
127
161
 
128
- expect(swarm1.connections.length).toEqual(0);
129
- expect(swarm2.connections.length).toEqual(0);
162
+ const connectSwarms = async (peer1: TestPeer, peer2: TestPeer, delay = async () => {}) => {
163
+ const connect1 = peer1.swarm.connected.waitForCount(1);
164
+ const connect2 = peer2.swarm.connected.waitForCount(1);
130
165
 
131
- const connectPromises = Promise.all([swarm1.connected.waitForCount(1), swarm2.connected.waitForCount(1)]);
166
+ peer1.swarm.onSwarmEvent({
167
+ peerAvailable: {
168
+ peer: peer2.peerId.asUint8Array(),
169
+ since: new Date(),
170
+ },
171
+ });
132
172
 
133
- swarm1.onSwarmEvent({
134
- peerAvailable: {
135
- peer: peerId2.asUint8Array(),
136
- since: new Date(),
137
- },
138
- });
139
- await sleep(15);
140
- swarm2.onSwarmEvent({
141
- peerAvailable: {
142
- peer: peerId1.asUint8Array(),
143
- since: new Date(),
144
- },
145
- });
173
+ await delay();
146
174
 
147
- await connectPromises;
175
+ peer2.swarm.onSwarmEvent({
176
+ peerAvailable: {
177
+ peer: peer1.peerId.asUint8Array(),
178
+ since: new Date(),
179
+ },
180
+ });
148
181
 
149
- await protocol1.testConnection(peerId2);
150
- }).timeout(10_000);
151
- });
182
+ if (
183
+ !(
184
+ peer1.swarm.connections.find((connection) => connection.remoteId.equals(peer2.peerId))?.state ===
185
+ ConnectionState.CONNECTED
186
+ )
187
+ ) {
188
+ await asyncTimeout(connect1, 3000);
189
+ }
190
+ if (
191
+ !(
192
+ peer2.swarm.connections.find((connection) => connection.remoteId.equals(peer1.peerId))?.state ===
193
+ ConnectionState.CONNECTED
194
+ )
195
+ ) {
196
+ await asyncTimeout(connect2, 3000);
197
+ }
198
+
199
+ await peer1.protocol.testConnection(peer2.peerId, 'test message 1');
200
+ await peer2.protocol.testConnection(peer1.peerId, 'test message 2');
201
+ };
@@ -2,11 +2,10 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
7
5
  import { Event, scheduleTask, sleep, synchronized } from '@dxos/async';
8
6
  import { Context } from '@dxos/context';
9
7
  import { ErrorStream } from '@dxos/debug';
8
+ import { invariant } from '@dxos/invariant';
10
9
  import { PublicKey } from '@dxos/keys';
11
10
  import { log, logInfo } from '@dxos/log';
12
11
  import { ListeningHandle, Messenger } from '@dxos/messaging';
@@ -21,6 +20,7 @@ import { TransportFactory } from '../transport';
21
20
  import { Topic } from '../types';
22
21
  import { WireProtocolProvider } from '../wire-protocol';
23
22
  import { Connection, ConnectionState } from './connection';
23
+ import { ConnectionLimiter } from './connection-limiter';
24
24
  import { Peer } from './peer';
25
25
 
26
26
  const INITIATION_DELAY = 100;
@@ -82,6 +82,7 @@ export class Swarm {
82
82
  private readonly _messenger: Messenger,
83
83
  private readonly _transportFactory: TransportFactory,
84
84
  private readonly _label: string | undefined,
85
+ private readonly _connectionLimiter: ConnectionLimiter,
85
86
  ) {
86
87
  log.trace(
87
88
  'dxos.mesh.swarm.constructor',
@@ -217,7 +218,6 @@ export class Swarm {
217
218
  return answer;
218
219
  }
219
220
 
220
- @synchronized
221
221
  async onSignal(message: SignalMessage): Promise<void> {
222
222
  log('signal', { message });
223
223
  if (this._ctx.disposed) {
@@ -258,6 +258,7 @@ export class Swarm {
258
258
  this._swarmMessenger,
259
259
  this._protocolProvider,
260
260
  this._transportFactory,
261
+ this._connectionLimiter,
261
262
  {
262
263
  onInitiated: (connection) => {
263
264
  this.connectionAdded.emit(connection);
@@ -2,8 +2,7 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
5
+ import { invariant } from '@dxos/invariant';
7
6
  import { PublicKey } from '@dxos/keys';
8
7
 
9
8
  import { SwarmController, Topology } from './topology';
@@ -2,9 +2,9 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
5
  import distance from 'xor-distance';
7
6
 
7
+ import { invariant } from '@dxos/invariant';
8
8
  import { PublicKey } from '@dxos/keys';
9
9
  import { log } from '@dxos/log';
10
10
 
@@ -2,8 +2,7 @@
2
2
  // Copyright 2020 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
6
-
5
+ import { invariant } from '@dxos/invariant';
7
6
  import { PublicKey } from '@dxos/keys';
8
7
  import { log } from '@dxos/log';
9
8
 
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { Transform } from 'node:stream';
6
- import invariant from 'tiny-invariant';
7
6
 
8
7
  import { Event, Trigger } from '@dxos/async';
9
8
  import { ErrorStream } from '@dxos/debug';
9
+ import { invariant } from '@dxos/invariant';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { log, logInfo } from '@dxos/log';
12
12
  import { Signal } from '@dxos/protocols/proto/dxos/mesh/swarm';
@@ -2,12 +2,13 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import invariant from 'tiny-invariant';
5
+ import { Writable } from 'node:stream';
6
6
 
7
7
  import { Event } from '@dxos/async';
8
8
  import { Stream } from '@dxos/codec-protobuf';
9
9
  import { Context } from '@dxos/context';
10
10
  import { ErrorStream } from '@dxos/debug';
11
+ import { invariant } from '@dxos/invariant';
11
12
  import { PublicKey } from '@dxos/keys';
12
13
  import { log } from '@dxos/log';
13
14
  import { ConnectionState, BridgeEvent, BridgeService } from '@dxos/protocols/proto/dxos/mesh/bridge';
@@ -57,18 +58,19 @@ export class WebRTCTransportProxy implements Transport {
57
58
  }
58
59
  });
59
60
 
60
- const dataListener = async (data: Uint8Array) => {
61
- try {
62
- await this._params.bridgeService.sendData({
61
+ this._params.stream.pipe(new Writable({
62
+ write: (chunk, _, callback) => {
63
+ this._params.bridgeService.sendData({
63
64
  proxyId: this._proxyId,
64
- payload: data
65
- });
66
- } catch (err: any) {
67
- log.catch(err);
68
- }
69
- };
70
- this._params.stream.on('data', dataListener);
71
- this._ctx.onDispose(() => { this._params.stream.off('data', dataListener); });
65
+ payload: chunk
66
+ }).then(
67
+ () => callback(),
68
+ (err: any) => {
69
+ log.catch(err);
70
+ }
71
+ );
72
+ },
73
+ }));
72
74
  },
73
75
  (error) => log.catch(error)
74
76
  );
@@ -3,9 +3,9 @@
3
3
  //
4
4
 
5
5
  import { Duplex } from 'node:stream';
6
- import invariant from 'tiny-invariant';
7
6
 
8
7
  import { Stream } from '@dxos/codec-protobuf';
8
+ import { invariant } from '@dxos/invariant';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { log } from '@dxos/log';
11
11
  import {
@@ -21,20 +21,30 @@ import { ComplexMap } from '@dxos/util';
21
21
 
22
22
  import { WebRTCTransport } from './webrtc-transport';
23
23
 
24
+ type TransportState = {
25
+ transport: WebRTCTransport;
26
+ stream: Duplex;
27
+ writeCallbacks: (() => void)[];
28
+ };
29
+
24
30
  export class WebRTCTransportService implements BridgeService {
25
- private readonly transports = new ComplexMap<PublicKey, { transport: WebRTCTransport; stream: Duplex }>(
26
- PublicKey.hash,
27
- );
31
+ private readonly transports = new ComplexMap<PublicKey, TransportState>(PublicKey.hash);
28
32
 
29
33
  // prettier-ignore
30
34
  constructor(
31
35
  private readonly _webrtcConfig?: any
32
- ) {}
36
+ ) { }
33
37
 
34
38
  open(request: ConnectionRequest): Stream<BridgeEvent> {
35
39
  const rpcStream: Stream<BridgeEvent> = new Stream(({ ready, next, close }) => {
36
40
  const duplex: Duplex = new Duplex({
37
- read: () => {},
41
+ read: () => {
42
+ const callbacks = [...transportState.writeCallbacks];
43
+ transportState.writeCallbacks.length = 0;
44
+ for (const cb of callbacks) {
45
+ cb();
46
+ }
47
+ },
38
48
  write: function (chunk, _, callback) {
39
49
  next({ data: { payload: chunk } });
40
50
  callback();
@@ -85,8 +95,15 @@ export class WebRTCTransportService implements BridgeService {
85
95
  close();
86
96
  });
87
97
 
98
+ const transportState: TransportState = {
99
+ transport,
100
+ stream: duplex,
101
+ writeCallbacks: [],
102
+ };
103
+
88
104
  ready();
89
- this.transports.set(request.proxyId, { transport, stream: duplex });
105
+
106
+ this.transports.set(request.proxyId, transportState);
90
107
  });
91
108
 
92
109
  return rpcStream;
@@ -99,7 +116,13 @@ export class WebRTCTransportService implements BridgeService {
99
116
 
100
117
  async sendData({ proxyId, payload }: DataRequest): Promise<void> {
101
118
  invariant(this.transports.has(proxyId));
102
- await this.transports.get(proxyId)!.stream.push(payload);
119
+ const state = this.transports.get(proxyId)!;
120
+ const bufferHasSpace = state.stream.push(payload);
121
+ if (!bufferHasSpace) {
122
+ await new Promise<void>((resolve) => {
123
+ state.writeCallbacks.push(resolve);
124
+ });
125
+ }
103
126
  }
104
127
 
105
128
  async close({ proxyId }: CloseRequest) {
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import SimplePeerConstructor, { Instance as SimplePeer } from 'simple-peer';
6
- import invariant from 'tiny-invariant';
7
6
 
8
7
  import { Event } from '@dxos/async';
9
8
  import { ErrorStream, raise } from '@dxos/debug';
9
+ import { invariant } from '@dxos/invariant';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { log } from '@dxos/log';
12
12
  import { trace } from '@dxos/protocols';