react-native-ble-mesh 1.1.1 → 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 (80) hide show
  1. package/README.md +288 -172
  2. package/docs/IOS-BACKGROUND-BLE.md +231 -0
  3. package/docs/OPTIMIZATION.md +183 -0
  4. package/docs/SPEC-v2.1.md +308 -0
  5. package/package.json +1 -1
  6. package/src/MeshNetwork.js +667 -465
  7. package/src/constants/index.js +1 -0
  8. package/src/crypto/AutoCrypto.js +90 -0
  9. package/src/crypto/CryptoProvider.js +99 -0
  10. package/src/crypto/index.js +15 -63
  11. package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
  12. package/src/crypto/providers/QuickCryptoProvider.js +158 -0
  13. package/src/crypto/providers/TweetNaClProvider.js +124 -0
  14. package/src/crypto/providers/index.js +11 -0
  15. package/src/errors/MeshError.js +2 -1
  16. package/src/expo/withBLEMesh.js +102 -0
  17. package/src/hooks/AppStateManager.js +9 -1
  18. package/src/hooks/useMesh.js +47 -13
  19. package/src/hooks/useMessages.js +6 -4
  20. package/src/hooks/usePeers.js +13 -9
  21. package/src/index.js +23 -8
  22. package/src/mesh/dedup/BloomFilter.js +44 -57
  23. package/src/mesh/dedup/DedupManager.js +67 -10
  24. package/src/mesh/fragment/Assembler.js +5 -0
  25. package/src/mesh/fragment/Fragmenter.js +1 -1
  26. package/src/mesh/index.js +1 -1
  27. package/src/mesh/monitor/ConnectionQuality.js +433 -0
  28. package/src/mesh/monitor/NetworkMonitor.js +376 -320
  29. package/src/mesh/monitor/index.js +7 -3
  30. package/src/mesh/peer/Peer.js +5 -2
  31. package/src/mesh/peer/PeerManager.js +21 -4
  32. package/src/mesh/router/MessageRouter.js +38 -19
  33. package/src/mesh/router/RouteTable.js +24 -8
  34. package/src/mesh/store/StoreAndForwardManager.js +305 -296
  35. package/src/mesh/store/index.js +1 -1
  36. package/src/protocol/deserializer.js +9 -10
  37. package/src/protocol/header.js +13 -7
  38. package/src/protocol/message.js +15 -3
  39. package/src/protocol/serializer.js +7 -10
  40. package/src/protocol/validator.js +23 -5
  41. package/src/service/BatteryOptimizer.js +285 -278
  42. package/src/service/EmergencyManager.js +224 -214
  43. package/src/service/HandshakeManager.js +163 -13
  44. package/src/service/MeshService.js +72 -6
  45. package/src/service/SessionManager.js +79 -2
  46. package/src/service/audio/AudioManager.js +8 -2
  47. package/src/service/file/FileAssembler.js +106 -0
  48. package/src/service/file/FileChunker.js +79 -0
  49. package/src/service/file/FileManager.js +307 -0
  50. package/src/service/file/FileMessage.js +122 -0
  51. package/src/service/file/index.js +15 -0
  52. package/src/service/text/TextManager.js +21 -15
  53. package/src/service/text/broadcast/BroadcastManager.js +16 -0
  54. package/src/storage/MessageStore.js +55 -2
  55. package/src/transport/BLETransport.js +141 -10
  56. package/src/transport/MockTransport.js +1 -1
  57. package/src/transport/MultiTransport.js +330 -0
  58. package/src/transport/WiFiDirectTransport.js +296 -0
  59. package/src/transport/adapters/NodeBLEAdapter.js +34 -0
  60. package/src/transport/adapters/RNBLEAdapter.js +56 -1
  61. package/src/transport/index.js +6 -0
  62. package/src/utils/EventEmitter.js +6 -9
  63. package/src/utils/bytes.js +12 -10
  64. package/src/utils/compression.js +293 -291
  65. package/src/utils/encoding.js +33 -8
  66. package/src/crypto/aead.js +0 -189
  67. package/src/crypto/chacha20.js +0 -181
  68. package/src/crypto/hkdf.js +0 -187
  69. package/src/crypto/hmac.js +0 -143
  70. package/src/crypto/keys/KeyManager.js +0 -271
  71. package/src/crypto/keys/KeyPair.js +0 -216
  72. package/src/crypto/keys/SecureStorage.js +0 -219
  73. package/src/crypto/keys/index.js +0 -32
  74. package/src/crypto/noise/handshake.js +0 -410
  75. package/src/crypto/noise/index.js +0 -27
  76. package/src/crypto/noise/session.js +0 -253
  77. package/src/crypto/noise/state.js +0 -268
  78. package/src/crypto/poly1305.js +0 -113
  79. package/src/crypto/sha256.js +0 -240
  80. package/src/crypto/x25519.js +0 -154
