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
package/README.md CHANGED
@@ -558,7 +558,7 @@ react-native-ble-mesh/
558
558
  │ └── hooks/ # React hooks
559
559
  ├── docs/ # Guides & specs
560
560
  ├── app.plugin.js # Expo plugin entry
561
- └── __tests__/ # 432 tests, 0 failures ✅
561
+ └── __tests__/ # 433 tests, 0 failures ✅
562
562
  ```
563
563
 
564
564
  ---
@@ -579,7 +579,7 @@ react-native-ble-mesh/
579
579
  ## Testing
580
580
 
581
581
  ```bash
582
- npm test # Run all 432 tests
582
+ npm test # Run all 433 tests
583
583
  npm run test:coverage # With coverage report
584
584
  ```
585
585
 
@@ -1,70 +1,183 @@
1
- # Optimization & Technical Improvements
1
+ # Performance Optimization Guide
2
2
 
3
- ## Summary of Changes
3
+ ## v2.1.0 Performance Overhaul
4
4
 
5
- ### 🔴 Breaking: Crypto Module Removed
6
- The pure JavaScript cryptographic implementations (`src/crypto/`) have been **removed entirely**. This includes:
7
- - X25519 key exchange (pure JS BigInt — extremely slow)
8
- - ChaCha20-Poly1305 AEAD encryption
9
- - SHA-256 hashing
10
- - HMAC-SHA256
11
- - HKDF key derivation
12
- - Noise Protocol XX handshake
5
+ Version 2.1.0 is a comprehensive performance optimization release targeting React Native speed, reduced GC pressure, and elimination of memory leaks. All changes are non-breaking.
13
6
 
14
- **Why:** Pure JS BigInt field arithmetic for X25519 is orders of magnitude slower than native implementations. On mobile devices, this caused:
15
- - ~100ms+ per key exchange (vs ~1ms with native)
16
- - Battery drain from CPU-intensive crypto
17
- - UI thread blocking on Hermes/JSC
7
+ ### Key Principles Applied
8
+
9
+ 1. **Zero-copy where possible** Use `Uint8Array.subarray()` instead of `slice()` for read-only views
10
+ 2. **Cache singletons** `TextEncoder`, `TextDecoder`, crypto providers, hex lookup tables
11
+ 3. **Avoid allocation in hot paths** — Inline computations, reuse buffers, pre-compute constants
12
+ 4. **O(1) over O(n)** — Circular buffers, running sums, pre-computed Sets
13
+ 5. **Clean up resources** — Clear timers, remove event listeners, bound map growth
14
+
15
+ ---
16
+
17
+ ## Hot Path Optimizations
18
+
19
+ ### Message Processing Pipeline
20
+
21
+ Every message flows through: transport → deserialize → dedup → route → serialize → transport. Each stage was optimized:
22
+
23
+ | Stage | Before | After | Improvement |
24
+ |-------|--------|-------|-------------|
25
+ | Deserialize header | 3x `slice()` copies | 0 copies (`subarray()`) | ~3x fewer allocations |
26
+ | Deserialize payload | 1x `slice()` copy | 0 copies (`subarray()`) | Zero-copy payload |
27
+ | CRC32 checksum | `slice(0, 44)` copy | `subarray(0, 44)` view | Zero-copy |
28
+ | BloomFilter check | `new TextEncoder()` + array alloc | Cached encoder, inlined positions | 3 fewer allocations/msg |
29
+ | BloomFilter fill ratio | O(n) bit scan | O(1) running counter | Constant time |
30
+ | Serialize header | `slice(0, 44)` for CRC | `subarray(0, 44)` view | Zero-copy |
31
+ | Hex conversion | `Array.from().map().join()` | Pre-computed lookup table | ~5x faster |
32
+ | UUID generation | 16 temp strings + join | Hex table + concatenation | ~3x faster |
33
+ | Encrypt/Decrypt nonce | `new Uint8Array(24)` per call | Pre-allocated per session | 0 allocations |
34
+
35
+ ### TextEncoder/TextDecoder Caching
36
+
37
+ Before v2.1.0, `new TextEncoder()` was called on every:
38
+ - Message validation (byte length check)
39
+ - Message encoding (broadcast, private, channel)
40
+ - BloomFilter dedup check (string → bytes)
41
+ - File chunk encoding (per-chunk in loop!)
42
+ - Protocol serialization (string payloads)
43
+ - Read receipt handling (per-receipt in loop)
44
+
45
+ Now: **One singleton per module**, created once at import time.
46
+
47
+ ### Crypto Provider Caching
48
+
49
+ - `AutoCrypto.detectProvider()` — Now caches the singleton result
50
+ - `QuickCryptoProvider` — Caches `require('tweetnacl')` result instead of calling per encrypt/decrypt
51
+ - DER header buffers for X25519 — Hoisted to module level (were re-created from hex per handshake)
52
+
53
+ ---
54
+
55
+ ## Memory Leak Fixes
56
+
57
+ ### Timer Leaks
58
+
59
+ | Location | Issue | Fix |
60
+ |----------|-------|-----|
61
+ | `MeshNetwork.sendFile()` | Per-chunk timeout timers never cleared on success | `clearTimeout()` in both success/error paths |
62
+ | `DedupManager._resetBloomFilter()` | Grace period timer leaked on repeated resets | Store timer ID, clear on reset/destroy |
63
+
64
+ ### Event Listener Leaks
65
+
66
+ | Location | Issue | Fix |
67
+ |----------|-------|-----|
68
+ | `MultiTransport._wireTransport()` | Handlers never removed across start/stop | Store references, remove in `stop()` |
69
+ | `useMesh.js` initialize | Listeners stacked on re-init | Store in refs, remove old before adding new |
70
+ | `AppStateManager` | `.bind()` per initialize, old subscription not removed | Bind once in constructor, remove old sub |
71
+
72
+ ### Unbounded Growth
73
+
74
+ | Location | Issue | Fix |
75
+ |----------|-------|-----|
76
+ | `NetworkMonitor._pendingMessages` | Undelivered messages never removed | Cleanup entries older than `nodeTimeoutMs` |
77
+ | `ConnectionQuality._peers` | Timer runs with 0 peers | Stop timer when last peer removed |
78
+ | `BLETransport.disconnectFromPeer()` | Write queue/writing maps not cleaned | Added cleanup in disconnect |
79
+
80
+ ---
81
+
82
+ ## Data Structure Improvements
83
+
84
+ ### Circular Buffers (O(n) → O(1))
85
+
86
+ `Array.shift()` is O(n) because it re-indexes all elements. For sliding windows that shift on every sample, this was a significant cost.
87
+
88
+ **Replaced with circular buffers:**
89
+ - `NetworkMonitor._latencies` → `Float64Array` ring buffer with running sum
90
+ - `ConnectionQuality._rssiSamples` → `Float64Array` ring buffer with running sum
91
+ - `ConnectionQuality._latencySamples` → `Float64Array` ring buffer with running sum
18
92
 
19
- **What to use instead:**
20
- - [`tweetnacl`](https://www.npmjs.com/package/tweetnacl) — Lightweight, audited, works everywhere (recommended)
21
- - [`libsodium-wrappers`](https://www.npmjs.com/package/libsodium-wrappers) — Full-featured, WASM-based
22
- - [`react-native-quick-crypto`](https://www.npmjs.com/package/react-native-quick-crypto) — Native crypto for RN (fastest)
93
+ Average computation is now O(1) via running sum instead of O(n) `reduce()`.
23
94
 
24
- Consumers should implement their own encryption layer using these established libraries.
95
+ ### Pre-Computed Sets (O(n) O(1))
25
96
 
26
- ### 🟢 Bug Fixes
97
+ `Object.values(ENUM).includes(value)` creates an array and does linear scan on every call.
27
98
 
28
- 1. **MeshNetwork restart** Fixed crash when calling `start()` after `stop()`. The service was trying to re-initialize (state check failed). Now skips initialization if already initialized.
99
+ **Replaced with module-level `Set`s:**
100
+ - `Peer.setConnectionState()` — `CONNECTION_STATE_SET.has(state)`
101
+ - `BatteryOptimizer.setMode()` — `BATTERY_MODE_SET.has(mode)`
29
102
 
30
- 2. **MockTransport auto-ID** — `MockTransport` now auto-generates a `localPeerId` if none provided, preventing "localPeerId required" errors when linking transports.
103
+ ### Direct Map Iteration
31
104
 
32
- 3. **Error message clarity** — All error classes (MeshError, ValidationError, ConnectionError, etc.) now prefix messages with the error class name (e.g., `"ValidationError: Invalid type"`), making error identification easier in catch blocks and logs.
105
+ `Array.from(map.values()).filter(...)` creates two arrays. Direct iteration creates one:
33
106
 
34
- ### 🟡 Performance Optimizations
107
+ ```js
108
+ // Before (2 arrays)
109
+ getConnectedPeers() { return this.getAllPeers().filter(p => p.isConnected()); }
35
110
 
