@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.
- package/dist/lib/browser/{chunk-HCC6HY7F.mjs → chunk-WUEX3RV3.mjs} +280 -144
- package/dist/lib/browser/chunk-WUEX3RV3.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node/index.cjs +295 -159
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +295 -159
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/muxing/balancer.d.ts +22 -8
- package/dist/types/src/muxing/balancer.d.ts.map +1 -1
- package/dist/types/src/muxing/balancer.test.d.ts +2 -0
- package/dist/types/src/muxing/balancer.test.d.ts.map +1 -0
- package/dist/types/src/muxing/framer.d.ts +5 -2
- package/dist/types/src/muxing/framer.d.ts.map +1 -1
- package/dist/types/src/muxing/muxer.d.ts +8 -4
- package/dist/types/src/muxing/muxer.d.ts.map +1 -1
- package/dist/types/src/teleport.d.ts +2 -3
- package/dist/types/src/teleport.d.ts.map +1 -1
- package/dist/types/src/testing/test-extension-with-streams.d.ts.map +1 -1
- package/package.json +11 -11
- package/src/muxing/balancer.test.ts +50 -0
- package/src/muxing/balancer.ts +127 -34
- package/src/muxing/framer.test.ts +1 -15
- package/src/muxing/framer.ts +43 -35
- package/src/muxing/muxer.ts +29 -16
- package/src/teleport.ts +22 -14
- package/src/testing/test-extension-with-streams.ts +16 -0
- package/dist/lib/browser/chunk-HCC6HY7F.mjs.map +0 -7
|
@@ -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
|
+
});
|
package/src/muxing/balancer.ts
CHANGED
|
@@ -2,14 +2,29 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as varint from 'varint';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
42
|
+
private readonly _sendBuffers: Map<number, ChunkEnvelope[]> = new Map();
|
|
43
|
+
private readonly _receiveBuffers = new Map<number, ChannelBuffer>();
|
|
27
44
|
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
const noCalls = this.
|
|
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.
|
|
44
|
-
this.
|
|
74
|
+
if (!this._sendBuffers.has(channelId)) {
|
|
75
|
+
this._sendBuffers.set(channelId, []);
|
|
45
76
|
}
|
|
46
77
|
|
|
47
|
-
const
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
await this._processCalls();
|
|
54
|
-
});
|
|
96
|
+
this._sendChunks().catch((err) => log.catch(err));
|
|
55
97
|
}
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
destroy() {
|
|
59
|
-
this.
|
|
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.
|
|
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
|
|
74
|
-
let
|
|
75
|
-
while (!
|
|
139
|
+
private _getNextChunk(): ChunkEnvelope {
|
|
140
|
+
let chunk;
|
|
141
|
+
while (!chunk) {
|
|
76
142
|
const channelId = this._getNextCallerId();
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
143
|
+
const sendBuffer = this._sendBuffers.get(channelId);
|
|
144
|
+
if (!sendBuffer) {
|
|
79
145
|
continue;
|
|
80
146
|
}
|
|
81
147
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
this.
|
|
148
|
+
chunk = sendBuffer.shift();
|
|
149
|
+
if (sendBuffer.length === 0) {
|
|
150
|
+
this._sendBuffers.delete(channelId);
|
|
85
151
|
}
|
|
86
152
|
}
|
|
87
|
-
return
|
|
153
|
+
return chunk;
|
|
88
154
|
}
|
|
89
155
|
|
|
90
|
-
private async
|
|
91
|
-
if (this.
|
|
156
|
+
private async _sendChunks() {
|
|
157
|
+
if (this._sendBuffers.size === 0) {
|
|
92
158
|
return;
|
|
93
159
|
}
|
|
94
160
|
|
|
95
|
-
const
|
|
161
|
+
const chunk = this._getNextChunk();
|
|
96
162
|
|
|
97
163
|
try {
|
|
98
|
-
await this.
|
|
99
|
-
|
|
164
|
+
await this._framer.port.send(chunk.msg);
|
|
165
|
+
chunk.trigger?.wake();
|
|
100
166
|
} catch (err: any) {
|
|
101
|
-
|
|
167
|
+
chunk.trigger?.throw(err);
|
|
102
168
|
}
|
|
103
169
|
|
|
104
|
-
await this.
|
|
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 *
|
|
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);
|
package/src/muxing/framer.ts
CHANGED
|
@@ -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
|
|
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
|
|
61
|
+
const frame = encodeFrame(message);
|
|
62
|
+
this._bytesSent += frame.length;
|
|
63
|
+
const canContinue = this._stream.push(frame);
|
|
54
64
|
if (!canContinue) {
|
|
55
|
-
this.
|
|
65
|
+
this._sendCallbacks.push(resolve);
|
|
56
66
|
} else {
|
|
57
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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.
|
|
81
|
-
this.
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
139
|
+
const frameLength = buffer.readUInt16BE(offset);
|
|
140
|
+
const bytesConsumed = FRAME_LENGTH_SIZE + frameLength;
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
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
|
};
|
package/src/muxing/muxer.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
341
|
-
|
|
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<
|
|
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
|
};
|