@@ -6,9 +6,13 @@
6
6
  */
7
7
 
8
8
  const { NetworkMonitor, HEALTH_STATUS, DEFAULT_CONFIG } = require('./NetworkMonitor');
9
+ const { ConnectionQuality, PeerQualityTracker, QUALITY_LEVEL } = require('./ConnectionQuality');
9
10
 
10
11
  module.exports = {
11
- NetworkMonitor,
12
- HEALTH_STATUS,
13
- DEFAULT_CONFIG,
12
+ NetworkMonitor,
13
+ HEALTH_STATUS,
14
+ DEFAULT_CONFIG,
15
+ ConnectionQuality,
16
+ PeerQualityTracker,
17
+ QUALITY_LEVEL
14
18
  };
@@ -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
  /**
@@ -230,7 +242,12 @@ class PeerManager extends EventEmitter {
230
242
  cleanup(maxAge = this.peerTimeout) {
231
243
  const removed = [];
232
244
  for (const [id, peer] of this._peers) {
233
- if (peer.isStale(maxAge) && !peer.isConnected()) {
245
+ // Don't remove peers that are actively connecting or connected
246
+ if (peer.isConnected() || peer.connectionState === CONNECTION_STATE.CONNECTING) {
247
+ continue;
248
+ }
249
+
250
+ if (peer.isStale(maxAge)) {
234
251
  this._peers.delete(id);
235
252
  removed.push(peer);
236
253
  this.emit(EVENTS.PEER_LOST, peer);
@@ -12,17 +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)}-${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]]}`;
26
35
  }
27
36
 
28
37
  /**
@@ -148,8 +157,8 @@ class MessageRouter extends EventEmitter {
148
157
  return null;
149
158
  }
150
159
 
151
- // Check hop count
152
- if (hopCount >= (maxHops || MESH_CONFIG.MAX_HOPS)) {
160
+ // Check hop count (allow delivery at exactly maxHops, just don't relay further)
161
+ if (hopCount > (maxHops || MESH_CONFIG.MAX_HOPS)) {
153
162
  this._stats.maxHopsDropped++;
154
163
  this.emit(EVENTS.MESSAGE_DROPPED, { messageId, reason: 'max_hops' });
155
164
  return null;
@@ -165,17 +174,18 @@ class MessageRouter extends EventEmitter {
165
174
  this._routeTable.addRoute(sourcePeerId, sourcePeerId, 0);
166
175
 
167
176
  // Check if message is for us
168
- const isForUs = !message.recipientId ||
169
- message.recipientId === this.localPeerId ||
170
- (message.flags & MESSAGE_FLAGS.IS_BROADCAST);
177
+ const isForUs = !message.recipientId || message.recipientId === this.localPeerId;
178
+ const isBroadcast = (message.flags & MESSAGE_FLAGS.IS_BROADCAST) !== 0;
171
179
 
172
- if (isForUs) {
180
+ // Deliver locally if for us or broadcast
181
+ if (isForUs || isBroadcast) {
173
182
  this.emit(EVENTS.MESSAGE_RECEIVED, message);
174
183
  }
175
184
 
176
- // Relay if broadcast or not for us
177
- const shouldRelay = (message.flags & MESSAGE_FLAGS.IS_BROADCAST) ||
178
- (message.recipientId && message.recipientId !== this.localPeerId);
185
+ // Relay decision: only relay if TTL allows AND message isn't exclusively for us
186
+ const shouldRelay = isBroadcast
187
+ ? (message.hopCount < (maxHops || MESH_CONFIG.MAX_HOPS)) // Broadcasts relay if hops remain
188
+ : (!isForUs); // Unicast only relays if not for us
179
189
 
180
190
  if (shouldRelay) {
181
191
  this._relayMessage(message, sourcePeerId);
@@ -237,8 +247,11 @@ class MessageRouter extends EventEmitter {
237
247
  return [nextHop];
238
248
  }
239
249
 
240
- // No known route, flood to all
241
- return Array.from(this._peers.keys()).filter(id => id !== excludePeerId);
250
+ // No known route - use limited flooding (max 3 peers, prefer recently active)
251
+ const allPeers = Array.from(this._peers.keys()).filter(id => id !== excludePeerId);
252
+ // Limit flood scope to prevent network storms
253
+ const maxFloodPeers = Math.min(3, allPeers.length);
254
+ return allPeers.slice(0, maxFloodPeers);
242
255
  }
243
256
 
244
257
  /**
@@ -270,25 +283,31 @@ class MessageRouter extends EventEmitter {
270
283
  expiresAt: now + MESH_CONFIG.MESSAGE_TTL_MS
271
284
  };
272
285
 
273
- this._dedupManager.markSeen(messageId);
274
- this._stats.messagesSent++;
275
-
276
286
  // Determine targets
277
287
  const targets = recipientId
278
288
  ? [this._routeTable.getNextHop(recipientId) || recipientId]
279
289
  : Array.from(this._peers.keys());
280
290
 
291
+ // Send to targets
292
+ let anySent = false;
281
293
  for (const peerId of targets) {
282
294
  const sendFn = this._peers.get(peerId);
283
295
  if (sendFn) {
284
296
  try {
285
297
  sendFn(message);
298
+ anySent = true;
286
299
  } catch (err) {
287
300
  this.emit(EVENTS.ERROR, err);
288
301
  }
289
302
  }
290
303
  }
291
304
 
305
+ // Only mark as seen AFTER successful send
306
+ if (anySent) {
307
+ this._dedupManager.markSeen(messageId);
308
+ this._stats.messagesSent++;
309
+ }
310
+
292
311
  this.emit(EVENTS.MESSAGE_SENT, message);
293
312
  return messageId;
294
313
  }
@@ -99,7 +99,13 @@ class RouteTable {
99
99
 
100
100
  // Enforce max routes limit
101
101
  if (!existingRoute && this._routes.size >= this.maxRoutes) {
102
- this._evictOldestRoute();
102
+ // First try to clean up expired routes before evicting valid ones
103
+ this.cleanup();
104
+
105
+ // If still at capacity after cleanup, evict oldest
106
+ if (this._routes.size >= this.maxRoutes) {
107
+ this._evictOldestRoute();
108
+ }
103
109
  }
104
110
 
105
111
  const route = {
@@ -275,16 +281,26 @@ class RouteTable {
275
281
  * @returns {Object} Statistics
276
282
  */
277
283
  getStats() {
278
- const routes = this.getAllRoutes();
279
- 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
+ }
280
298
 
281
299
  return {
282
- totalRoutes: routes.length,
300
+ totalRoutes,
283
301
  directNeighbors: this._neighbors.size,
284
- maxHops: hopCounts.length > 0 ? Math.max(...hopCounts) : 0,
285
- avgHops: hopCounts.length > 0
286
- ? hopCounts.reduce((a, b) => a + b, 0) / hopCounts.length
287
- : 0
302
+ maxHops: totalRoutes > 0 ? maxHops : 0,
303
+ avgHops: totalRoutes > 0 ? hopSum / totalRoutes : 0
288
304
  };
289
305
  }
290
306
  }