36
- 4. **BLE connection timeout cleanup** — Fixed timer leak in `BLETransport.connectToPeer()`. The timeout timer was never cleared on successful connection, leaking memory. Now properly clears the timer when connection succeeds.
111
+ // After (1 array)
112
+ getConnectedPeers() {
113
+ const result = [];
114
+ for (const peer of this._peers.values()) {
115
+ if (peer.isConnected()) result.push(peer);
116
+ }
117
+ return result;
118
+ }
119
+ ```
37
120
 
38
- ### 🧪 Test Improvements
121
+ Applied to: `PeerManager.getConnectedPeers()`, `getSecuredPeers()`, `getDirectPeers()`
39
122
 
40
- - **Fixed all 10 previously failing tests** (was 396 total, 10 failing → 344 total, 0 failing)
41
- - **Added new test suites:**
42
- - `__tests__/transport/BLETransport.test.js` — Lifecycle, scanning, connections, broadcast, timeout handling
43
- - `__tests__/transport/MockTransport.test.js` — Linking, message passing, peer simulation
44
- - `__tests__/mesh/MeshNetwork.unit.test.js` — Config merging, validation, lifecycle, restart
45
- - `__tests__/service/BatteryOptimizer.test.js` — Mode switching, battery levels, cleanup
46
- - `__tests__/service/MeshService.test.js` — Full lifecycle, identity, peers, messaging
47
- - `__tests__/platform/ios.test.js` — Background mode, MTU fragmentation, state restoration
48
- - `__tests__/platform/android.test.js` — Permissions, MTU (23/512), Doze mode, memory pressure, BloomFilter FP rate
123
+ ---
49
124
 
50
- ### 📱 Platform Compatibility Verified
125
+ ## React Native Hook Optimizations
51
126
 
52
- **iOS:**
53
- - BLE background mode behavior tested
54
- - MTU 185 (BLE 4.2+) fragmentation verified
55
- - Battery optimizer integration tested
56
- - Store-and-forward for state restoration
127
+ ### usePeers — Eliminated Double Re-render
57
128
 
58
- **Android:**
59
- - BLE permission denial handled gracefully
60
- - MTU 23 (BLE 4.0) and 512 (BLE 5.0) fragmentation tested
61
- - Doze mode with low-power settings verified
62
- - LRU cache respects size limits under memory pressure
63
- - BloomFilter false positive rate verified (<20% at reasonable capacity)
129
+ Before: Every peer event called both `setPeers()` and `setLastUpdate(Date.now())`, causing two re-renders (React may batch, but not always across async boundaries).
64
130
 
65
- ## Remaining Recommendations
131
+ After: `lastUpdate` is a `useRef` (no re-render). Added shallow comparison to skip `setPeers()` when peers haven't actually changed.
132
+
133
+ Also: `getPeer()` now uses a `useMemo` Map for O(1) lookup instead of O(n) `Array.find()`.
134
+
135
+ ### useMessages — Reduced Array Copies
136
+
137
+ Before: 3 array copies per incoming message when over `maxMessages` (`[msg, ...prev]` + `slice(maxMessages)` + `slice(0, maxMessages)`).
138
+
139
+ After: 1 array copy + in-place truncation via `updated.length = maxMessages`.
140
+
141
+ ---
142
+
143
+ ## Storage Optimization
144
+
145
+ ### MessageStore Payload Encoding
146
+
147
+ Before: `Array.from(Uint8Array)` converted each byte to a boxed JS Number — **8x memory bloat** (1 byte → 8 bytes for Number object + array overhead).
148
+
149
+ After: Base64 encoding. A 10KB payload uses ~13.3KB as base64 string (1.33x) instead of ~80KB as Number array (8x).
150
+
151
+ ---
152
+
153
+ ## Compression Optimization
154
+
155
+ ### LZ4 Hash Table Reuse
156
+
157
+ Before: `new Int32Array(4096)` (16KB allocation) on every `_lz4Compress()` call.
158
+
159
+ After: Pre-allocated in constructor, reused with `.fill(-1)` reset per call.
160
+
161
+ ---
162
+
163
+ ## WiFi Direct Base64 Fix
164
+
165
+ ### O(n^2) → O(n) String Building
166
+
167
+ Before: Character-by-character `binary += String.fromCharCode(bytes[i])` — O(n^2) due to string immutability.
168
+
169
+ After: Chunk-based `String.fromCharCode.apply(null, bytes.subarray(i, i+8192))` with final `join()` — O(n).
170
+
171
+ For a 1MB file transfer, this eliminates ~1 million intermediate string allocations.
172
+
173
+ ---
174
+
175
+ ## Historical Changes (v2.0.0)
176
+
177
+ ### Crypto Module Removed
178
+ The pure JavaScript cryptographic implementations (`src/crypto/`) were removed in v2.0.0. Pure JS BigInt field arithmetic for X25519 was orders of magnitude slower than native:
179
+ - ~100ms+ per key exchange (vs ~1ms with native)
180
+ - Battery drain from CPU-intensive crypto
181
+ - UI thread blocking on Hermes/JSC
66
182
 
67
- 1. **Add `tweetnacl` as peer dependency** for consumers who need encryption
68
- 2. **Consider TypeScript migration** — current JS codebase with JSDoc is good but TS would catch more errors
69
- 3. **Add integration tests with real BLE** — current tests use MockTransport; consider Detox/Appium for device testing
70
- 4. **Publish to npm** with proper semver (this is a breaking change → v2.0.0)
183
+ Replaced by the pluggable provider system: `tweetnacl`, `react-native-quick-crypto`, or `expo-crypto`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ble-mesh",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "React Native Bluetooth Low Energy (BLE) mesh networking library with end-to-end encryption, offline messaging, peer-to-peer communication, and Noise Protocol security for iOS and Android",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -23,6 +23,11 @@ const { ValidationError, MeshError } = require('./errors');
23
23
  const { FileManager } = require('./service/file');
24
24
  const { SERVICE_STATE, EVENTS } = require('./constants');
25
25
 
26
+ /** @private Cached TextEncoder singleton – avoids per-call allocation */
27
+ const _encoder = new TextEncoder();
28
+ /** @private Cached TextDecoder singleton – avoids per-call allocation */
29
+ const _decoder = new TextDecoder();
30
+
26
31
  /**
27
32
  * Default MeshNetwork configuration
28
33
  * @constant {Object}
@@ -571,13 +576,14 @@ class MeshNetwork extends EventEmitter {
571
576
 
572
577
  // Send offer (JSON is OK for metadata, but use binary type marker)
573
578
  const offerJson = JSON.stringify(transfer.offer);
574
- const offerBytes = new TextEncoder().encode(offerJson);
579
+ const offerBytes = _encoder.encode(offerJson);
575
580
  const offerPayload = new Uint8Array(1 + offerBytes.length);
576
581
  offerPayload[0] = 0x01; // Binary type marker for OFFER
577
582
  offerPayload.set(offerBytes, 1);
578
583
  await this._service._sendRaw(peerId, offerPayload);
579
584
 
580
585
  // Send chunks sequentially using binary protocol
586
+ const transferIdBytes = _encoder.encode(transfer.id);
581
587
  for (let i = 0; i < transfer.chunks.length; i++) {
582
588
  // Check if still running (handles app backgrounding)
583
589
  if (this._state !== 'running') {
@@ -588,7 +594,6 @@ class MeshNetwork extends EventEmitter {
588
594
  const chunk = transfer.chunks[i];
589
595
 
590
596
  // Binary format: [type(1)] [transferIdLen(1)] [transferId(N)] [index(2)] [data(M)]
591
- const transferIdBytes = new TextEncoder().encode(transfer.id);
592
597
  const header = new Uint8Array(1 + 1 + transferIdBytes.length + 2);
593
598
  let offset = 0;
594
599
  header[offset++] = 0x02; // Binary type marker for CHUNK
@@ -605,13 +610,16 @@ class MeshNetwork extends EventEmitter {
605
610
 
606
611
  // Add per-chunk timeout (10 seconds)
607
612
  const sendPromise = this._service._sendRaw(peerId, payload);
608
- const timeoutPromise = new Promise((_, reject) =>
609
- setTimeout(() => reject(new Error('Chunk send timeout')), 10000)
610
- );
613
+ let timeoutId;
614
+ const timeoutPromise = new Promise((_, reject) => {
615
+ timeoutId = setTimeout(() => reject(new Error('Chunk send timeout')), 10000);
616
+ });
611
617
 
612
618
  try {
613
619
  await Promise.race([sendPromise, timeoutPromise]);
620
+ clearTimeout(timeoutId);
614
621
  } catch (err) {
622
+ clearTimeout(timeoutId);
615
623
  this._fileManager.cancelTransfer(transfer.id);
616
624
  this.emit('fileTransferFailed', {
617
625
  id: transfer.id, name: fileInfo.name, error: err.message
@@ -818,7 +826,7 @@ class MeshNetwork extends EventEmitter {
818
826
  const sendFn = async (payload) => {
819
827
  // Re-encrypt and send via the proper encrypted channel
820
828
  try {
821
- const text = new TextDecoder().decode(payload);
829
+ const text = _decoder.decode(payload);
822
830
  await this._service.sendPrivateMessage(peerId, text);
823
831
  } catch (e) {
824
832
  // Fallback to raw send if encryption fails
@@ -892,7 +900,7 @@ class MeshNetwork extends EventEmitter {
892
900
  * @private
893
901
  */
894
902
  _encodeMessage(text) {
895
- return new TextEncoder().encode(text);
903
+ return _encoder.encode(text);
896
904
  }
897
905
 
898
906
  /**
@@ -936,7 +944,7 @@ class MeshNetwork extends EventEmitter {
936
944
  }
937
945
 
938
946
  // Check size limit (UTF-8 encoded)
939
- const byteLength = new TextEncoder().encode(text).length;
947
+ const byteLength = _encoder.encode(text).length;
940
948
  if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
941
949
  throw ValidationError.outOfRange('text', byteLength, {
942
950
  min: 1,
@@ -14,25 +14,36 @@ const QuickCryptoProvider = require('./providers/QuickCryptoProvider');
14
14
  const ExpoCryptoProvider = require('./providers/ExpoCryptoProvider');
15
15
  const TweetNaClProvider = require('./providers/TweetNaClProvider');
16
16
 
17
+ /** @type {import('./CryptoProvider')|null} Cached singleton provider */
18
+ let _cachedProvider = null;
19
+
17
20
  /**
18
21
  * Detects and returns the best available crypto provider.
22
+ * The result is cached as a singleton for subsequent calls.
19
23
  * @returns {import('./CryptoProvider')} Best available provider
20
24
  * @throws {Error} If no crypto provider is available
21
25
  */
22
26
  function detectProvider() {
27
+ if (_cachedProvider) {
28
+ return _cachedProvider;
29
+ }
30
+
23
31
  // 1. Native speed (react-native-quick-crypto)
24
32
  if (QuickCryptoProvider.isAvailable()) {
25
- return new QuickCryptoProvider();
33
+ _cachedProvider = new QuickCryptoProvider();
34
+ return _cachedProvider;
26
35
  }
27
36
 
28
37
  // 2. Expo (expo-crypto + tweetnacl)
29
38
  if (ExpoCryptoProvider.isAvailable()) {
30
- return new ExpoCryptoProvider();
39
+ _cachedProvider = new ExpoCryptoProvider();
40
+ return _cachedProvider;
31
41
  }
32
42
 
33
43
  // 3. Universal (tweetnacl)
34
44
  if (TweetNaClProvider.isAvailable()) {
35
- return new TweetNaClProvider();
45
+ _cachedProvider = new TweetNaClProvider();
46
+ return _cachedProvider;
36
47
  }
37
48
 
38
49
  throw new Error(
@@ -98,14 +98,15 @@ class ExpoCryptoProvider extends CryptoProvider {
98
98
  hash(data) {
99
99
  // expo-crypto's digestStringAsync is async — for sync compat, use tweetnacl
100
100
  const nacl = this._getNacl();
101
- return nacl.hash(data).slice(0, 32);
101
+ return nacl.hash(data).subarray(0, 32);
102
102
  }
103
103
 
104
104
  /** @inheritdoc */
105
105
  randomBytes(length) {
106
106
  const expoCrypto = this._getExpoCrypto();
107
107
  if (expoCrypto.getRandomBytes) {
108
- return new Uint8Array(expoCrypto.getRandomBytes(length));
108
+ const bytes = expoCrypto.getRandomBytes(length);
109
+ return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
109
110
  }
110
111
  // Fallback to tweetnacl
111
112
  const nacl = this._getNacl();
@@ -10,6 +10,11 @@
10
10
 
11
11
  const CryptoProvider = require('../CryptoProvider');
12
12
 
13
+ /** DER header for PKCS8 private key wrapping (X25519) */
14
+ const PKCS8_HEADER = Buffer.from('302e020100300506032b656e04220420', 'hex');
15
+ /** DER header for SPKI public key wrapping (X25519) */
16
+ const SPKI_HEADER = Buffer.from('302a300506032b656e032100', 'hex');
17
+
13
18
  /**
14
19
  * Crypto provider using react-native-quick-crypto.
15
20
  * Provides native-speed crypto on React Native (JSI binding).
@@ -21,6 +26,7 @@ class QuickCryptoProvider extends CryptoProvider {
21
26
  constructor(options = {}) {
22
27
  super();
23
28
  this._crypto = options.crypto || null;
29
+ this._nacl = null;
24
30
  }
25
31
 
26
32
  get name() {
@@ -55,7 +61,7 @@ class QuickCryptoProvider extends CryptoProvider {
55
61
  const crypto = this._getCrypto();
56
62
  const privKey = crypto.createPrivateKey({
57
63
  key: Buffer.concat([
58
- Buffer.from('302e020100300506032b656e04220420', 'hex'),
64
+ PKCS8_HEADER,
59
65
  Buffer.from(secretKey)
60
66
  ]),
61
67
  format: 'der',
@@ -63,7 +69,7 @@ class QuickCryptoProvider extends CryptoProvider {
63
69
  });
64
70
  const pubKey = crypto.createPublicKey({
65
71
  key: Buffer.concat([
66
- Buffer.from('302a300506032b656e032100', 'hex'),
72
+ SPKI_HEADER,
67
73
  Buffer.from(publicKey)
68
74
  ]),
69
75
  format: 'der',
@@ -73,11 +79,29 @@ class QuickCryptoProvider extends CryptoProvider {
73
79
  return new Uint8Array(shared);
74
80
  }
75
81
 
82
+ /**
83
+ * Lazily loads tweetnacl (cached)
84
+ * @returns {Object} nacl module
85
+ * @private
86
+ */
87
+ _getNacl() {
88
+ if (!this._nacl) {
89
+ try {
90
+ this._nacl = require('tweetnacl');
91
+ } catch (e) {
92
+ throw new Error(
93
+ 'tweetnacl is required for QuickCryptoProvider encrypt/decrypt/hash. Install: npm install tweetnacl'
94
+ );
95
+ }
96
+ }
97
+ return this._nacl;
98
+ }
99
+
76
100
  /** @inheritdoc */
77
101
  encrypt(key, nonce, plaintext, _ad) {
78
102
  // Use tweetnacl for encryption to ensure cross-provider compatibility
79
103
  // QuickCrypto's advantage is in fast native key generation (X25519), not AEAD
80
- const nacl = require('tweetnacl');
104
+ const nacl = this._getNacl();
81
105
 
82
106
  // Ensure 24-byte nonce for XSalsa20-Poly1305
83
107
  let fullNonce = nonce;
@@ -91,7 +115,7 @@ class QuickCryptoProvider extends CryptoProvider {
91
115
 
92
116
  /** @inheritdoc */
93
117
  decrypt(key, nonce, ciphertext, _ad) {
94
- const nacl = require('tweetnacl');
118
+ const nacl = this._getNacl();
95
119
 
96
120
  // Ensure 24-byte nonce for XSalsa20-Poly1305
97
121
  let fullNonce = nonce;
@@ -110,9 +134,9 @@ class QuickCryptoProvider extends CryptoProvider {
110
134
  /** @inheritdoc */
111
135
  hash(data) {
112
136
  // Use SHA-512 truncated to 32 bytes for cross-provider compatibility
113
- const nacl = require('tweetnacl');
137
+ const nacl = this._getNacl();
114
138
  const full = nacl.hash(data); // SHA-512
115
- return full.slice(0, 32);
139
+ return full.subarray(0, 32);
116
140
  }
117
141
 
118
142
  /** @inheritdoc */
@@ -98,7 +98,7 @@ class TweetNaClProvider extends CryptoProvider {
98
98
  const nacl = this._getNacl();
99
99
  // tweetnacl provides SHA-512; we return first 32 bytes for SHA-256 compatibility
100
100
  const full = nacl.hash(data);
101
- return full.slice(0, 32);
101
+ return full.subarray(0, 32);
102
102
  }
103
103
 
104
104
  /** @inheritdoc */
@@ -45,6 +45,8 @@ class AppStateManager {
45
45
  this._initialized = false;
46
46
  /** @private */
47
47
  this._AppState = null;
48
+ /** @private */
49
+ this._boundHandleStateChange = this._handleStateChange.bind(this);
48
50
  }
49
51
 
50
52
  /**
@@ -64,9 +66,15 @@ class AppStateManager {
64
66
  return false;
65
67
  }
66
68
 
69
+ // Remove old subscription before creating new one
70
+ if (this._subscription) {
71
+ this._subscription.remove();
72
+ this._subscription = null;
73
+ }
74
+
67
75
  this._subscription = this._AppState.addEventListener(
68
76
  'change',
69
- this._handleStateChange.bind(this)
77
+ this._boundHandleStateChange
70
78
  );
71
79
 
72
80
  this._initialized = true;
@@ -47,6 +47,8 @@ function useMesh(config = {}) {
47
47
  // Create mesh instance ref (persists across renders)
48
48
  const meshRef = useRef(null);
49
49
  const mountedRef = useRef(true);
50
+ const stateHandlerRef = useRef(null);
51
+ const errorHandlerRef = useRef(null);
50
52
 
51
53
  // State
52
54
  const [state, setState] = useState('uninitialized');
@@ -71,18 +73,29 @@ function useMesh(config = {}) {
71
73
 
72
74
  const mesh = getMesh();
73
75
 
76
+ // Remove old listeners if they exist (prevents accumulation on re-init)
77
+ if (stateHandlerRef.current) {
78
+ mesh.off('state-changed', stateHandlerRef.current);
79
+ }
80
+ if (errorHandlerRef.current) {
81
+ mesh.off('error', errorHandlerRef.current);
82
+ }
83
+
74
84
  // Setup state change listener
75
- mesh.on('state-changed', ({ newState }) => {
85
+ stateHandlerRef.current = ({ newState }) => {
76
86
  if (mountedRef.current) {
77
87
  setState(newState);
78
88
  }
79
- });
89
+ };
80
90
 
81
- mesh.on('error', (err) => {
91
+ errorHandlerRef.current = (err) => {
82
92
  if (mountedRef.current) {
83
93
  setError(err);
84
94
  }
85
- });
95
+ };
96
+
97
+ mesh.on('state-changed', stateHandlerRef.current);
98
+ mesh.on('error', errorHandlerRef.current);
86
99
 
87
100
  // Initialize with storage
88
101
  await mesh.initialize({
@@ -58,11 +58,11 @@ function useMessages(mesh, options = {}) {
58
58
 
59
59
  setMessages(prev => {
60
60
  const updated = [msg, ...prev];
61
- // Trim to max messages
62
61
  if (updated.length > maxMessages) {
63
- const removed = updated.slice(maxMessages);
64
- removed.forEach(m => messageIdRef.current.delete(m.id));
65
- return updated.slice(0, maxMessages);
62
+ for (let i = maxMessages; i < updated.length; i++) {
63
+ messageIdRef.current.delete(updated[i].id);
64
+ }
65
+ updated.length = maxMessages;
66
66
  }
67
67
  return updated;
68
68
  });
@@ -35,18 +35,23 @@ function usePeers(mesh) {
35
35
  throw new Error('usePeers requires React. Install react as a dependency.');
36
36
  }
37
37
 
38
- const { useState, useEffect, useCallback, useMemo } = React;
38
+ const { useState, useEffect, useCallback, useMemo, useRef } = React;
39
39
 
40
40
  const [peers, setPeers] = useState([]);
41
- const [lastUpdate, setLastUpdate] = useState(Date.now());
41
+ const lastUpdateRef = useRef(Date.now());
42
42
 
43
43
  // Update peers from mesh
44
44
  const refreshPeers = useCallback(() => {
45
45
  if (mesh) {
46
46
  try {
47
47
  const allPeers = mesh.getPeers();
48
- setPeers(allPeers);
49
- setLastUpdate(Date.now());
48
+ setPeers(prev => {
49
+ if (prev.length === allPeers.length && prev.every((p, i) => p.id === allPeers[i]?.id && p.connectionState === allPeers[i]?.connectionState)) {
50
+ return prev;
51
+ }
52
+ return allPeers;
53
+ });
54
+ lastUpdateRef.current = Date.now();
50
55
  } catch (e) {
51
56
  // Mesh might not be ready
52
57
  }
@@ -84,10 +89,9 @@ function usePeers(mesh) {
84
89
  return peers.filter(p => p.connectionState === 'secured');
85
90
  }, [peers]);
86
91
 
87
- // Get single peer by ID
88
- const getPeer = useCallback((peerId) => {
89
- return peers.find(p => p.id === peerId);
90
- }, [peers]);
92
+ // Get single peer by ID (O(1) lookup via peerMap)
93
+ const peerMap = useMemo(() => new Map(peers.map(p => [p.id, p])), [peers]);
94
+ const getPeer = useCallback((peerId) => peerMap.get(peerId), [peerMap]);
91
95
 
92
96
  // Check if peer is connected
93
97
  const isConnected = useCallback((peerId) => {
@@ -104,7 +108,7 @@ function usePeers(mesh) {
104
108
  getPeer,
105
109
  isConnected,
106
110
  refresh: refreshPeers,
107
- lastUpdate
111
+ lastUpdate: lastUpdateRef.current
108
112
  };
109
113
  }
110
114