@peers-app/peers-sdk 0.8.0 → 0.8.1

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,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
- // this.socket = socket;
40
- this.socket = new streamed_socket_1.StreamedSocket(socket, this.maxChunkSize);
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
- expect(clientChunkCalls.length).toBe(4);
266
- expect(serverChunkCalls.length).toBe(5);
267
- // expect calls to never have chunks greater than chunk size
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;