@dxos/teleport 0.1.55-main.ff36828 → 0.1.55

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.
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import * as varint from 'varint';
7
+
8
+ import { describe, test } from '@dxos/test';
9
+
10
+ import { encodeChunk, decodeChunk } from './balancer';
11
+
12
+ describe('Balancer', () => {
13
+ test('varints', () => {
14
+ const values = [0, 1, 5, 127, 128, 255, 256, 257, 1024, 1024 * 1024];
15
+ for (const value of values) {
16
+ const encoded = varint.encode(value, Buffer.allocUnsafe(4)).slice(0, varint.encode.bytes);
17
+ const length = varint.encode.bytes;
18
+ expect(encoded.length).to.eq(length);
19
+
20
+ const decoded = varint.decode(encoded);
21
+ expect(decoded).to.equal(value);
22
+ expect(varint.decode.bytes).to.equal(length);
23
+ }
24
+ });
25
+
26
+ it('should correctly encode and decode a chunk without dataLength', () => {
27
+ const channelId = 1;
28
+ const chunk = Uint8Array.from([0x11, 0x22, 0x33]);
29
+
30
+ const encoded = encodeChunk({ chunk, channelId });
31
+ const decoded = decodeChunk(encoded, () => false);
32
+
33
+ expect(decoded.channelId).to.equal(channelId);
34
+ expect(decoded.dataLength).to.equal(undefined);
35
+ expect(decoded.chunk).to.deep.equal(chunk);
36
+ });
37
+
38
+ it('should correctly encode and decode a chunk with dataLength', () => {
39
+ const channelId = 2;
40
+ const chunk = Uint8Array.from([0x44, 0x55, 0x66]);
41
+ const dataLength = chunk.length;
42
+
43
+ const encoded = encodeChunk({ chunk, channelId, dataLength });
44
+ const decoded = decodeChunk(encoded, (channelId) => channelId === 2);
45
+
46
+ expect(decoded.channelId).to.equal(channelId);
47
+ expect(decoded.dataLength).to.equal(dataLength);
48
+ expect(decoded.chunk).to.deep.equal(chunk);
49
+ });
50
+ });
@@ -2,14 +2,29 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Trigger } from '@dxos/async';
5
+ import * as varint from 'varint';
6
6
 
7
- // TODO(egorgripasov): Is BinaryPort a better name?
8
- import { RpcPort } from './rpc-port';
7
+ import { Trigger, Event } from '@dxos/async';
8
+ import { log } from '@dxos/log';
9
+
10
+ import { Framer } from './framer';
11
+
12
+ const MAX_CHUNK_SIZE = 8192;
9
13
 
10
14
  type Chunk = {
11
- msg: Uint8Array;
12
- trigger: Trigger;
15
+ chunk: Uint8Array;
16
+ channelId: number;
17
+ dataLength?: number;
18
+ };
19
+
20
+ type ChunkEnvelope = {
21
+ msg: Buffer;
22
+ trigger?: Trigger;
23
+ };
24
+
25
+ type ChannelBuffer = {
26
+ buffer: Buffer;
27
+ msgLength: number;
13
28
  };
14
29
 
