react-native-ble-mesh 2.0.0 → 2.1.0

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.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/docs/OPTIMIZATION.md +165 -52
  3. package/package.json +1 -1
  4. package/src/MeshNetwork.js +16 -8
  5. package/src/crypto/AutoCrypto.js +14 -3
  6. package/src/crypto/providers/ExpoCryptoProvider.js +3 -2
  7. package/src/crypto/providers/QuickCryptoProvider.js +30 -6
  8. package/src/crypto/providers/TweetNaClProvider.js +1 -1
  9. package/src/hooks/AppStateManager.js +9 -1
  10. package/src/hooks/useMesh.js +17 -4
  11. package/src/hooks/useMessages.js +4 -4
  12. package/src/hooks/usePeers.js +13 -9
  13. package/src/mesh/dedup/BloomFilter.js +44 -57
  14. package/src/mesh/dedup/DedupManager.js +32 -1
  15. package/src/mesh/fragment/Fragmenter.js +1 -1
  16. package/src/mesh/monitor/ConnectionQuality.js +42 -17
  17. package/src/mesh/monitor/NetworkMonitor.js +58 -13
  18. package/src/mesh/peer/Peer.js +5 -2
  19. package/src/mesh/peer/PeerManager.js +15 -3
  20. package/src/mesh/router/MessageRouter.js +14 -6
  21. package/src/mesh/router/RouteTable.js +17 -7
  22. package/src/mesh/store/StoreAndForwardManager.js +13 -2
  23. package/src/protocol/deserializer.js +9 -10
  24. package/src/protocol/header.js +13 -7
  25. package/src/protocol/message.js +15 -3
  26. package/src/protocol/serializer.js +7 -10
  27. package/src/protocol/validator.js +23 -5
  28. package/src/service/BatteryOptimizer.js +4 -1
  29. package/src/service/HandshakeManager.js +12 -16
  30. package/src/service/SessionManager.js +12 -10
  31. package/src/service/text/TextManager.js +21 -15
  32. package/src/storage/MessageStore.js +55 -2
  33. package/src/transport/BLETransport.js +11 -2
  34. package/src/transport/MultiTransport.js +35 -10
  35. package/src/transport/WiFiDirectTransport.js +4 -3
  36. package/src/utils/EventEmitter.js +6 -9
  37. package/src/utils/bytes.js +12 -10
  38. package/src/utils/compression.js +5 -3
  39. package/src/utils/encoding.js +33 -8
@@ -13,6 +13,15 @@
13
13
  const FNV_PRIME = 0x01000193;
14
14
  const FNV_OFFSET = 0x811c9dc5;
15
15
 
16
+ // Cached TextEncoder singleton (avoids per-call allocation)
17
+ let _encoder = null;
18
+ function _getEncoder() {
19
+ if (!_encoder && typeof TextEncoder !== 'undefined') {
20
+ _encoder = new TextEncoder();
21
+ }
22
+ return _encoder;
23
+ }
24
+
16
25
  /**
17
26
  * Bloom filter for probabilistic set membership testing
18
27
  * @class BloomFilter
@@ -56,6 +65,13 @@ class BloomFilter {
56
65
  * @private
57
66
  */
58
67
  this._count = 0;
68
+
69
+ /**
70
+ * Running count of set bits (avoids O(n) scan in getFillRatio)
71
+ * @type {number}
72
+ * @private
73
+ */
74
+ this._setBitCount = 0;
59
75
  }
60
76
 
