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.
- package/README.md +2 -2
- package/docs/OPTIMIZATION.md +165 -52
- package/package.json +1 -1
- package/src/MeshNetwork.js +16 -8
- package/src/crypto/AutoCrypto.js +14 -3
- package/src/crypto/providers/ExpoCryptoProvider.js +3 -2
- package/src/crypto/providers/QuickCryptoProvider.js +30 -6
- package/src/crypto/providers/TweetNaClProvider.js +1 -1
- package/src/hooks/AppStateManager.js +9 -1
- package/src/hooks/useMesh.js +17 -4
- package/src/hooks/useMessages.js +4 -4
- package/src/hooks/usePeers.js +13 -9
- package/src/mesh/dedup/BloomFilter.js +44 -57
- package/src/mesh/dedup/DedupManager.js +32 -1
- package/src/mesh/fragment/Fragmenter.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +42 -17
- package/src/mesh/monitor/NetworkMonitor.js +58 -13
- package/src/mesh/peer/Peer.js +5 -2
- package/src/mesh/peer/PeerManager.js +15 -3
- package/src/mesh/router/MessageRouter.js +14 -6
- package/src/mesh/router/RouteTable.js +17 -7
- package/src/mesh/store/StoreAndForwardManager.js +13 -2
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +15 -3
- package/src/protocol/serializer.js +7 -10
- package/src/protocol/validator.js +23 -5
- package/src/service/BatteryOptimizer.js +4 -1
- package/src/service/HandshakeManager.js +12 -16
- package/src/service/SessionManager.js +12 -10
- package/src/service/text/TextManager.js +21 -15
- package/src/storage/MessageStore.js +55 -2
- package/src/transport/BLETransport.js +11 -2
- package/src/transport/MultiTransport.js +35 -10
- package/src/transport/WiFiDirectTransport.js +4 -3
- package/src/utils/EventEmitter.js +6 -9
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +5 -3
- 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 =
|
|
94
|
-
|
|
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
|
|
144
|
-
for (
|
|
145
|
-
this.
|
|
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
|
|
157
|
-
for (
|
|
158
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
57
|
-
this.
|
|
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.
|
|
73
|
-
if (this.
|
|
74
|
-
this._rssiSamples.
|
|
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.
|
|
85
|
-
if (this.
|
|
86
|
-
this._latencySamples.
|
|
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.
|
|
133
|
-
return this.
|
|
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.
|
|
142
|
-
return this.
|
|
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 {
|
|
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.
|
|
439
|
-
if
|
|
440
|
-
|
|
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.
|
|
508
|
+
if (this._latencyCount === 0) {
|
|
478
509
|
return 0;
|
|
479
510
|
}
|
|
480
|
-
return Math.round(
|
|
481
|
-
|
|
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) {
|
package/src/mesh/peer/Peer.js
CHANGED
|
@@ -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 (!
|
|
136
|
+
if (!CONNECTION_STATE_SET.has(state)) {
|
|
134
137
|
throw ValidationError.invalidArgument('state', state, {
|
|
135
|
-
validValues:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
285
|
-
|
|
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
|
|
300
|
+
totalRoutes,
|
|
289
301
|
directNeighbors: this._neighbors.size,
|
|
290
|
-
maxHops:
|
|
291
|
-
avgHops:
|
|
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
|
-
|
|
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.
|
|
43
|
-
const view = new DataView(
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
230
|
+
return data.subarray(8, 24);
|
|
232
231
|
}
|
|
233
232
|
|
|
234
233
|
module.exports = {
|