15
30
  /**
@@ -22,45 +37,96 @@ export class Balancer {
22
37
  private _lastCallerIndex = 0;
23
38
  private _channels: number[] = [];
24
39
 
40
+ private readonly _framer = new Framer();
25
41
  // TODO(egorgripasov): Will cause a memory leak if channels do not appreciate the backpressure.
26
- private readonly _calls: Map<number, Chunk[]> = new Map();
42
+ private readonly _sendBuffers: Map<number, ChunkEnvelope[]> = new Map();
43
+ private readonly _receiveBuffers = new Map<number, ChannelBuffer>();
27
44
 
28
- constructor(private readonly _port: RpcPort, private readonly _sysChannelId: number) {
45
+ public incomingData = new Event<Uint8Array>();
46
+ public readonly stream = this._framer.stream;
47
+
48
+ constructor(private readonly _sysChannelId: number) {
29
49
  this._channels.push(_sysChannelId);
50
+
51
+ // Handle incoming messages.
52
+ this._framer.port.subscribe(this._processIncomingMessage.bind(this));
53
+ }
54
+
55
+ get bytesSent() {
56
+ return this._framer.bytesSent;
57
+ }
58
+
59
+ get bytesReceived() {
60
+ return this._framer.bytesReceived;
30
61
  }
31
62
 
32
63
  addChannel(channel: number) {
33
64
  this._channels.push(channel);
34
65
  }
35
66
 
36
- pushChunk(msg: Uint8Array, trigger: Trigger, channelId: number) {
37
- const noCalls = this._calls.size === 0;
67
+ pushData(data: Uint8Array, trigger: Trigger, channelId: number) {
68
+ const noCalls = this._sendBuffers.size === 0;
38
69
 
39
70
  if (!this._channels.includes(channelId)) {
40
71
  throw new Error(`Unknown channel ${channelId}`);
41
72
  }
42
73
 
43
- if (!this._calls.has(channelId)) {
44
- this._calls.set(channelId, []);
74
+ if (!this._sendBuffers.has(channelId)) {
75
+ this._sendBuffers.set(channelId, []);
45
76
  }
46
77
 
47
- const channelCalls = this._calls.get(channelId)!;
48
- channelCalls.push({ msg, trigger });
78
+ const sendBuffer = this._sendBuffers.get(channelId)!;
79
+
80
+ const chunks = [];
81
+ for (let idx = 0; idx < data.length; idx += MAX_CHUNK_SIZE) {
82
+ chunks.push(data.subarray(idx, idx + MAX_CHUNK_SIZE));
83
+ }
84
+
85
+ chunks.forEach((chunk, index) => {
86
+ const msg = encodeChunk({
87
+ chunk,
88
+ channelId,
89
+ dataLength: index === 0 ? data.length : undefined,
90
+ });
91
+ sendBuffer.push({ msg, trigger: index === chunks.length - 1 ? trigger : undefined });
92
+ });
49
93
 
50
94
  // Start processing calls if this is the first call.
51
95
  if (noCalls) {
52
- process.nextTick(async () => {
53
- await this._processCalls();
54
- });
96
+ this._sendChunks().catch((err) => log.catch(err));
55
97
  }
56
98
  }
57
99
 
58
100
  destroy() {
59
- this._calls.clear();
101
+ this._sendBuffers.clear();
102
+ this._framer.destroy();
103
+ }
104
+
105
+ private _processIncomingMessage(msg: Uint8Array) {
106
+ const { channelId, dataLength, chunk } = decodeChunk(msg, (channelId) => !this._receiveBuffers.has(channelId));
107
+ if (!this._receiveBuffers.has(channelId)) {
108
+ if (chunk.length < dataLength!) {
109
+ this._receiveBuffers.set(channelId, {
110
+ buffer: Buffer.from(chunk),
111
+ msgLength: dataLength!,
112
+ });
113
+ } else {
114
+ this.incomingData.emit(chunk);
115
+ }
116
+ } else {
117
+ const channelBuffer = this._receiveBuffers.get(channelId)!;
118
+ channelBuffer.buffer = Buffer.concat([channelBuffer.buffer, chunk]);
119
+ if (channelBuffer.buffer.length < channelBuffer.msgLength) {
120
+ return;
121
+ }
122
+ const msg = channelBuffer.buffer;
123
+ this._receiveBuffers.delete(channelId);
124
+ this.incomingData.emit(msg);
125
+ }
60
126
  }
61
127
 
62
128
  private _getNextCallerId() {
63
- if (this._calls.has(this._sysChannelId)) {
129
+ if (this._sendBuffers.has(this._sysChannelId)) {
64
130
  return this._sysChannelId;
65
131
  }
66
132
 
@@ -70,37 +136,64 @@ export class Balancer {
70
136
  return this._channels[index];
71
137
  }
72
138
 
73
- private _getNextCall(): Chunk {
74
- let call;
75
- while (!call) {
139
+ private _getNextChunk(): ChunkEnvelope {
140
+ let chunk;
141
+ while (!chunk) {
76
142
  const channelId = this._getNextCallerId();
77
- const channelCalls = this._calls.get(channelId);
78
- if (!channelCalls) {
143
+ const sendBuffer = this._sendBuffers.get(channelId);
144
+ if (!sendBuffer) {
79
145
  continue;
80
146
  }
81
147
 
82
- call = channelCalls.shift();
83
- if (channelCalls.length === 0) {
84
- this._calls.delete(channelId);
148
+ chunk = sendBuffer.shift();
149
+ if (sendBuffer.length === 0) {
150
+ this._sendBuffers.delete(channelId);
85
151
  }
86
152
  }
87
- return call;
153
+ return chunk;
88
154
  }
89
155
 
90
- private async _processCalls() {
91
- if (this._calls.size === 0) {
156
+ private async _sendChunks() {
157
+ if (this._sendBuffers.size === 0) {
92
158
  return;
93
159
  }
94
160
 
95
- const call = this._getNextCall();
161
+ const chunk = this._getNextChunk();
96
162
 
97
163
  try {
98
- await this._port.send(call.msg);
99
- call.trigger.wake();
164
+ await this._framer.port.send(chunk.msg);
165
+ chunk.trigger?.wake();
100
166
  } catch (err: any) {
101
- call.trigger.throw(err);
167
+ chunk.trigger?.throw(err);
102
168
  }
103
169
 
104
- await this._processCalls();
170
+ await this._sendChunks();
105
171
  }
106
172
  }
173
+
174
+ export const encodeChunk = ({ channelId, dataLength, chunk }: Chunk): Buffer => {
175
+ const channelTagLength = varint.encodingLength(channelId);
176
+ const dataLengthLength = dataLength ? varint.encodingLength(dataLength) : 0;
177
+ const message = Buffer.allocUnsafe(channelTagLength + dataLengthLength + chunk.length);
178
+ varint.encode(channelId, message);
179
+ if (dataLength) {
180
+ varint.encode(dataLength, message, channelTagLength);
181
+ }
182
+ message.set(chunk, channelTagLength + dataLengthLength);
183
+ return message;
184
+ };
185
+
186
+ export const decodeChunk = (data: Uint8Array, withLength: (channelId: number) => boolean): Chunk => {
187
+ const channelId = varint.decode(data);
188
+ let dataLength: number | undefined;
189
+ let offset = varint.decode.bytes;
190
+
191
+ if (withLength(channelId)) {
192
+ dataLength = varint.decode(data, offset);
193
+ offset += varint.decode.bytes;
194
+ }
195
+
196
+ const chunk = data.subarray(offset);
197
+
198
+ return { channelId, dataLength, chunk };
199
+ };
@@ -5,7 +5,6 @@
5
5
  import { expect } from 'chai';
6
6
  import { pipeline } from 'node:stream';
7
7
  import randomBytes from 'randombytes';
8
- import * as varint from 'varint';
9
8
  import waitForExpect from 'wait-for-expect';
10
9
 
11
10
  import { sleep } from '@dxos/async';
@@ -50,21 +49,8 @@ const pipe = (a: NodeJS.ReadWriteStream, b: NodeJS.ReadWriteStream): (() => void
50
49
  };
51
50
 
52
51
  describe('Framer', () => {
53
- test('varints', () => {
54
- const values = [0, 1, 5, 127, 128, 255, 256, 257, 1024, 1024 * 1024];
55
- for (const value of values) {
56
- const encoded = varint.encode(value, Buffer.allocUnsafe(4)).slice(0, varint.encode.bytes);
57
- const length = varint.encode.bytes;
58
- expect(encoded.length).to.eq(length);
59
-
60
- const decoded = varint.decode(encoded);
61
- expect(decoded).to.equal(value);
62
- expect(varint.decode.bytes).to.equal(length);
63
- }
64
- });
65
-
66
52
  test('frame encoding', () => {
67
- const sizes = [0, 1, 5, 127, 128, 255, 256, 257, 1024, 1024 * 1024];
53
+ const sizes = [0, 1, 5, 127, 128, 255, 256, 257, 1024, 1024 * 60];
68
54
  for (const size of sizes) {
69
55
  const payload = randomBytes(size);
70
56
  const frame = encodeFrame(payload);
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { Duplex } from 'node:stream';
6
6
  import invariant from 'tiny-invariant';
7
- import * as varint from 'varint';
8
7
 
9
8
  import { RpcPort } from './rpc-port';
10
9
 
10
+ const FRAME_LENGTH_SIZE = 2;
11
+
11
12
  /**
12
13
  * Framer that turns a stream of binary messages into a framed RpcPort.
13
14
  *
@@ -18,14 +19,21 @@ export class Framer {
18
19
  private _messageCb?: (msg: Uint8Array) => void;
19
20
  private _subscribeCb?: () => void;
20
21
  private _buffer?: Buffer; // The rest of the bytes from the previous write call.
21
- private _responseQueue: (() => void)[] = [];
22
+ private _sendCallbacks: (() => void)[] = [];
23
+
24
+ private _bytesSent = 0;
25
+ private _bytesReceived = 0;
22
26
 
23
27
  private readonly _stream = new Duplex({
24
28
  objectMode: false,
25
- read: () => {},
29
+ read: () => {
30
+ this._processResponseQueue();
31
+ },
26
32
  write: (chunk, encoding, callback) => {
27
33
  invariant(!this._subscribeCb, 'Internal Framer bug. Concurrent writes detected.');
28
34
 
35
+ this._bytesReceived += chunk.length;
36
+
29
37
  if (this._buffer && this._buffer.length > 0) {
30
38
  this._buffer = Buffer.concat([this._buffer, chunk]);
31
39
  } else {
@@ -50,11 +58,13 @@ export class Framer {
50
58
  send: (message) => {
51
59
  // log('write', { len: message.length, frame: Buffer.from(message).toString('hex') })
52
60
  return new Promise<void>((resolve) => {
53
- const canContinue = this._stream.push(encodeFrame(message));
61
+ const frame = encodeFrame(message);
62
+ this._bytesSent += frame.length;
63
+ const canContinue = this._stream.push(frame);
54
64
  if (!canContinue) {
55
- this._responseQueue.push(resolve);
65
+ this._sendCallbacks.push(resolve);
56
66
  } else {
57
- process.nextTick(resolve);
67
+ resolve();
58
68
  }
59
69
  });
60
70
  },
@@ -72,13 +82,17 @@ export class Framer {
72
82
  return this._stream;
73
83
  }
74
84
 
75
- constructor() {
76
- this.stream.on('drain', this._processResponseQueue.bind(this));
85
+ get bytesSent() {
86
+ return this._bytesSent;
87
+ }
88
+
89
+ get bytesReceived() {
90
+ return this._bytesReceived;
77
91
  }
78
92
 
79
93
  private _processResponseQueue() {
80
- const responseQueue = this._responseQueue;
81
- this._responseQueue = [];
94
+ const responseQueue = this._sendCallbacks;
95
+ this._sendCallbacks = [];
82
96
  responseQueue.forEach((cb) => cb());
83
97
  }
84
98
 
@@ -109,7 +123,6 @@ export class Framer {
109
123
 
110
124
  destroy() {
111
125
  // TODO(dmaretskyi): Call stream.end() instead?
112
- this._stream.removeAllListeners('drain');
113
126
  this._stream.destroy();
114
127
  }
115
128
  }
@@ -118,35 +131,30 @@ export class Framer {
118
131
  * Attempts to read a frame from the input buffer.
119
132
  */