61
77
  /**
@@ -90,72 +106,50 @@ class BloomFilter {
90
106
  return item;
91
107
  }
92
108
  if (typeof item === 'string') {
93
- const encoder = new TextEncoder();
94
- return encoder.encode(item);
109
+ const encoder = _getEncoder();
110
+ if (encoder) {
111
+ return encoder.encode(item);
112
+ }
113
+ // Fallback for environments without TextEncoder
114
+ const bytes = new Uint8Array(item.length);
115
+ for (let i = 0; i < item.length; i++) {
116
+ bytes[i] = item.charCodeAt(i) & 0xff;
117
+ }
118
+ return bytes;
95
119
  }
96
120
  throw new Error('Item must be string or Uint8Array');
97
121
  }
98
122
 
99
- /**
100
- * Computes hash positions for an item
101
- * @param {string|Uint8Array} item - Item to hash
102
- * @returns {number[]} Array of bit positions
103
- * @private
104
- */
105
- _getPositions(item) {
106
- const data = this._toBytes(item);
107
- const positions = [];
108
- for (let i = 0; i < this.hashCount; i++) {
109
- const hash = this._fnv1a(data, i);
110
- positions.push(hash % this.size);
111
- }
112
- return positions;
113
- }
114
-
115
- /**
116
- * Sets a bit at the given position
117
- * @param {number} position - Bit position
118
- * @private
119
- */
120
- _setBit(position) {
121
- const byteIndex = Math.floor(position / 8);
122
- const bitIndex = position % 8;
123
- this._bits[byteIndex] |= (1 << bitIndex);
124
- }
125
-
126
- /**
127
- * Gets a bit at the given position
128
- * @param {number} position - Bit position
129
- * @returns {boolean} True if bit is set
130
- * @private
131
- */
132
- _getBit(position) {
133
- const byteIndex = Math.floor(position / 8);
134
- const bitIndex = position % 8;
135
- return (this._bits[byteIndex] & (1 << bitIndex)) !== 0;
136
- }
137
-
138
123
  /**
139
124
  * Adds an item to the filter
125
+ * Inlined position computation to avoid intermediate array allocation
140
126
  * @param {string|Uint8Array} item - Item to add
141
127
  */
