@peers-app/peers-sdk 0.8.0 → 0.8.2
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/data/files/file-read-stream.d.ts +2 -1
- package/dist/data/files/file-read-stream.js +9 -5
- package/dist/data/files/files.d.ts +4 -2
- package/dist/data/files/files.js +4 -4
- package/dist/device/binary-peer-connection.d.ts +54 -0
- package/dist/device/binary-peer-connection.js +350 -0
- package/dist/device/binary-peer-connection.test.d.ts +1 -0
- package/dist/device/binary-peer-connection.test.js +204 -0
- package/dist/device/connection.d.ts +29 -0
- package/dist/device/connection.js +85 -2
- package/dist/device/connection.test.js +20 -4
- package/dist/device/socket-io-binary-peer.d.ts +27 -0
- package/dist/device/socket-io-binary-peer.js +97 -0
- package/dist/device/socket.type.d.ts +52 -0
- package/dist/device/streamed-socket.d.ts +4 -1
- package/dist/device/streamed-socket.js +45 -4
- package/dist/device/tx-encoding.js +22 -4
- package/dist/device/tx-encoding.test.js +28 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/package-loader/package-loader.d.ts +2 -0
- package/dist/package-loader/package-loader.js +4 -2
- package/dist/rpc-types.d.ts +1 -0
- package/dist/rpc-types.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const keys_1 = require("../keys");
|
|
4
|
+
const device_1 = require("./device");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const binary_peer_connection_1 = require("./binary-peer-connection");
|
|
7
|
+
// Mock binary peer for testing - simulates both WebRTC and libp2p peers
|
|
8
|
+
class MockBinaryPeer {
|
|
9
|
+
dataHandler;
|
|
10
|
+
closeHandler;
|
|
11
|
+
drainHandler;
|
|
12
|
+
otherPeer;
|
|
13
|
+
on(event, handler) {
|
|
14
|
+
if (event === 'data') {
|
|
15
|
+
this.dataHandler = handler;
|
|
16
|
+
}
|
|
17
|
+
else if (event === 'close') {
|
|
18
|
+
this.closeHandler = handler;
|
|
19
|
+
}
|
|
20
|
+
else if (event === 'drain') {
|
|
21
|
+
this.drainHandler = handler;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
send(data) {
|
|
25
|
+
if (this.otherPeer?.dataHandler) {
|
|
26
|
+
const bytes = typeof data === 'string'
|
|
27
|
+
? Buffer.from(data)
|
|
28
|
+
: Buffer.isBuffer(data)
|
|
29
|
+
? data
|
|
30
|
+
: Buffer.from(data);
|
|
31
|
+
// Simulate async delivery
|
|
32
|
+
setImmediate(() => {
|
|
33
|
+
this.otherPeer.dataHandler(bytes);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
destroy() {
|
|
38
|
+
this.dataHandler = undefined;
|
|
39
|
+
if (this.closeHandler) {
|
|
40
|
+
this.closeHandler();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function getMockPeerPair() {
|
|
45
|
+
const iPeer = new MockBinaryPeer();
|
|
46
|
+
const rPeer = new MockBinaryPeer();
|
|
47
|
+
// Connect the peers
|
|
48
|
+
iPeer.otherPeer = rPeer;
|
|
49
|
+
rPeer.otherPeer = iPeer;
|
|
50
|
+
await (0, utils_1.sleep)(10);
|
|
51
|
+
return {
|
|
52
|
+
iPeer,
|
|
53
|
+
rPeer,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
describe('binary-peer-connection', () => {
|
|
57
|
+
describe('MockBinaryPeer', () => {
|
|
58
|
+
it('should exchange data between mock peers', async () => {
|
|
59
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
60
|
+
let iPeerDataReceived = '';
|
|
61
|
+
iPeer.on('data', (data) => {
|
|
62
|
+
iPeerDataReceived += data.toString();
|
|
63
|
+
});
|
|
64
|
+
let rPeerDataReceived = '';
|
|
65
|
+
rPeer.on('data', (data) => {
|
|
66
|
+
rPeerDataReceived += data.toString();
|
|
67
|
+
});
|
|
68
|
+
rPeer.send('rPeer');
|
|
69
|
+
iPeer.send('iPeer');
|
|
70
|
+
await (0, utils_1.sleep)(50);
|
|
71
|
+
expect(iPeerDataReceived).toBe('rPeer');
|
|
72
|
+
expect(rPeerDataReceived).toBe('iPeer');
|
|
73
|
+
// Clean up
|
|
74
|
+
iPeer.destroy();
|
|
75
|
+
rPeer.destroy();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('wrapBinaryPeer', () => {
|
|
79
|
+
it('should successfully handshake over wrtc protocol', async () => {
|
|
80
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
81
|
+
const userId = `00000000000000000user001`;
|
|
82
|
+
const keys = (0, keys_1.newKeys)();
|
|
83
|
+
const connectionId = `00000000000000000conn001`;
|
|
84
|
+
const iPeerDeviceId = `000000000000000device001`;
|
|
85
|
+
const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
|
|
86
|
+
const iPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, { protocol: 'wrtc' });
|
|
87
|
+
const rPeerDeviceId = `000000000000000device002`;
|
|
88
|
+
const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
|
|
89
|
+
const rPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, { protocol: 'wrtc' });
|
|
90
|
+
await iPeerConnection.doHandshake('wrtc://' + connectionId);
|
|
91
|
+
expect(rPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
92
|
+
expect(rPeerConnection.remoteDeviceInfo?.deviceId).toBe(iPeerDeviceId);
|
|
93
|
+
expect(iPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
94
|
+
expect(iPeerConnection.remoteDeviceInfo?.deviceId).toBe(rPeerDeviceId);
|
|
95
|
+
// Clean up
|
|
96
|
+
iPeer.destroy();
|
|
97
|
+
rPeer.destroy();
|
|
98
|
+
});
|
|
99
|
+
it('should successfully handshake over libp2p protocol', async () => {
|
|
100
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
101
|
+
const userId = `00000000000000000user001`;
|
|
102
|
+
const keys = (0, keys_1.newKeys)();
|
|
103
|
+
const connectionId = `00000000000000000conn001`;
|
|
104
|
+
const iPeerDeviceId = `000000000000000device001`;
|
|
105
|
+
const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
|
|
106
|
+
const iPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, { protocol: 'libp2p' });
|
|
107
|
+
const rPeerDeviceId = `000000000000000device002`;
|
|
108
|
+
const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
|
|
109
|
+
const rPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, { protocol: 'libp2p' });
|
|
110
|
+
await iPeerConnection.doHandshake('libp2p://' + connectionId);
|
|
111
|
+
expect(rPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
112
|
+
expect(rPeerConnection.remoteDeviceInfo?.deviceId).toBe(iPeerDeviceId);
|
|
113
|
+
expect(iPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
114
|
+
expect(iPeerConnection.remoteDeviceInfo?.deviceId).toBe(rPeerDeviceId);
|
|
115
|
+
// Clean up
|
|
116
|
+
iPeer.destroy();
|
|
117
|
+
rPeer.destroy();
|
|
118
|
+
});
|
|
119
|
+
it('should set secureLocal/secureRemote when markTransportSecure is true', async () => {
|
|
120
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
121
|
+
const userId = `00000000000000000user001`;
|
|
122
|
+
const keys = (0, keys_1.newKeys)();
|
|
123
|
+
const connectionId = `00000000000000000conn001`;
|
|
124
|
+
const iPeerDeviceId = `000000000000000device001`;
|
|
125
|
+
const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
|
|
126
|
+
const iPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, {
|
|
127
|
+
protocol: 'libp2p',
|
|
128
|
+
markTransportSecure: true
|
|
129
|
+
});
|
|
130
|
+
const rPeerDeviceId = `000000000000000device002`;
|
|
131
|
+
const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
|
|
132
|
+
const rPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, {
|
|
133
|
+
protocol: 'libp2p',
|
|
134
|
+
markTransportSecure: true
|
|
135
|
+
});
|
|
136
|
+
expect(iPeerConnection.secureLocal).toBe(true);
|
|
137
|
+
expect(iPeerConnection.secureRemote).toBe(true);
|
|
138
|
+
expect(rPeerConnection.secureLocal).toBe(true);
|
|
139
|
+
expect(rPeerConnection.secureRemote).toBe(true);
|
|
140
|
+
// Clean up
|
|
141
|
+
iPeer.destroy();
|
|
142
|
+
rPeer.destroy();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('backwards compatibility', () => {
|
|
146
|
+
it('wrapWrtc should work as alias and set secure flags', async () => {
|
|
147
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
148
|
+
const userId = `00000000000000000user001`;
|
|
149
|
+
const keys = (0, keys_1.newKeys)();
|
|
150
|
+
const connectionId = `00000000000000000conn001`;
|
|
151
|
+
const iPeerDeviceId = `000000000000000device001`;
|
|
152
|
+
const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
|
|
153
|
+
const iPeerConnection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, iPeer, iPeerDevice, true);
|
|
154
|
+
const rPeerDeviceId = `000000000000000device002`;
|
|
155
|
+
const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
|
|
156
|
+
const rPeerConnection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, rPeer, rPeerDevice, false);
|
|
157
|
+
// wrapWrtc should set secure flags by default (transport-level encryption)
|
|
158
|
+
expect(iPeerConnection.secureLocal).toBe(true);
|
|
159
|
+
expect(iPeerConnection.secureRemote).toBe(true);
|
|
160
|
+
await iPeerConnection.doHandshake('wrtc://' + connectionId);
|
|
161
|
+
expect(rPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
162
|
+
expect(rPeerConnection.remoteDeviceInfo?.deviceId).toBe(iPeerDeviceId);
|
|
163
|
+
// Clean up
|
|
164
|
+
iPeer.destroy();
|
|
165
|
+
rPeer.destroy();
|
|
166
|
+
});
|
|
167
|
+
it('wrapLibp2p should work as alias and set secure flags', async () => {
|
|
168
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
169
|
+
const userId = `00000000000000000user001`;
|
|
170
|
+
const keys = (0, keys_1.newKeys)();
|
|
171
|
+
const connectionId = `00000000000000000conn001`;
|
|
172
|
+
const iPeerDeviceId = `000000000000000device001`;
|
|
173
|
+
const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
|
|
174
|
+
const iPeerConnection = (0, binary_peer_connection_1.wrapLibp2p)(connectionId, iPeer, iPeerDevice, true);
|
|
175
|
+
const rPeerDeviceId = `000000000000000device002`;
|
|
176
|
+
const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
|
|
177
|
+
const rPeerConnection = (0, binary_peer_connection_1.wrapLibp2p)(connectionId, rPeer, rPeerDevice, false);
|
|
178
|
+
// wrapLibp2p should set secure flags by default (transport-level encryption)
|
|
179
|
+
expect(iPeerConnection.secureLocal).toBe(true);
|
|
180
|
+
expect(iPeerConnection.secureRemote).toBe(true);
|
|
181
|
+
await iPeerConnection.doHandshake('libp2p://' + connectionId);
|
|
182
|
+
expect(rPeerConnection.remoteDeviceInfo).toBeDefined();
|
|
183
|
+
expect(rPeerConnection.remoteDeviceInfo?.deviceId).toBe(iPeerDeviceId);
|
|
184
|
+
// Clean up
|
|
185
|
+
iPeer.destroy();
|
|
186
|
+
rPeer.destroy();
|
|
187
|
+
});
|
|
188
|
+
it('binary peers should bypass StreamedSocket (handlesOwnEncoding)', async () => {
|
|
189
|
+
const { iPeer } = await getMockPeerPair();
|
|
190
|
+
const userId = `00000000000000000user001`;
|
|
191
|
+
const keys = (0, keys_1.newKeys)();
|
|
192
|
+
const connectionId = `00000000000000000conn001`;
|
|
193
|
+
const deviceId = `000000000000000device001`;
|
|
194
|
+
const device = new device_1.Device(userId, deviceId, keys);
|
|
195
|
+
const connection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, iPeer, device, true);
|
|
196
|
+
// The socket should NOT be wrapped in StreamedSocket
|
|
197
|
+
// We can verify this by checking that the socket is the original one
|
|
198
|
+
// (StreamedSocket would wrap and change the socket reference)
|
|
199
|
+
expect(connection.socket.handlesOwnEncoding).toBe(true);
|
|
200
|
+
// Clean up
|
|
201
|
+
iPeer.destroy();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -22,6 +22,17 @@ export declare class Connection {
|
|
|
22
22
|
readonly socket: ISocket;
|
|
23
23
|
constructor(socket: ISocket, localDevice: Device, localDeviceServerAddresses?: string[] | undefined, getTrustLevel?: GetTrustLevel);
|
|
24
24
|
get verified(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Determines whether to apply application-level encryption (sign + box).
|
|
27
|
+
*
|
|
28
|
+
* Encryption is SKIPPED when:
|
|
29
|
+
* - Binary transports (wrtc/libp2p): Set secureLocal=true & secureRemote=true
|
|
30
|
+
* because transport-level encryption (DTLS-SRTP, noise) handles security
|
|
31
|
+
* - Secure WebSockets (wss/https): Both sides confirm secure protocol after handshake
|
|
32
|
+
*
|
|
33
|
+
* Encryption is APPLIED when:
|
|
34
|
+
* - Insecure WebSockets (ws/http): Data travels over unencrypted channel
|
|
35
|
+
*/
|
|
25
36
|
get encryptTraffic(): boolean;
|
|
26
37
|
get remoteDeviceInfo(): {
|
|
27
38
|
userId: string;
|
|
@@ -30,6 +41,24 @@ export declare class Connection {
|
|
|
30
41
|
publicBoxKey: string;
|
|
31
42
|
};
|
|
32
43
|
get connectionId(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Get connection throughput stats with calculated rates.
|
|
46
|
+
* Returns bytes/messages sent/received and both average and recent rates.
|
|
47
|
+
* Recent rates are calculated over a ~10 second window for more accurate instantaneous throughput.
|
|
48
|
+
*/
|
|
49
|
+
getStats(): {
|
|
50
|
+
bytesSent: number;
|
|
51
|
+
bytesReceived: number;
|
|
52
|
+
messagesSent: number;
|
|
53
|
+
messagesReceived: number;
|
|
54
|
+
elapsedMs: number;
|
|
55
|
+
avgSendRate: number;
|
|
56
|
+
avgReceiveRate: number;
|
|
57
|
+
sendRate: number;
|
|
58
|
+
receiveRate: number;
|
|
59
|
+
sendRateMBps: number;
|
|
60
|
+
receiveRateMBps: number;
|
|
61
|
+
} | null;
|
|
33
62
|
exposeRPC<T extends Function>(name: string, fn: T): void;
|
|
34
63
|
exposeRPC<T extends Function>(fn: T): void;
|
|
35
64
|
emit<T = any>(eventName: string, ...args: any): Promise<T>;
|
|
@@ -36,8 +36,14 @@ class Connection {
|
|
|
36
36
|
this.localDeviceServerAddresses = localDeviceServerAddresses;
|
|
37
37
|
this.getTrustLevel = getTrustLevel;
|
|
38
38
|
this.localDeviceServerAddresses = localDeviceServerAddresses?.map(normalizeAddress);
|
|
39
|
-
//
|
|
40
|
-
|
|
39
|
+
// If socket handles its own encoding (e.g., binary transports like wrtc/libp2p),
|
|
40
|
+
// use it directly. Otherwise, wrap in StreamedSocket for chunking and encoding.
|
|
41
|
+
if (socket.handlesOwnEncoding) {
|
|
42
|
+
this.socket = socket;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.socket = new streamed_socket_1.StreamedSocket(socket, this.maxChunkSize);
|
|
46
|
+
}
|
|
41
47
|
if (!socket.id) {
|
|
42
48
|
socket.id = (0, utils_1.newid)();
|
|
43
49
|
}
|
|
@@ -71,6 +77,17 @@ class Connection {
|
|
|
71
77
|
get verified() {
|
|
72
78
|
return this._verified;
|
|
73
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Determines whether to apply application-level encryption (sign + box).
|
|
82
|
+
*
|
|
83
|
+
* Encryption is SKIPPED when:
|
|
84
|
+
* - Binary transports (wrtc/libp2p): Set secureLocal=true & secureRemote=true
|
|
85
|
+
* because transport-level encryption (DTLS-SRTP, noise) handles security
|
|
86
|
+
* - Secure WebSockets (wss/https): Both sides confirm secure protocol after handshake
|
|
87
|
+
*
|
|
88
|
+
* Encryption is APPLIED when:
|
|
89
|
+
* - Insecure WebSockets (ws/http): Data travels over unencrypted channel
|
|
90
|
+
*/
|
|
74
91
|
get encryptTraffic() {
|
|
75
92
|
if (this.overrideEncryption === 'dont-encrypt-traffic-after-handshake') {
|
|
76
93
|
return false;
|
|
@@ -78,6 +95,7 @@ class Connection {
|
|
|
78
95
|
if (this.overrideEncryption === 'encrypt-traffic-regardless-of-protocol') {
|
|
79
96
|
return true;
|
|
80
97
|
}
|
|
98
|
+
// Encrypt only if EITHER side is using an insecure protocol
|
|
81
99
|
return !(this.secureLocal && this.secureRemote);
|
|
82
100
|
}
|
|
83
101
|
get remoteDeviceInfo() {
|
|
@@ -86,6 +104,66 @@ class Connection {
|
|
|
86
104
|
get connectionId() {
|
|
87
105
|
return this.socket.id || '';
|
|
88
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Get connection throughput stats with calculated rates.
|
|
109
|
+
* Returns bytes/messages sent/received and both average and recent rates.
|
|
110
|
+
* Recent rates are calculated over a ~10 second window for more accurate instantaneous throughput.
|
|
111
|
+
*/
|
|
112
|
+
getStats() {
|
|
113
|
+
const stats = this.socket.stats;
|
|
114
|
+
if (!stats) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const elapsedMs = now - stats.startTime;
|
|
119
|
+
const elapsedSec = elapsedMs / 1000;
|
|
120
|
+
// Calculate recent rates (last ~10 seconds for more accurate instantaneous rate)
|
|
121
|
+
const sampleWindowMs = 10000;
|
|
122
|
+
const timeSinceLastSample = now - stats.lastSampleTime;
|
|
123
|
+
let recentSendRate = 0;
|
|
124
|
+
let recentReceiveRate = 0;
|
|
125
|
+
if (timeSinceLastSample >= sampleWindowMs) {
|
|
126
|
+
// Calculate rate since last sample
|
|
127
|
+
const bytesSentSinceSample = stats.bytesSent - stats.lastSampleBytesSent;
|
|
128
|
+
const bytesReceivedSinceSample = stats.bytesReceived - stats.lastSampleBytesReceived;
|
|
129
|
+
const sampleElapsedSec = timeSinceLastSample / 1000;
|
|
130
|
+
recentSendRate = bytesSentSinceSample / sampleElapsedSec;
|
|
131
|
+
recentReceiveRate = bytesReceivedSinceSample / sampleElapsedSec;
|
|
132
|
+
// Update the sample for next calculation
|
|
133
|
+
stats.lastSampleTime = now;
|
|
134
|
+
stats.lastSampleBytesSent = stats.bytesSent;
|
|
135
|
+
stats.lastSampleBytesReceived = stats.bytesReceived;
|
|
136
|
+
}
|
|
137
|
+
else if (stats.lastSampleTime === stats.startTime) {
|
|
138
|
+
// First sample - use overall rate
|
|
139
|
+
recentSendRate = elapsedSec > 0 ? stats.bytesSent / elapsedSec : 0;
|
|
140
|
+
recentReceiveRate = elapsedSec > 0 ? stats.bytesReceived / elapsedSec : 0;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Between samples - calculate rate since last sample
|
|
144
|
+
const bytesSentSinceSample = stats.bytesSent - stats.lastSampleBytesSent;
|
|
145
|
+
const bytesReceivedSinceSample = stats.bytesReceived - stats.lastSampleBytesReceived;
|
|
146
|
+
const sampleElapsedSec = timeSinceLastSample / 1000;
|
|
147
|
+
recentSendRate = sampleElapsedSec > 0 ? bytesSentSinceSample / sampleElapsedSec : 0;
|
|
148
|
+
recentReceiveRate = sampleElapsedSec > 0 ? bytesReceivedSinceSample / sampleElapsedSec : 0;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
bytesSent: stats.bytesSent,
|
|
152
|
+
bytesReceived: stats.bytesReceived,
|
|
153
|
+
messagesSent: stats.messagesSent,
|
|
154
|
+
messagesReceived: stats.messagesReceived,
|
|
155
|
+
elapsedMs,
|
|
156
|
+
// Average rates over entire connection lifetime (bytes per second)
|
|
157
|
+
avgSendRate: elapsedSec > 0 ? stats.bytesSent / elapsedSec : 0,
|
|
158
|
+
avgReceiveRate: elapsedSec > 0 ? stats.bytesReceived / elapsedSec : 0,
|
|
159
|
+
// Recent/instantaneous rates (bytes per second) - more accurate for current throughput
|
|
160
|
+
sendRate: recentSendRate,
|
|
161
|
+
receiveRate: recentReceiveRate,
|
|
162
|
+
// Human-readable rates in MB/s (using recent rates for more accurate display)
|
|
163
|
+
sendRateMBps: recentSendRate / (1024 * 1024),
|
|
164
|
+
receiveRateMBps: recentReceiveRate / (1024 * 1024),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
89
167
|
exposeRPC(fn, name) {
|
|
90
168
|
if (typeof name === 'function') {
|
|
91
169
|
const _fn = name;
|
|
@@ -101,6 +179,8 @@ class Connection {
|
|
|
101
179
|
emit(eventName, ...args) {
|
|
102
180
|
return new Promise((resolve, reject) => {
|
|
103
181
|
try {
|
|
182
|
+
// Apply app-level encryption for insecure transports (ws/http)
|
|
183
|
+
// Skipped for secure transports (wss/https, wrtc, libp2p)
|
|
104
184
|
if (this._verified && this.encryptTraffic) {
|
|
105
185
|
args = this.localDevice.signAndBoxDataForDevice(args, this._remoteDeviceInfo);
|
|
106
186
|
}
|
|
@@ -149,10 +229,13 @@ class Connection {
|
|
|
149
229
|
}
|
|
150
230
|
let result;
|
|
151
231
|
try {
|
|
232
|
+
// Decrypt incoming args for insecure transports (ws/http)
|
|
233
|
+
// Skipped for secure transports (wss/https, wrtc, libp2p)
|
|
152
234
|
if (this._verified && this.encryptTraffic) {
|
|
153
235
|
args = this.localDevice.openBoxedAndSignedData(args);
|
|
154
236
|
}
|
|
155
237
|
result = await handler(...args);
|
|
238
|
+
// Encrypt outgoing result for insecure transports
|
|
156
239
|
if (this._verified && this.encryptTraffic) {
|
|
157
240
|
result = this.localDevice.signAndBoxDataForDevice(result, this._remoteDeviceInfo);
|
|
158
241
|
}
|
|
@@ -4,6 +4,7 @@ const utils_1 = require("../utils");
|
|
|
4
4
|
const keys_1 = require("../keys");
|
|
5
5
|
const connection_1 = require("./connection");
|
|
6
6
|
const device_1 = require("./device");
|
|
7
|
+
const tx_encoding_1 = require("./tx-encoding");
|
|
7
8
|
function createTestSocket(localHandlers, remoteHandlers) {
|
|
8
9
|
const socket = {
|
|
9
10
|
id: (0, utils_1.newid)(),
|
|
@@ -235,6 +236,8 @@ describe(connection_1.Connection, () => {
|
|
|
235
236
|
const clientConnection = new connection_1.Connection(clientSocket, clientDevice);
|
|
236
237
|
const serverConnection = new connection_1.Connection(serverSocket, serverDevice, ['localhost']);
|
|
237
238
|
const chunkSize = 256; // 256 minimum chunk size due to encoding overheads
|
|
239
|
+
const metadataOverhead = 128; // From StreamedSocket implementation
|
|
240
|
+
const effectiveChunkSize = chunkSize - metadataOverhead; // 128 bytes
|
|
238
241
|
clientConnection.maxChunkSize = chunkSize;
|
|
239
242
|
serverConnection.maxChunkSize = chunkSize;
|
|
240
243
|
clientConnection.socket.maxChunkSize = chunkSize;
|
|
@@ -258,13 +261,26 @@ describe(connection_1.Connection, () => {
|
|
|
258
261
|
expect(result.received.message).toBe("A".repeat(500));
|
|
259
262
|
expect(result.received.metadata).toBe("B".repeat(300));
|
|
260
263
|
expect(result.received.payload).toBe("C".repeat(400));
|
|
261
|
-
expect(JSON.stringify(largeData).length).toEqual(1241);
|
|
262
264
|
// Count chunk emissions
|
|
263
265
|
const clientChunkCalls = clientEmitSpy.mock.calls.filter((call) => call[0] === clientConnection.socket.safeSocketChunkEventName);
|
|
264
266
|
const serverChunkCalls = serverEmitSpy.mock.calls.filter((call) => call[0] === serverConnection.socket.safeSocketChunkEventName);
|
|
265
|
-
|
|
266
|
-
expect(
|
|
267
|
-
|
|
267
|
+
// Verify that chunking occurred (both request and response should be chunked)
|
|
268
|
+
expect(clientChunkCalls.length).toBeGreaterThan(0); // Request chunks
|
|
269
|
+
expect(serverChunkCalls.length).toBeGreaterThan(0); // Response chunks
|
|
270
|
+
// Calculate expected chunks based on encoded data size
|
|
271
|
+
const requestEncoded = (0, tx_encoding_1.txEncode)(largeData);
|
|
272
|
+
const responseData = {
|
|
273
|
+
received: largeData,
|
|
274
|
+
processedSize: JSON.stringify(largeData).length
|
|
275
|
+
};
|
|
276
|
+
const responseEncoded = (0, tx_encoding_1.txEncode)(responseData);
|
|
277
|
+
const expectedRequestChunks = Math.ceil(requestEncoded.length / effectiveChunkSize);
|
|
278
|
+
const expectedResponseChunks = Math.ceil(responseEncoded.length / effectiveChunkSize);
|
|
279
|
+
// Allow some flexibility due to encryption/boxing overhead if encryptTraffic is enabled
|
|
280
|
+
// The actual chunks may be slightly more due to encryption overhead
|
|
281
|
+
expect(clientChunkCalls.length).toBeGreaterThanOrEqual(expectedRequestChunks);
|
|
282
|
+
expect(serverChunkCalls.length).toBeGreaterThanOrEqual(expectedResponseChunks);
|
|
283
|
+
// Verify that chunks never exceed the max chunk size (including metadata)
|
|
268
284
|
for (const call of clientChunkCalls) {
|
|
269
285
|
const chunkData = call[1];
|
|
270
286
|
expect(chunkData.length).toBeLessThanOrEqual(chunkSize);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket.IO Binary Peer Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps a socket.io socket to implement the IBinaryPeer interface,
|
|
5
|
+
* enabling unified binary transport across WebSocket, WebRTC, and libp2p.
|
|
6
|
+
*
|
|
7
|
+
* Uses a single binary event '_bin' for all data transfer, achieving
|
|
8
|
+
* ~130 MB/s throughput with proper backpressure handling.
|
|
9
|
+
*/
|
|
10
|
+
import { IBinaryPeer } from './binary-peer-connection';
|
|
11
|
+
/**
|
|
12
|
+
* Minimal socket.io interface - works with both client and server sockets
|
|
13
|
+
*/
|
|
14
|
+
interface ISocketIO {
|
|
15
|
+
id?: string;
|
|
16
|
+
on(event: string, handler: (...args: any[]) => void): void;
|
|
17
|
+
emit(event: string, ...args: any[]): void;
|
|
18
|
+
removeAllListeners(event?: string): void;
|
|
19
|
+
disconnect(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a socket.io socket (client or server) as an IBinaryPeer.
|
|
23
|
+
* This allows websocket connections to use the same binary protocol
|
|
24
|
+
* as WebRTC and libp2p connections.
|
|
25
|
+
*/
|
|
26
|
+
export declare function wrapSocketIOAsBinaryPeer(socket: ISocketIO): IBinaryPeer;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Socket.IO Binary Peer Wrapper
|
|
4
|
+
*
|
|
5
|
+
* Wraps a socket.io socket to implement the IBinaryPeer interface,
|
|
6
|
+
* enabling unified binary transport across WebSocket, WebRTC, and libp2p.
|
|
7
|
+
*
|
|
8
|
+
* Uses a single binary event '_bin' for all data transfer, achieving
|
|
9
|
+
* ~130 MB/s throughput with proper backpressure handling.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.wrapSocketIOAsBinaryPeer = wrapSocketIOAsBinaryPeer;
|
|
13
|
+
// Event name for binary data transfer
|
|
14
|
+
const BINARY_EVENT = '_bin';
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a socket.io socket (client or server) as an IBinaryPeer.
|
|
17
|
+
* This allows websocket connections to use the same binary protocol
|
|
18
|
+
* as WebRTC and libp2p connections.
|
|
19
|
+
*/
|
|
20
|
+
function wrapSocketIOAsBinaryPeer(socket) {
|
|
21
|
+
const dataHandlers = [];
|
|
22
|
+
const closeHandlers = [];
|
|
23
|
+
const drainHandlers = [];
|
|
24
|
+
let pendingBytes = 0;
|
|
25
|
+
// Listen for binary data
|
|
26
|
+
socket.on(BINARY_EVENT, (data) => {
|
|
27
|
+
// Ensure we have a proper Uint8Array
|
|
28
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
29
|
+
dataHandlers.forEach(handler => {
|
|
30
|
+
try {
|
|
31
|
+
handler(bytes);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error('[socket-io-binary] Error in data handler:', err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
// Listen for disconnect
|
|
39
|
+
socket.on('disconnect', () => {
|
|
40
|
+
closeHandlers.forEach(handler => {
|
|
41
|
+
try {
|
|
42
|
+
handler();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error('[socket-io-binary] Error in close handler:', err);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
on(event, handler) {
|
|
51
|
+
if (event === 'data') {
|
|
52
|
+
dataHandlers.push(handler);
|
|
53
|
+
}
|
|
54
|
+
else if (event === 'close') {
|
|
55
|
+
closeHandlers.push(handler);
|
|
56
|
+
}
|
|
57
|
+
else if (event === 'drain') {
|
|
58
|
+
drainHandlers.push(handler);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
send(data) {
|
|
62
|
+
const bytes = typeof data === 'string'
|
|
63
|
+
? Buffer.from(data)
|
|
64
|
+
: data instanceof Uint8Array
|
|
65
|
+
? data
|
|
66
|
+
: Buffer.from(data);
|
|
67
|
+
pendingBytes += bytes.length;
|
|
68
|
+
// Socket.io handles binary data natively
|
|
69
|
+
socket.emit(BINARY_EVENT, bytes);
|
|
70
|
+
// Simulate drain after send completes
|
|
71
|
+
setImmediate(() => {
|
|
72
|
+
pendingBytes = Math.max(0, pendingBytes - bytes.length);
|
|
73
|
+
if (pendingBytes < 1024 * 1024) { // Under 1MB
|
|
74
|
+
drainHandlers.forEach(h => h());
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
destroy() {
|
|
79
|
+
socket.removeAllListeners(BINARY_EVENT);
|
|
80
|
+
socket.disconnect();
|
|
81
|
+
},
|
|
82
|
+
getBufferedAmount() {
|
|
83
|
+
// Check socket.io's internal send buffer if available
|
|
84
|
+
const socketAny = socket;
|
|
85
|
+
if (socketAny.sendBuffer && Array.isArray(socketAny.sendBuffer)) {
|
|
86
|
+
// Estimate bytes in send buffer
|
|
87
|
+
return socketAny.sendBuffer.reduce((sum, item) => {
|
|
88
|
+
if (item.data && item.data[1] instanceof Uint8Array) {
|
|
89
|
+
return sum + item.data[1].length;
|
|
90
|
+
}
|
|
91
|
+
return sum + 1000; // Estimate for unknown items
|
|
92
|
+
}, 0);
|
|
93
|
+
}
|
|
94
|
+
return pendingBytes;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,12 +1,64 @@
|
|
|
1
1
|
export declare const DEFAULT_MAX_CHUNK_SIZE: number;
|
|
2
2
|
export type RPCCallback = (error?: any, result?: any) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight stats tracking for socket throughput.
|
|
5
|
+
* Tracks bytes and messages sent/received with minimal overhead.
|
|
6
|
+
*/
|
|
7
|
+
export interface ISocketStats {
|
|
8
|
+
bytesSent: number;
|
|
9
|
+
bytesReceived: number;
|
|
10
|
+
messagesSent: number;
|
|
11
|
+
messagesReceived: number;
|
|
12
|
+
readonly startTime: number;
|
|
13
|
+
lastSampleTime: number;
|
|
14
|
+
lastSampleBytesSent: number;
|
|
15
|
+
lastSampleBytesReceived: number;
|
|
16
|
+
}
|
|
17
|
+
export type TransportType = 'ws' | 'wrtc' | 'libp2p' | 'unknown';
|
|
3
18
|
export interface ISocket {
|
|
4
19
|
id?: string;
|
|
5
20
|
connected?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* If true, this socket handles its own encoding/framing and should NOT be wrapped
|
|
23
|
+
* in StreamedSocket. Used by binary transports (wrtc, libp2p) that have their own
|
|
24
|
+
* message encoding.
|
|
25
|
+
*/
|
|
26
|
+
handlesOwnEncoding?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Transport type for this socket. Used to optimize chunk sizes for raw byte streaming.
|
|
29
|
+
* - 'wrtc': WebRTC (64KB max due to SCTP limits)
|
|
30
|
+
* - 'libp2p': libp2p (1MB chunks OK)
|
|
31
|
+
* - 'ws': WebSocket (1MB+ chunks OK)
|
|
32
|
+
*/
|
|
33
|
+
transportType?: TransportType;
|
|
34
|
+
/**
|
|
35
|
+
* Optional stats tracking. When present, tracks bytes/messages sent and received.
|
|
36
|
+
*/
|
|
37
|
+
stats?: ISocketStats;
|
|
6
38
|
emit(eventName: string, args: any, callback: RPCCallback): void;
|
|
7
39
|
on(eventName: string, handler: ((...args: any[]) => void)): void;
|
|
8
40
|
removeAllListeners(eventName?: string): void;
|
|
9
41
|
disconnect(close?: boolean): void;
|
|
42
|
+
/**
|
|
43
|
+
* Send raw bytes directly on the transport, bypassing all encoding/RPC layers.
|
|
44
|
+
* Used for high-throughput streaming (e.g., database file transfer).
|
|
45
|
+
* Returns a promise that resolves when the data has been accepted by the transport.
|
|
46
|
+
* The promise may resolve immediately if no backpressure, or wait for buffer to drain.
|
|
47
|
+
* @param streamId - Identifier for this stream (to multiplex multiple streams)
|
|
48
|
+
* @param data - Raw bytes to send
|
|
49
|
+
*/
|
|
50
|
+
sendRawBytes?(streamId: string, data: Uint8Array): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Register a handler for raw bytes on a specific stream.
|
|
53
|
+
* @param streamId - Stream identifier to listen on
|
|
54
|
+
* @param handler - Callback receiving raw bytes
|
|
55
|
+
*/
|
|
56
|
+
onRawBytes?(streamId: string, handler: (data: Uint8Array) => void): void;
|
|
57
|
+
/**
|
|
58
|
+
* Remove raw bytes handler for a stream.
|
|
59
|
+
* @param streamId - Stream identifier to stop listening on
|
|
60
|
+
*/
|
|
61
|
+
removeRawBytesHandler?(streamId: string): void;
|
|
10
62
|
}
|
|
11
63
|
export declare enum TrustLevel {
|
|
12
64
|
Malicious = -20,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { ISocket, RPCCallback } from "./socket.type";
|
|
1
|
+
import { ISocket, ISocketStats, RPCCallback } from "./socket.type";
|
|
2
|
+
/** Creates a new stats object for tracking socket throughput */
|
|
3
|
+
export declare function createSocketStats(): ISocketStats;
|
|
2
4
|
export declare class StreamedSocket implements ISocket {
|
|
3
5
|
readonly socket: ISocket;
|
|
4
6
|
maxChunkSize: number;
|
|
5
7
|
readonly safeSocketChunkEventName = "__streamed-socket-chunk";
|
|
6
8
|
readonly safeSocketResponseEventName = "__streamed-socket-response";
|
|
9
|
+
readonly stats: ISocketStats;
|
|
7
10
|
constructor(socket: ISocket, maxChunkSize?: number);
|
|
8
11
|
get id(): string | undefined;
|
|
9
12
|
get connected(): boolean | undefined;
|