120
133
  export const decodeFrame = (buffer: Buffer, offset: number): { payload: Buffer; bytesConsumed: number } | undefined => {
121
- try {
122
- const frameLength = varint.decode(buffer, offset);
123
- const tagLength = varint.decode.bytes;
124
-
125
- if (buffer.length < offset + tagLength + frameLength) {
126
- // Not enough bytes to read the frame.
127
- return undefined;
128
- }
134
+ if (buffer.length < offset + FRAME_LENGTH_SIZE) {
135
+ // Not enough bytes to read the frame length.
136
+ return undefined;
137
+ }
129
138
 
130
- const payload = buffer.subarray(offset + tagLength, offset + tagLength + frameLength);
139
+ const frameLength = buffer.readUInt16BE(offset);
140
+ const bytesConsumed = FRAME_LENGTH_SIZE + frameLength;
131
141
 
132
- return {
133
- payload,
134
- bytesConsumed: tagLength + frameLength,
135
- };
136
- } catch (err) {
137
- if (err instanceof RangeError) {
138
- // Not enough bytes to read the tag.
139
- return undefined;
140
- } else {
141
- throw err;
142
- }
142
+ if (buffer.length < offset + bytesConsumed) {
143
+ // Not enough bytes to read the frame.
144
+ return undefined;
143
145
  }
146
+
147
+ const payload = buffer.subarray(offset + FRAME_LENGTH_SIZE, offset + bytesConsumed);
148
+
149
+ return {
150
+ payload,
151
+ bytesConsumed,
152
+ };
144
153
  };
145
154
 
146
155
  export const encodeFrame = (payload: Uint8Array): Buffer => {
147
- const tagLength = varint.encodingLength(payload.length);
148
- const frame = Buffer.allocUnsafe(tagLength + payload.length);
149
- varint.encode(payload.length, frame);
150
- frame.set(payload, tagLength);
156
+ const frame = Buffer.allocUnsafe(FRAME_LENGTH_SIZE + payload.length);
157
+ frame.writeUInt16BE(payload.length, 0);
158
+ frame.set(payload, FRAME_LENGTH_SIZE);
151
159
  return frame;
152
160
  };
@@ -14,7 +14,6 @@ import { ConnectionInfo } from '@dxos/protocols/proto/dxos/devtools/swarm';
14
14
  import { Command } from '@dxos/protocols/proto/dxos/mesh/muxer';
15
15
 
16
16
  import { Balancer } from './balancer';
17
- import { Framer } from './framer';
18
17
  import { RpcPort } from './rpc-port';
19
18
 
20
19
  const Command = schema.getCodecForType('dxos.mesh.muxer.Command');
@@ -32,9 +31,17 @@ export type CreateChannelOpts = {
32
31
  contentType?: string;
33
32
  };
34
33
 
34
+ export type MuxerStats = {
35
+ channels: ConnectionInfo.StreamStats[];
36
+ bytesSent: number;
37
+ bytesReceived: number;
38
+ };
39
+
35
40
  const STATS_INTERVAL = 1000;
36
41
 
37
- const SYSTEM_CHANNEL_ID = -1;
42
+ const MAX_SAFE_FRAME_SIZE = 1_000_000;
43
+
44
+ const SYSTEM_CHANNEL_ID = 0;
38
45
 
39
46
  /**
40
47
  * Channel based multiplexer.
@@ -47,26 +54,23 @@ const SYSTEM_CHANNEL_ID = -1;
47
54
  * A higher level API (could be build on top of this muxer) for channel discovery is required.
48
55
  */
49
56
  export class Muxer {
50
- private readonly _framer = new Framer();
51
- private readonly _balancer = new Balancer(this._framer.port, SYSTEM_CHANNEL_ID);
52
- public readonly stream = this._framer.stream;
53
-
57
+ private readonly _balancer = new Balancer(SYSTEM_CHANNEL_ID);
54
58
  private readonly _channelsByLocalId = new Map<number, Channel>();
55
59
  private readonly _channelsByTag = new Map<string, Channel>();
60
+ private readonly _ctx = new Context();
56
61
 
57
- private _nextId = 0;
62
+ private _nextId = 1;
58
63
  private _destroyed = false;
59
64
  private _destroying = false;
60
65
 
61
66
  public close = new Event<Error | undefined>();
67
+ public statsUpdated = new Event<MuxerStats>();
62
68
 
63
- public statsUpdated = new Event<ConnectionInfo.StreamStats[]>();
64
-
65
- private readonly _ctx = new Context();
69
+ public readonly stream = this._balancer.stream;
66
70
 
67
71
  constructor() {
68
72
  // Add a channel for control messages.
69
- this._framer.port.subscribe(async (msg) => {
73
+ this._balancer.incomingData.on(async (msg) => {
70
74
  await this._handleCommand(Command.decode(msg));
71
75
  });
72
76
 
@@ -220,7 +224,6 @@ export class Muxer {
220
224
 
221
225
  this._destroyed = true;
222
226
  this._balancer.destroy();
223
- this._framer.destroy();
224
227
 
225
228
  for (const channel of this._channelsByTag.values()) {
226
229
  channel.destroy?.(err);
@@ -276,7 +279,7 @@ export class Muxer {
276
279
  private async _sendCommand(cmd: Command, channelId = -1) {
277
280
  try {
278
281
  const trigger = new Trigger<void>();
279
- this._balancer.pushChunk(Command.encode(cmd), trigger, channelId);
282
+ this._balancer.pushData(Command.encode(cmd), trigger, channelId);
280
283
  await trigger.wait();
281
284
  } catch (err: any) {
282
285
  this.destroy(err);
@@ -307,6 +310,10 @@ export class Muxer {
307
310
  }
308
311
 
309
312
  private async _sendData(channel: Channel, data: Uint8Array): Promise<void> {
313
+ if (data.length > MAX_SAFE_FRAME_SIZE) {
314
+ log.warn('frame size exceeds maximum safe value', { size: data.length, threshold: MAX_SAFE_FRAME_SIZE });
315
+ }
316
+
310
317
  channel.stats.bytesSent += data.length;
311
318
  if (channel.remoteId === null) {
312
319
  // Remote side has not opened the channel yet.
@@ -337,14 +344,20 @@ export class Muxer {
337
344
  return;
338
345
  }
339
346
 
340
- this.statsUpdated.emit(
341
- Array.from(this._channelsByTag.values()).map((channel) => ({
347
+ const bytesSent = this._balancer.bytesSent;
348
+ const bytesReceived = this._balancer.bytesReceived;
349
+
350
+ this.statsUpdated.emit({
351
+ channels: Array.from(this._channelsByTag.values()).map((channel) => ({
342
352
  id: channel.id,
343
353
  tag: channel.tag,
354
+ contentType: channel.contentType,
344
355
  bytesSent: channel.stats.bytesSent,
345
356
  bytesReceived: channel.stats.bytesReceived,
346
357
  })),
347
- );
358
+ bytesSent,
359
+ bytesReceived,
360
+ });
348
361
  }
349
362
  }
350
363
 
package/src/teleport.ts CHANGED
@@ -11,12 +11,11 @@ import { failUndefined } from '@dxos/debug';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { log } from '@dxos/log';
13
13
  import { schema, RpcClosedError } from '@dxos/protocols';
14
- import { ConnectionInfo } from '@dxos/protocols/proto/dxos/devtools/swarm';
15
14
  import { ControlService } from '@dxos/protocols/proto/dxos/mesh/teleport/control';
16
15
  import { createProtoRpcPeer, ProtoRpcPeer } from '@dxos/rpc';
17
16
  import { Callback } from '@dxos/util';
18
17
 
19
- import { CreateChannelOpts, Muxer, RpcPort } from './muxing';
18
+ import { CreateChannelOpts, Muxer, MuxerStats, RpcPort } from './muxing';
20
19
 
21
20
  export type TeleportParams = {
22
21
  initiator: boolean;
@@ -84,13 +83,23 @@ export class Teleport {
84
83
  await this.destroy(err);
85
84
  });
86
85
  }
86
+
87
+ this._muxer.statsUpdated.on((stats) => {
88
+ log.trace('dxos.mesh.teleport.stats', {
89
+ localPeerId,
90
+ remotePeerId,
91
+ bytesSent: stats.bytesSent,
92
+ bytesReceived: stats.bytesReceived,
93
+ channels: stats.channels,
94
+ });
95
+ });
87
96
  }
88
97
 
89
98
  get stream(): Duplex {
90
99
  return this._muxer.stream;
91
100
  }
92
101
 
93
- get stats(): Event<ConnectionInfo.StreamStats[]> {
102
+ get stats(): Event<MuxerStats> {
94
103
  return this._muxer.statsUpdated;
95
104
  }
96
105
 
@@ -105,7 +114,6 @@ export class Teleport {
105
114
 
106
115
  async close(err?: Error) {
107
116
  // TODO(dmaretskyi): Try soft close.
108
-
109
117
  await this.destroy(err);
110
118
  }
111
119
 
@@ -207,6 +215,10 @@ export interface TeleportExtension {
207
215
  onClose(err?: Error): Promise<void>;
208
216
  }
209
217
 
218
+ type ControlRpcBundle = {
219
+ Control: ControlService;
220
+ };
221
+
210
222
  type ControlExtensionOpts = {
211
223
  heartbeatInterval: number;
212
224
  heartbeatTimeout: number;
@@ -220,13 +232,17 @@ class ControlExtension implements TeleportExtension {
220
232
  },
221
233
  });
222
234
 
235
+ public readonly onExtensionRegistered = new Callback<(extensionName: string) => void>();
236
+
223
237
  private _extensionContext!: ExtensionContext;
224
238
  private _rpc!: ProtoRpcPeer<{ Control: ControlService }>;
225
239
 
226
- public readonly onExtensionRegistered = new Callback<(extensionName: string) => void>();
227
-
228
240
  constructor(private readonly opts: ControlExtensionOpts) {}
229
241
 
242
+ async registerExtension(name: string) {
243
+ await this._rpc.rpc.Control.registerExtension({ name });
244
+ }
245
+
230
246
  async onOpen(extensionContext: ExtensionContext): Promise<void> {
231
247
  this._extensionContext = extensionContext;
232
248
 
@@ -273,12 +289,4 @@ class ControlExtension implements TeleportExtension {
273
289
  await this._ctx.dispose();
274
290
  await this._rpc.close();
275
291
  }
276
-
277
- async registerExtension(name: string) {
278
- await this._rpc.rpc.Control.registerExtension({ name });
279
- }
280
292
  }
281
-
282
- type ControlRpcBundle = {
283
- Control: ControlService;
284
- };
@@ -84,6 +84,20 @@ export class TestExtensionWithStreams implements TeleportExtension {
84
84
  networkStream.on('close', () => {
85
85
  networkStream.removeAllListeners();
86
86
  });
87
+
88
+ streamEntry.reportingTimer = setInterval(() => {
89
+ const { bytesSent, bytesReceived, sendErrors, receiveErrors } = streamEntry;
90
+ // log.info('stream stats', { streamTag, bytesSent, bytesReceived, sendErrors, receiveErrors });
91
+ log.trace('dxos.test.stream-stats', {
92
+ streamTag,
93
+ bytesSent,
94
+ bytesReceived,
95
+ sendErrors,
96
+ receiveErrors,
97
+ from: this.extensionContext?.localPeerId,
98
+ to: this.extensionContext?.remotePeerId,
99
+ });
100
+ }, 100);
87
101
  }
88
102
 
89
103
  private _closeStream(streamTag: string): Stats {
@@ -92,6 +106,7 @@ export class TestExtensionWithStreams implements TeleportExtension {
92
106
  const stream = this._streams.get(streamTag)!;
93
107
 
94
108
  clearTimeout(stream.timer);
109
+ clearTimeout(stream.reportingTimer);
95
110
 
96
111
  const { bytesSent, bytesReceived, sendErrors, receiveErrors, startTimestamp } = stream;
97
112
 
@@ -244,4 +259,5 @@ type TestStream = {
244
259
  receiveErrors: number;
245
260
  timer?: NodeJS.Timer;
246
261
  startTimestamp?: number;
262
+ reportingTimer?: NodeJS.Timer;
247
263
  };