@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.
@@ -1,11 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StreamedSocket = void 0;
4
+ exports.createSocketStats = createSocketStats;
4
5
  const lodash_1 = require("lodash");
5
6
  const utils_1 = require("../utils");
6
7
  const socket_type_1 = require("./socket.type");
7
8
  const tx_encoding_1 = require("./tx-encoding");
8
9
  const msgpack_1 = require("@msgpack/msgpack");
10
+ /** Creates a new stats object for tracking socket throughput */
11
+ function createSocketStats() {
12
+ const now = Date.now();
13
+ return {
14
+ bytesSent: 0,
15
+ bytesReceived: 0,
16
+ messagesSent: 0,
17
+ messagesReceived: 0,
18
+ startTime: now,
19
+ // For calculating recent rates
20
+ lastSampleTime: now,
21
+ lastSampleBytesSent: 0,
22
+ lastSampleBytesReceived: 0,
23
+ };
24
+ }
9
25
  // Encode chunk metadata + data into a single Uint8Array
10
26
  function encodeChunk(chunk) {
11
27
  // Encode metadata using msgpack
@@ -30,14 +46,24 @@ function encodeChunk(chunk) {
30
46
  }
31
47
  // Decode binary chunk back into IMessageChunk
32
48
  function decodeChunk(bytes) {
49
+ // Ensure we have a proper Uint8Array with a valid ArrayBuffer
50
+ // Socket.io might send data that doesn't have a proper buffer property
51
+ let normalizedBytes;
52
+ if (bytes.buffer instanceof ArrayBuffer) {
53
+ normalizedBytes = bytes;
54
+ }
55
+ else {
56
+ // Create a new Uint8Array copy which will have a proper buffer
57
+ normalizedBytes = new Uint8Array(bytes);
58
+ }
33
59
  // Read metadata length
34
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
60
+ const view = new DataView(normalizedBytes.buffer, normalizedBytes.byteOffset, normalizedBytes.byteLength);
35
61
  const metadataLength = view.getUint32(0, false); // big-endian
36
62
  // Extract metadata
37
- const metadataBytes = bytes.subarray(4, 4 + metadataLength);
63
+ const metadataBytes = normalizedBytes.subarray(4, 4 + metadataLength);
38
64
  const metadata = (0, msgpack_1.decode)(metadataBytes);
39
65
  // Extract data
40
- const data = bytes.subarray(4 + metadataLength);
66
+ const data = normalizedBytes.subarray(4 + metadataLength);
41
67
  return {
42
68
  messageId: metadata.messageId,
43
69
  chunkIndex: metadata.chunkIndex,
@@ -51,10 +77,13 @@ class StreamedSocket {
51
77
  maxChunkSize;
52
78
  safeSocketChunkEventName = '__streamed-socket-chunk';
53
79
  safeSocketResponseEventName = '__streamed-socket-response';
80
+ stats = createSocketStats();
54
81
  constructor(socket, maxChunkSize = socket_type_1.DEFAULT_MAX_CHUNK_SIZE) {
55
82
  this.socket = socket;
56
83
  this.maxChunkSize = maxChunkSize;
57
84
  socket.on(this.safeSocketChunkEventName, (encodedChunk) => {
85
+ // Track received bytes
86
+ this.stats.bytesReceived += encodedChunk.length;
58
87
  const chunk = decodeChunk(encodedChunk);
59
88
  this.handleChunk(chunk);
60
89
  });
@@ -103,13 +132,18 @@ class StreamedSocket {
103
132
  if (chunks.length > 1) {
104
133
  console.debug(`Sending ${chunks.length} chunks for event ${eventName} with messageId ${messageId}`);
105
134
  }
135
+ let totalBytesSent = 0;
106
136
  for (const chunk of chunks) {
107
137
  // Encode chunk with metadata into single Uint8Array
108
138
  const encodedChunk = encodeChunk(chunk);
139
+ totalBytesSent += encodedChunk.length;
109
140
  // TODO on error or timeout retry
110
141
  // TODO respond to backpressure from socket
111
142
  this.socket.emit(this.safeSocketChunkEventName, encodedChunk, lodash_1.noop);
112
143
  }
144
+ // Track stats
145
+ this.stats.bytesSent += totalBytesSent;
146
+ this.stats.messagesSent++;
113
147
  }
114
148
  handlers = {};
115
149
  on(eventName, handler) {
@@ -159,6 +193,8 @@ class StreamedSocket {
159
193
  reassembled.set(chunk.data, offset);
160
194
  offset += chunk.data.length;
161
195
  }
196
+ // Track message received
197
+ this.stats.messagesReceived++;
162
198
  delete this.chunkBuffers[chunkZero.messageId];
163
199
  const eventName = chunkZero.eventName;
164
200
  const args = (0, tx_encoding_1.txDecode)(reassembled);
@@ -190,7 +226,12 @@ class StreamedSocket {
190
226
  }, lodash_1.noop);
191
227
  }
192
228
  catch (error) {
193
- console.error(`Error handling chunked request:`, error);
229
+ if (String(error).includes('No handler registered for event')) {
230
+ console.warn(String(error));
231
+ }
232
+ else {
233
+ console.error(`Error handling chunked request:`, error);
234
+ }
194
235
  return;
195
236
  }
196
237
  }
@@ -5,16 +5,34 @@ exports.txDecode = txDecode;
5
5
  const msgpack_1 = require("@msgpack/msgpack");
6
6
  const fflate_1 = require("fflate");
7
7
  const serial_json_1 = require("../serial-json");
8
+ // Minimum size before attempting compression (bytes)
9
+ // Small payloads don't benefit much from compression
10
+ const COMPRESSION_THRESHOLD = 8 * 1024; // 8KB
11
+ // Minimum compression ratio to justify the CPU cost
12
+ // If compressed isn't at least 20% smaller, skip it
13
+ const MIN_COMPRESSION_RATIO = 0.8;
14
+ // Compression level: 1 (fastest) to 9 (best compression)
15
+ // Level 1 is ~5-10x faster than level 6 with reasonable compression
16
+ const COMPRESSION_LEVEL = 1;
8
17
  function txEncode(data) {
9
18
  const noCycles = (0, serial_json_1.toJSON)(data); // remove cycles and encode dates, etc.
10
19
  // First encode to msgpack
11
20
  const encoded = (0, msgpack_1.encode)(noCycles);
12
21
  let body;
13
22
  let flag;
14
- if (encoded.length > 1024) {
15
- // Compress big payloads
16
- body = (0, fflate_1.compressSync)(encoded);
17
- flag = 1; // compressed
23
+ if (encoded.length > COMPRESSION_THRESHOLD) {
24
+ // Try to compress - use fast compression level
25
+ const compressed = (0, fflate_1.compressSync)(encoded, { level: COMPRESSION_LEVEL });
26
+ // Only use compression if it actually helps significantly
27
+ if (compressed.length < encoded.length * MIN_COMPRESSION_RATIO) {
28
+ body = compressed;
29
+ flag = 1; // compressed
30
+ }
31
+ else {
32
+ // Compression didn't help much, skip it
33
+ body = encoded;
34
+ flag = 0; // not compressed
35
+ }
18
36
  }
19
37
  else {
20
38
  body = encoded;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tx_encoding_1 = require("./tx-encoding");
4
+ const msgpack_1 = require("@msgpack/msgpack");
5
+ const serial_json_1 = require("../serial-json");
4
6
  describe('tx-encode-decode', () => {
5
7
  describe('txEncode and txDecode', () => {
6
8
  it('should encode and decode simple objects', () => {
@@ -88,14 +90,22 @@ describe('tx-encode-decode', () => {
88
90
  expect(encoded[0]).toBe(0); // flag = 0 means not compressed
89
91
  });
90
92
  it('should compress large payloads (> 1024 bytes)', () => {
91
- // Create an object that will exceed 1024 bytes when encoded
93
+ // Create an object that will exceed 8KB when msgpack-encoded
94
+ // msgpack is more compact than JSON, so we need significantly more data
95
+ // A string of 20,000 chars is roughly 20KB in JSON, but msgpack uses length prefix + bytes
96
+ // So we need even more to ensure msgpack encoding exceeds 8KB
92
97
  const largeObj = {
93
- data: 'x'.repeat(2000),
94
- array: Array.from({ length: 100 }, (_, i) => ({ id: i, value: `item ${i}` }))
98
+ data: 'x'.repeat(30000),
99
+ array: Array.from({ length: 1000 }, (_, i) => ({
100
+ id: i,
101
+ value: `item ${i} with some extra text to make it larger and ensure we exceed the threshold`,
102
+ nested: { prop: `nested value ${i}` }
103
+ }))
95
104
  };
96
- const jsonStr = JSON.stringify(largeObj);
97
- expect(jsonStr.length).toBeGreaterThan(1024);
98
105
  const encoded = (0, tx_encoding_1.txEncode)(largeObj);
106
+ // Verify the encoded size (without flag byte) exceeds 8KB threshold
107
+ const encodedSize = encoded.length - 1; // subtract flag byte
108
+ expect(encodedSize).toBeGreaterThan(8 * 1024);
99
109
  // First byte is the flag
100
110
  expect(encoded[0]).toBe(1); // flag = 1 means compressed
101
111
  // Verify it can still be decoded correctly
@@ -103,14 +113,22 @@ describe('tx-encode-decode', () => {
103
113
  expect(decoded).toEqual(largeObj);
104
114
  });
105
115
  it('should have smaller encoded size for large compressible data', () => {
116
+ // Create data that exceeds 8KB when msgpack-encoded and compresses well
117
+ // Use data with some repetition but also enough variety to ensure msgpack size is large
106
118
  const largeRepetitiveObj = {
107
- data: 'repeat '.repeat(500),
108
- values: Array.from({ length: 200 }, () => 'same string')
119
+ data: 'repeat '.repeat(10000), // Highly repetitive string - will compress well
120
+ values: Array.from({ length: 5000 }, (_, i) => `item ${i} with some text that repeats to create compressible data pattern`)
109
121
  };
122
+ // First, verify the msgpack-encoded size exceeds 8KB threshold
123
+ const noCycles = (0, serial_json_1.toJSON)(largeRepetitiveObj);
124
+ const msgpackEncoded = (0, msgpack_1.encode)(noCycles);
125
+ expect(msgpackEncoded.length).toBeGreaterThan(8 * 1024);
110
126
  const encoded = (0, tx_encoding_1.txEncode)(largeRepetitiveObj);
111
- // Compressed size should be significantly smaller than the raw data
112
- const rawDataSize = JSON.stringify(largeRepetitiveObj).length;
113
- expect(encoded.length).toBeLessThan(rawDataSize / 2);
127
+ // Verify compression flag is set (means compression was applied)
128
+ expect(encoded[0]).toBe(1); // flag = 1 means compressed
129
+ // Compressed size should be significantly smaller than the raw msgpack data
130
+ const compressedSize = encoded.length - 1; // subtract flag byte
131
+ expect(compressedSize).toBeLessThan(msgpackEncoded.length * 0.8); // Should be at least 20% smaller
114
132
  // Verify correctness
115
133
  const decoded = (0, tx_encoding_1.txDecode)(encoded);
116
134
  expect(decoded).toEqual(largeRepetitiveObj);
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export * from "./types/workflow";
12
12
  export * from "./types/workflow-logger";
13
13
  export * from "./types/workflow-run-context";
14
14
  export * from "./types/zod-types";
15
+ export * from "./device/binary-peer-connection";
16
+ export * from "./device/socket-io-binary-peer";
15
17
  export * from "./device/connection";
16
18
  export * from "./device/device";
17
19
  export * from "./device/device-election";
package/dist/index.js CHANGED
@@ -29,6 +29,8 @@ __exportStar(require("./types/workflow"), exports);
29
29
  __exportStar(require("./types/workflow-logger"), exports);
30
30
  __exportStar(require("./types/workflow-run-context"), exports);
31
31
  __exportStar(require("./types/zod-types"), exports);
32
+ __exportStar(require("./device/binary-peer-connection"), exports);
33
+ __exportStar(require("./device/socket-io-binary-peer"), exports);
32
34
  __exportStar(require("./device/connection"), exports);
33
35
  __exportStar(require("./device/device"), exports);
34
36
  __exportStar(require("./device/device-election"), exports);
@@ -3,6 +3,8 @@ import type { IPeersPackage } from "../types/peers-package";
3
3
  import { IPackage } from "../data/packages";
4
4
  export declare class PackageLoader {
5
5
  readonly dataContext: DataContext;
6
+ static PeersSDK: any;
7
+ static Zod: any;
6
8
  private packageInstances;
7
9
  require: (<T>(module: string) => T) | undefined;
8
10
  constructor(dataContext: DataContext);
@@ -8,6 +8,8 @@ const packages_1 = require("../data/packages");
8
8
  const tools_2 = require("../data/tools");
9
9
  class PackageLoader {
10
10
  dataContext;
11
+ static PeersSDK;
12
+ static Zod;
11
13
  packageInstances = {};
12
14
  require = undefined;
13
15
  constructor(dataContext) {
@@ -59,9 +61,9 @@ class PackageLoader {
59
61
  case 'PeersSDK':
60
62
  // Import all peers-sdk exports
61
63
  // return _require('peers-sdk');
62
- return _require('../index');
64
+ return PackageLoader.PeersSDK || _require('../index');
63
65
  case 'zod':
64
- return _require('zod');
66
+ return PackageLoader.Zod || _require('zod');
65
67
  default:
66
68
  // For other modules, use the standard require
67
69
  console.warn(`Package ${pkg.name} is requiring a module ${moduleId}, which is not provided by the package loader.`);
@@ -18,6 +18,7 @@ export declare const rpcServerCalls: {
18
18
  encryptData: ((value: string, groupId?: string) => Promise<string>);
19
19
  tableMethodCall: ((dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>);
20
20
  getFileContents: ((fileId: string, encoding?: BufferEncoding) => Promise<string>);
21
+ injectUIBundle: ((uiBundleFileId: string) => Promise<void>);
21
22
  resetAllDeviceSyncInfo: (() => Promise<void>);
22
23
  importGroupShare: ((groupShareJson: string) => Promise<string>);
23
24
  };
package/dist/rpc-types.js CHANGED
@@ -19,6 +19,8 @@ exports.rpcServerCalls = {
19
19
  tableMethodCall: rpcStub('tableMethodCall'),
20
20
  // TODO lock this down so not all code can get any file contents
21
21
  getFileContents: rpcStub('getFileContents'),
22
+ // Inject UI bundle directly into WebView (bypasses slow postMessage for large strings)
23
+ injectUIBundle: rpcStub('injectUIBundle'),
22
24
  resetAllDeviceSyncInfo: rpcStub('resetAllDeviceSyncInfo'),
23
25
  importGroupShare: rpcStub('importGroupShare'),
24
26
  // TODO try to get rid of this and rely on the client-side table and server-side table individually emitting events
@@ -33,7 +35,9 @@ exports.rpcClientCalls = {
33
35
  setClientPath: rpcStub('setClientPath'),
34
36
  openThread: rpcStub('openThread'),
35
37
  };
36
- exports.isClient = typeof window !== 'undefined';
38
+ // Check if we're in a web browser or webview (not React Native or Node.js)
39
+ // React Native has window but not document, so we check for both
40
+ exports.isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
37
41
  if (exports.isClient) {
38
42
  // @ts-ignore
39
43
  const _window = window;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"