142
128
  add(item) {
143
- const positions = this._getPositions(item);
144
- for (const pos of positions) {
145
- this._setBit(pos);
129
+ const data = this._toBytes(item);
130
+ for (let i = 0; i < this.hashCount; i++) {
131
+ const pos = this._fnv1a(data, i) % this.size;
132
+ const byteIndex = pos >> 3;
133
+ const mask = 1 << (pos & 7);
134
+ if (!(this._bits[byteIndex] & mask)) {
135
+ this._bits[byteIndex] |= mask;
136
+ this._setBitCount++;
137
+ }
146
138
  }
147
139
  this._count++;
148
140
  }
149
141
 
150
142
  /**
151
143
  * Tests if an item might be in the filter
144
+ * Inlined position computation to avoid intermediate array allocation
152
145
  * @param {string|Uint8Array} item - Item to test
153
146
  * @returns {boolean} True if item might be present, false if definitely absent
154
147
  */
155
148
  mightContain(item) {
156
- const positions = this._getPositions(item);
157
- for (const pos of positions) {
158
- if (!this._getBit(pos)) {
149
+ const data = this._toBytes(item);
150
+ for (let i = 0; i < this.hashCount; i++) {
151
+ const pos = this._fnv1a(data, i) % this.size;
152
+ if (!(this._bits[pos >> 3] & (1 << (pos & 7)))) {
159
153
  return false;
160
154
  }
161
155
  }
@@ -168,22 +162,15 @@ class BloomFilter {
168
162
  clear() {
169
163
  this._bits.fill(0);
170
164
  this._count = 0;
165
+ this._setBitCount = 0;
171
166
  }
172
167
 
173
168
  /**
174
- * Gets the fill ratio of the filter
169
+ * Gets the fill ratio of the filter (O(1) using running count)
175
170
  * @returns {number} Ratio of set bits to total bits (0-1)
176
171
  */
177
172
  getFillRatio() {
178
- let setBits = 0;
179
- for (let i = 0; i < this._bits.length; i++) {
180
- let byte = this._bits[i];
181
- while (byte) {
182
- setBits += byte & 1;
183
- byte >>>= 1;
184
- }
185
- }
186
- return setBits / this.size;
173
+ return this._setBitCount / this.size;
187
174
  }
188
175
 
189
176
  /**
@@ -69,6 +69,13 @@ class DedupManager {
69
69
  */
70
70
  this._autoResetThreshold = config.autoResetThreshold;
71
71
 
72
+ /**
73
+ * Grace period timer ID for bloom filter reset
74
+ * @type {ReturnType<typeof setTimeout>|null}
75
+ * @private
76
+ */
77
+ this._graceTimer = null;
78
+
72
79
  /**
73
80
  * Statistics for monitoring
74
81
  * @type {Object}
@@ -165,9 +172,15 @@ class DedupManager {
165
172
  // Keep old filter for a grace period by checking both
166
173
  this._oldBloomFilter = oldFilter;
167
174
 
175
+ // Clear any existing grace timer before starting a new one
176
+ if (this._graceTimer) {
177
+ clearTimeout(this._graceTimer);
178
+ }
179
+
168
180
  // Clear old filter after grace period
169
- setTimeout(() => {
181
+ this._graceTimer = setTimeout(() => {
170
182
  this._oldBloomFilter = null;
183
+ this._graceTimer = null;
171
184
  }, 60000); // 1 minute grace
172
185
  }
173
186
 
@@ -175,6 +188,11 @@ class DedupManager {
175
188
  * Resets both the Bloom filter and cache
176
189
  */
177
190
  reset() {
191
+ if (this._graceTimer) {
192
+ clearTimeout(this._graceTimer);
193
+ this._graceTimer = null;
194
+ }
195
+ this._oldBloomFilter = null;
178
196
  this._bloomFilter.clear();
179
197
  this._cache.clear();
180
198
  this._stats.resets++;
@@ -233,6 +251,19 @@ class DedupManager {
233
251
  getSeenTimestamp(messageId) {
234
252
  return this._cache.getTimestamp(messageId);
235
253
  }
254
+
255
+ /**
256
+ * Destroys the dedup manager and cleans up resources
257
+ */
258
+ destroy() {
259
+ if (this._graceTimer) {
260
+ clearTimeout(this._graceTimer);
261
+ this._graceTimer = null;
262
+ }
263
+ this._oldBloomFilter = null;
264
+ this._bloomFilter.clear();
265
+ this._cache.clear();
266
+ }
236
267
  }
237
268
 
238
269
  module.exports = DedupManager;
@@ -121,7 +121,7 @@ function fragment(payload, messageId, maxSize = DEFAULT_FRAGMENT_SIZE) {
121
121
  for (let i = 0; i < fragmentCount; i++) {
122
122
  const remainingLength = payload.length - offset;
123
123
  const chunkLength = Math.min(payloadCapacity, remainingLength);
124
- const chunk = payload.slice(offset, offset + chunkLength);
124
+ const chunk = payload.subarray(offset, offset + chunkLength);
125
125
 
126
126
  // Create fragment with header
127
127
  const header = createFragmentHeader(i, fragmentCount, chunkLength);
@@ -53,8 +53,16 @@ class PeerQualityTracker {
53
53
  constructor(peerId, config) {
54
54
  this.peerId = peerId;
55
55
  this._config = config;
56
- this._rssiSamples = [];
57
- this._latencySamples = [];
56
+ // Circular buffer for RSSI samples
57
+ this._rssiSamples = new Float64Array(config.sampleSize);
58
+ this._rssiIndex = 0;
59
+ this._rssiCount = 0;
60
+ this._rssiSum = 0;
61
+ // Circular buffer for latency samples
62
+ this._latencySamples = new Float64Array(config.sampleSize);
63
+ this._latencyIndex = 0;
64
+ this._latencyCount = 0;
65
+ this._latencySum = 0;
58
66
  this._packetsSent = 0;
59
67
  this._packetsAcked = 0;
60
68
  this._bytesTransferred = 0;
@@ -65,25 +73,37 @@ class PeerQualityTracker {
65
73
  }
66
74
 
67
75
  /**
68
- * Records an RSSI sample
76
+ * Records an RSSI sample using circular buffer
69
77
  * @param {number} rssi - Signal strength in dBm
70
78
  */
71
79
  recordRssi(rssi) {
72
- this._rssiSamples.push(rssi);
73
- if (this._rssiSamples.length > this._config.sampleSize) {
74
- this._rssiSamples.shift();
80
+ const capacity = this._rssiSamples.length;
81
+ if (this._rssiCount >= capacity) {
82
+ this._rssiSum -= this._rssiSamples[this._rssiIndex];
83
+ }
84
+ this._rssiSamples[this._rssiIndex] = rssi;
85
+ this._rssiSum += rssi;
86
+ this._rssiIndex = (this._rssiIndex + 1) % capacity;
87
+ if (this._rssiCount < capacity) {
88
+ this._rssiCount++;
75
89
  }
76
90
  this._lastActivity = Date.now();
77
91
  }
78
92
 
79
93
  /**
80
- * Records a latency measurement
94
+ * Records a latency measurement using circular buffer
81
95
  * @param {number} latencyMs - Round-trip latency in ms
82
96
  */
83
97
  recordLatency(latencyMs) {
84
- this._latencySamples.push(latencyMs);
85
- if (this._latencySamples.length > this._config.sampleSize) {
86
- this._latencySamples.shift();
98
+ const capacity = this._latencySamples.length;
99
+ if (this._latencyCount >= capacity) {
100
+ this._latencySum -= this._latencySamples[this._latencyIndex];
101
+ }
102
+ this._latencySamples[this._latencyIndex] = latencyMs;
103
+ this._latencySum += latencyMs;
104
+ this._latencyIndex = (this._latencyIndex + 1) % capacity;
105
+ if (this._latencyCount < capacity) {
106
+ this._latencyCount++;
87
107
  }
88
108
  this._lastActivity = Date.now();
89
109
  }
@@ -125,21 +145,21 @@ class PeerQualityTracker {
125
145
  }
126
146
 
127
147
  /**
128
- * Gets the average RSSI
148
+ * Gets the average RSSI (O(1) using running sum)
129
149
  * @returns {number|null}
130
150
  */
131
151
  getAvgRssi() {
132
- if (this._rssiSamples.length === 0) { return null; }
133
- return this._rssiSamples.reduce((a, b) => a + b, 0) / this._rssiSamples.length;
152
+ if (this._rssiCount === 0) { return null; }
153
+ return this._rssiSum / this._rssiCount;
134
154
  }
135
155
 
136
156
  /**
137
- * Gets the average latency
157
+ * Gets the average latency (O(1) using running sum)
138
158
  * @returns {number|null}
139
159
  */
140
160
  getAvgLatency() {
141
- if (this._latencySamples.length === 0) { return null; }
142
- return this._latencySamples.reduce((a, b) => a + b, 0) / this._latencySamples.length;
161
+ if (this._latencyCount === 0) { return null; }
162
+ return this._latencySum / this._latencyCount;
143
163
  }
144
164
 
145
165
  /**
@@ -388,10 +408,15 @@ class ConnectionQuality extends EventEmitter {
388
408
  }
389
409
 
390
410
  /**
391
- * Periodic update — recalculates all qualities and emits changes
411
+ * Periodic update — recalculates all qualities and emits changes.
412
+ * Stops the timer automatically when no peers are connected.
392
413
  * @private
393
414
  */
394
415
  _update() {
416
+ if (this._peers.size === 0) {
417
+ this.stop();
418
+ return;
419
+ }
395
420
  for (const tracker of this._peers.values()) {
396
421
  const quality = tracker.calculate();
397
422
  if (quality.changed) {
@@ -117,11 +117,32 @@ class NetworkMonitor extends EventEmitter {
117
117
  this._pendingMessages = new Map();
118
118
 
119
119
  /**
120
- * Latency samples
121
- * @type {number[]}
120
+ * Latency samples (circular buffer)
121
+ * @type {Float64Array}
122
122
  * @private
123
123
  */
124
- this._latencies = [];
124
+ this._latencies = new Float64Array(this._config.latencySampleSize);
125
+
126
+ /**
127
+ * Circular buffer write index
128
+ * @type {number}
129
+ * @private
130
+ */
131
+ this._latencyIndex = 0;
132
+
133
+ /**
134
+ * Number of latency samples stored
135
+ * @type {number}
136
+ * @private
137
+ */
138
+ this._latencyCount = 0;
139
+
140
+ /**
141
+ * Running sum of latency samples for O(1) average
142
+ * @type {number}
143
+ * @private
144
+ */
145
+ this._latencySum = 0;
125
146
 
126
147
  /**
127
148
  * Global statistics
@@ -386,7 +407,10 @@ class NetworkMonitor extends EventEmitter {
386
407
  reset() {
387
408
  this._nodes.clear();
388
409
  this._pendingMessages.clear();
389
- this._latencies = [];
410
+ this._latencies = new Float64Array(this._config.latencySampleSize);
411
+ this._latencyIndex = 0;
412
+ this._latencyCount = 0;
413
+ this._latencySum = 0;
390
414
  this._stats = {
391
415
  totalMessagesSent: 0,
392
416
  totalMessagesDelivered: 0,
@@ -430,14 +454,21 @@ class NetworkMonitor extends EventEmitter {
430
454
  }
431
455
 
432
456
  /**
433
- * Adds a latency sample.
457
+ * Adds a latency sample using circular buffer.
434
458
  * @param {number} latency - Latency in ms
435
459
  * @private
436
460
  */
437
461
  _addLatencySample(latency) {
438
- this._latencies.push(latency);
439
- if (this._latencies.length > this._config.latencySampleSize) {
440
- this._latencies.shift();
462
+ const capacity = this._latencies.length;
463
+ // Subtract the old value being overwritten if buffer is full
464
+ if (this._latencyCount >= capacity) {
465
+ this._latencySum -= this._latencies[this._latencyIndex];
466
+ }
467
+ this._latencies[this._latencyIndex] = latency;
468
+ this._latencySum += latency;
469
+ this._latencyIndex = (this._latencyIndex + 1) % capacity;
470
+ if (this._latencyCount < capacity) {
471
+ this._latencyCount++;
441
472
  }
442
473
  }
443
474
 
@@ -469,17 +500,30 @@ class NetworkMonitor extends EventEmitter {
469
500
  }
470
501
 
471
502
  /**
472
- * Calculates average latency from samples.
503
+ * Calculates average latency from samples (O(1) using running sum).
473
504
  * @returns {number} Average latency
474
505
  * @private
475
506
  */
476
507
  _calculateAverageLatency() {
477
- if (this._latencies.length === 0) {
508
+ if (this._latencyCount === 0) {
478
509
  return 0;
479
510
  }
480
- return Math.round(
481
- this._latencies.reduce((a, b) => a + b, 0) / this._latencies.length
482
- );
511
+ return Math.round(this._latencySum / this._latencyCount);
512
+ }
513
+
514
+ /**
515
+ * Cleans up stale pending messages older than nodeTimeoutMs.
516
+ * @private
517
+ */
518
+ _cleanupPendingMessages() {
519
+ const now = Date.now();
520
+ const timeout = this._config.nodeTimeoutMs;
521
+ for (const [messageId, pending] of this._pendingMessages) {
522
+ if (now - pending.timestamp > timeout) {
523
+ this._pendingMessages.delete(messageId);
524
+ this._stats.totalMessagesFailed++;
525
+ }
526
+ }
483
527
  }
484
528
 
485
529
  /**
@@ -520,6 +564,7 @@ class NetworkMonitor extends EventEmitter {
520
564
  this._healthCheckTimer = setInterval(
521
565
  () => {
522
566
  try {
567
+ this._cleanupPendingMessages();
523
568
  const report = this.generateHealthReport();
524
569
  this.emit('health-report', report);
525
570
  } catch (error) {
@@ -8,6 +8,9 @@
8
8
  const { CONNECTION_STATE } = require('../../constants');
9
9
  const { ValidationError } = require('../../errors');
10
10
 
11
+ /** Pre-computed Set of valid connection states for O(1) lookup */
12
+ const CONNECTION_STATE_SET = new Set(Object.values(CONNECTION_STATE));
13
+
11
14
  /**
12
15
  * Represents a peer in the mesh network
13
16
  * @class Peer
@@ -130,9 +133,9 @@ class Peer {
130
133
  * @param {string} state - New connection state
131
134
  */
132
135
  setConnectionState(state) {
133
- if (!Object.values(CONNECTION_STATE).includes(state)) {
136
+ if (!CONNECTION_STATE_SET.has(state)) {
134
137
  throw ValidationError.invalidArgument('state', state, {
135
- validValues: Object.values(CONNECTION_STATE)
138
+ validValues: Array.from(CONNECTION_STATE_SET)
136
139
  });
137
140
  }
138
141
  this.connectionState = state;
@@ -109,7 +109,11 @@ class PeerManager extends EventEmitter {
109
109
  * @returns {Peer[]} Array of connected peers
110
110
  */
111
111
  getConnectedPeers() {
112
- return this.getAllPeers().filter(peer => peer.isConnected());
112
+ const result = [];
113
+ for (const peer of this._peers.values()) {
114
+ if (peer.isConnected()) { result.push(peer); }
115
+ }
116
+ return result;
113
117
  }
114
118
 
115
119
  /**
@@ -117,7 +121,11 @@ class PeerManager extends EventEmitter {
117
121
  * @returns {Peer[]} Array of secured peers
118
122
  */
119
123
  getSecuredPeers() {
120
- return this.getAllPeers().filter(peer => peer.isSecured());
124
+ const result = [];
125
+ for (const peer of this._peers.values()) {
126
+ if (peer.isSecured()) { result.push(peer); }
127
+ }
128
+ return result;
121
129
  }
122
130
 
123
131
  /**
@@ -125,7 +133,11 @@ class PeerManager extends EventEmitter {
125
133
  * @returns {Peer[]} Array of direct peers
126
134
  */
127
135
  getDirectPeers() {
128
- return this.getAllPeers().filter(peer => peer.isDirect());
136
+ const result = [];
137
+ for (const peer of this._peers.values()) {
138
+ if (peer.isDirect()) { result.push(peer); }
139
+ }
140
+ return result;
129
141
  }
130
142
 
131
143
  /**
@@ -12,18 +12,26 @@ const { DedupManager } = require('../dedup');
12
12
  const RouteTable = require('./RouteTable');
13
13
  const { randomBytes } = require('../../utils/bytes');
14
14
 
15
+ /**
16
+ * Hex lookup table for fast byte-to-hex conversion
17
+ * @constant {string[]}
18
+ * @private
19
+ */
20
+ const HEX = Array.from({ length: 256 }, (_, i) => (i < 16 ? '0' : '') + i.toString(16));
21
+
15
22
  /**
16
23
  * Generates a UUID v4 string
17
24
  * @returns {string} UUID string
18
25
  * @private
19
26
  */
20
27
  function generateUUID() {
21
- const bytes = randomBytes(16);
22
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
23
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
24
- const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
25
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-` +
26
- `${hex.slice(16, 20)}-${hex.slice(20)}`;
28
+ const b = randomBytes(16);
29
+ b[6] = (b[6] & 0x0f) | 0x40;
30
+ b[8] = (b[8] & 0x3f) | 0x80;
31
+ return `${HEX[b[0]]}${HEX[b[1]]}${HEX[b[2]]}${HEX[b[3]]}-${
32
+ HEX[b[4]]}${HEX[b[5]]}-${HEX[b[6]]}${HEX[b[7]]}-${
33
+ HEX[b[8]]}${HEX[b[9]]}-${HEX[b[10]]}${HEX[b[11]]}${
34
+ HEX[b[12]]}${HEX[b[13]]}${HEX[b[14]]}${HEX[b[15]]}`;
27
35
  }
28
36
 
29
37
  /**
@@ -281,16 +281,26 @@ class RouteTable {
281
281
  * @returns {Object} Statistics
282
282
  */
283
283
  getStats() {
284
- const routes = this.getAllRoutes();
285
- const hopCounts = routes.map(r => r.hopCount);
284
+ const now = Date.now();
285
+ let totalRoutes = 0;
286
+ let maxHops = 0;
287
+ let hopSum = 0;
288
+
289
+ for (const [, route] of this._routes) {
290
+ if (now <= route.expiresAt) {
291
+ totalRoutes++;
292
+ if (route.hopCount > maxHops) {
293
+ maxHops = route.hopCount;
294
+ }
295
+ hopSum += route.hopCount;
296
+ }
297
+ }
286
298
 
287
299
  return {
288
- totalRoutes: routes.length,
300
+ totalRoutes,
289
301
  directNeighbors: this._neighbors.size,
290
- maxHops: hopCounts.length > 0 ? Math.max(...hopCounts) : 0,
291
- avgHops: hopCounts.length > 0
292
- ? hopCounts.reduce((a, b) => a + b, 0) / hopCounts.length
293
- : 0
302
+ maxHops: totalRoutes > 0 ? maxHops : 0,
303
+ avgHops: totalRoutes > 0 ? hopSum / totalRoutes : 0
294
304
  };
295
305
  }
296
306
  }
@@ -11,6 +11,14 @@
11
11
  const EventEmitter = require('../../utils/EventEmitter');
12
12
  const { EVENTS } = require('../../constants');
13
13
  const { ValidationError } = require('../../errors');
14
+ const { randomBytes } = require('../../utils/bytes');
15
+
16
+ /**
17
+ * Hex lookup table for fast byte-to-hex conversion
18
+ * @constant {string[]}
19
+ * @private
20
+ */
21
+ const HEX = Array.from({ length: 256 }, (_, i) => (i < 16 ? '0' : '') + i.toString(16));
14
22
 
15
23
  /**
16
24
  * Default configuration for store and forward
@@ -439,9 +447,12 @@ class StoreAndForwardManager extends EventEmitter {
439
447
  * @private
440
448
  */
441
449
  _generateId() {
442
- const { randomBytes } = require('../../utils/bytes');
443
450
  const bytes = randomBytes(16);
444
- return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
451
+ let id = '';
452
+ for (let i = 0; i < bytes.length; i++) {
453
+ id += HEX[bytes[i]];
454
+ }
455
+ return id;
445
456
  }
446
457
 
447
458
  /**
@@ -39,15 +39,14 @@ function deserializeHeader(data) {
39
39
  });
40
40
  }
41
41
 
42
- const headerData = data.slice(0, HEADER_SIZE);
43
- const view = new DataView(headerData.buffer, headerData.byteOffset, HEADER_SIZE);
42
+ const headerData = data.subarray(0, HEADER_SIZE);
43
+ const view = new DataView(data.buffer, data.byteOffset, HEADER_SIZE);
44
44
 
45
45
  // Extract checksum from bytes 44-47 (big-endian)
46
46
  const storedChecksum = view.getUint32(44, false);
47
47
 
48
- // Calculate checksum over bytes 0-43
49
- const checksumData = headerData.slice(0, 44);
50
- const calculatedChecksum = crc32(checksumData);
48
+ // Calculate checksum over bytes 0-43 (subarray = zero-copy view)
49
+ const calculatedChecksum = crc32(headerData.subarray(0, 44));
51
50
 
52
51
  // Verify checksum
53
52
  if (storedChecksum !== calculatedChecksum) {
@@ -65,7 +64,7 @@ function deserializeHeader(data) {
65
64
  hopCount: headerData[3],
66
65
  maxHops: headerData[4],
67
66
  // bytes 5-7 are reserved
68
- messageId: headerData.slice(8, 24),
67
+ messageId: new Uint8Array(headerData.buffer, headerData.byteOffset + 8, 16),
69
68
  timestamp: readUint64BE(view, 24),
70
69
  expiresAt: readUint64BE(view, 32),
71
70
  payloadLength: view.getUint16(40, false),
@@ -120,8 +119,8 @@ function deserialize(data) {
120
119
  });
121
120
  }
122
121
 
123
- // Extract payload
124
- const payload = data.slice(HEADER_SIZE, HEADER_SIZE + header.payloadLength);
122
+ // Extract payload (subarray = zero-copy view)
123
+ const payload = data.subarray(HEADER_SIZE, HEADER_SIZE + header.payloadLength);
125
124
 
126
125
  return new Message(header, payload);
127
126
  }
@@ -175,7 +174,7 @@ function deserializeBatch(data) {
175
174
  }
176
175
 
177
176
  // Extract and deserialize this message
178
- const messageData = data.slice(offset, offset + messageLength);
177
+ const messageData = data.subarray(offset, offset + messageLength);
179
178
 
180
179
  try {
181
180
  const message = deserialize(messageData);
@@ -228,7 +227,7 @@ function peekMessageId(data) {
228
227
  if (!(data instanceof Uint8Array) || data.length < 24) {
229
228
  return null;
230
229
  }
231
- return data.slice(8, 24);
230
+ return data.subarray(8, 24);
232
231
  }
233
232
 
234
233
  module.exports = {