react-native-ble-mesh 2.0.0 → 2.1.2
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 +63 -53
- package/src/constants/audio.js +4 -4
- package/src/constants/ble.js +1 -1
- package/src/constants/crypto.js +1 -1
- package/src/constants/errors.js +2 -2
- package/src/constants/events.js +1 -1
- package/src/constants/protocol.js +2 -2
- package/src/crypto/AutoCrypto.js +16 -3
- package/src/crypto/CryptoProvider.js +17 -17
- package/src/crypto/providers/ExpoCryptoProvider.js +15 -9
- package/src/crypto/providers/QuickCryptoProvider.js +41 -12
- package/src/crypto/providers/TweetNaClProvider.js +10 -8
- package/src/errors/AudioError.js +2 -1
- package/src/errors/ConnectionError.js +2 -2
- package/src/errors/CryptoError.js +1 -1
- package/src/errors/HandshakeError.js +2 -2
- package/src/errors/MeshError.js +4 -4
- package/src/errors/MessageError.js +2 -2
- package/src/errors/ValidationError.js +3 -3
- package/src/expo/withBLEMesh.js +10 -10
- package/src/hooks/AppStateManager.js +11 -2
- package/src/hooks/useMesh.js +23 -10
- package/src/hooks/useMessages.js +17 -16
- package/src/hooks/usePeers.js +19 -14
- package/src/index.js +2 -2
- package/src/mesh/dedup/BloomFilter.js +45 -57
- package/src/mesh/dedup/DedupManager.js +36 -8
- package/src/mesh/dedup/MessageCache.js +3 -0
- package/src/mesh/fragment/Assembler.js +5 -4
- package/src/mesh/fragment/Fragmenter.js +3 -3
- package/src/mesh/monitor/ConnectionQuality.js +59 -25
- package/src/mesh/monitor/NetworkMonitor.js +80 -28
- package/src/mesh/peer/Peer.js +9 -11
- package/src/mesh/peer/PeerDiscovery.js +18 -19
- package/src/mesh/peer/PeerManager.js +29 -17
- package/src/mesh/router/MessageRouter.js +28 -20
- package/src/mesh/router/PathFinder.js +10 -13
- package/src/mesh/router/RouteTable.js +25 -14
- package/src/mesh/store/StoreAndForwardManager.js +32 -24
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +18 -14
- package/src/protocol/serializer.js +9 -12
- package/src/protocol/validator.js +29 -10
- package/src/service/BatteryOptimizer.js +22 -18
- package/src/service/EmergencyManager.js +18 -25
- package/src/service/HandshakeManager.js +112 -18
- package/src/service/MeshService.js +106 -22
- package/src/service/SessionManager.js +50 -13
- package/src/service/audio/AudioManager.js +80 -38
- package/src/service/audio/buffer/FrameBuffer.js +7 -8
- package/src/service/audio/buffer/JitterBuffer.js +1 -1
- package/src/service/audio/codec/LC3Codec.js +18 -19
- package/src/service/audio/codec/LC3Decoder.js +10 -10
- package/src/service/audio/codec/LC3Encoder.js +11 -9
- package/src/service/audio/session/AudioSession.js +14 -17
- package/src/service/audio/session/VoiceMessage.js +15 -22
- package/src/service/audio/transport/AudioFragmenter.js +17 -9
- package/src/service/audio/transport/AudioFramer.js +8 -12
- package/src/service/file/FileAssembler.js +4 -2
- package/src/service/file/FileChunker.js +1 -1
- package/src/service/file/FileManager.js +26 -20
- package/src/service/file/FileMessage.js +7 -12
- package/src/service/text/TextManager.js +75 -42
- package/src/service/text/broadcast/BroadcastManager.js +14 -17
- package/src/service/text/channel/Channel.js +10 -14
- package/src/service/text/channel/ChannelManager.js +10 -10
- package/src/service/text/message/TextMessage.js +12 -19
- package/src/service/text/message/TextSerializer.js +2 -2
- package/src/storage/AsyncStorageAdapter.js +17 -14
- package/src/storage/MemoryStorage.js +11 -8
- package/src/storage/MessageStore.js +77 -32
- package/src/storage/Storage.js +9 -9
- package/src/transport/BLETransport.js +27 -16
- package/src/transport/MockTransport.js +7 -2
- package/src/transport/MultiTransport.js +43 -11
- package/src/transport/Transport.js +9 -9
- package/src/transport/WiFiDirectTransport.js +26 -20
- package/src/transport/adapters/BLEAdapter.js +19 -19
- package/src/transport/adapters/NodeBLEAdapter.js +24 -23
- package/src/transport/adapters/RNBLEAdapter.js +14 -11
- package/src/utils/EventEmitter.js +15 -16
- package/src/utils/LRUCache.js +10 -4
- package/src/utils/RateLimiter.js +1 -1
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +10 -8
- package/src/utils/encoding.js +39 -8
- package/src/utils/retry.js +11 -13
- package/src/utils/time.js +9 -4
- package/src/utils/validation.js +1 -1
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__/ #
|
|
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
|
|
582
|
+
npm test # Run all 433 tests
|
|
583
583
|
npm run test:coverage # With coverage report
|
|
584
584
|
```
|
|
585
585
|
|
package/docs/OPTIMIZATION.md
CHANGED
|
@@ -1,70 +1,183 @@
|
|
|
1
|
-
# Optimization
|
|
1
|
+
# Performance Optimization Guide
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v2.1.0 Performance Overhaul
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
### Pre-Computed Sets (O(n) → O(1))
|
|
25
96
|
|
|
26
|
-
|
|
97
|
+
`Object.values(ENUM).includes(value)` creates an array and does linear scan on every call.
|
|
27
98
|
|
|
28
|
-
|
|
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
|
-
|
|
103
|
+
### Direct Map Iteration
|
|
31
104
|
|
|
32
|
-
|
|
105
|
+
`Array.from(map.values()).filter(...)` creates two arrays. Direct iteration creates one:
|
|
33
106
|
|
|
34
|
-
|
|
107
|
+
```js
|
|
108
|
+
// Before (2 arrays)
|
|
109
|
+
getConnectedPeers() { return this.getAllPeers().filter(p => p.isConnected()); }
|
|
35
110
|
|
|
36
|
-
|
|
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
|
-
|
|
121
|
+
Applied to: `PeerManager.getConnectedPeers()`, `getSecuredPeers()`, `getDirectPeers()`
|
|
39
122
|
|
|
40
|
-
|
|
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
|
-
|
|
125
|
+
## React Native Hook Optimizations
|
|
51
126
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.1.2",
|
|
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",
|
package/src/MeshNetwork.js
CHANGED
|
@@ -23,9 +23,14 @@ 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
|
-
* @constant {
|
|
33
|
+
* @constant {any}
|
|
29
34
|
*/
|
|
30
35
|
const DEFAULT_CONFIG = Object.freeze({
|
|
31
36
|
/** Display name for this node */
|
|
@@ -83,21 +88,21 @@ class MeshNetwork extends EventEmitter {
|
|
|
83
88
|
* @param {Object} [config.encryption] - Encryption settings
|
|
84
89
|
* @param {Object} [config.routing] - Routing settings
|
|
85
90
|
* @param {Object} [config.compression] - Compression settings
|
|
86
|
-
* @param {
|
|
91
|
+
* @param {any} [config.storeAndForward] - Store and forward settings
|
|
87
92
|
*/
|
|
88
93
|
constructor(config = {}) {
|
|
89
94
|
super();
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
97
|
* Configuration
|
|
93
|
-
* @type {
|
|
98
|
+
* @type {any}
|
|
94
99
|
* @private
|
|
95
100
|
*/
|
|
96
101
|
this._config = this._mergeConfig(DEFAULT_CONFIG, config);
|
|
97
102
|
|
|
98
103
|
/**
|
|
99
104
|
* Underlying MeshService
|
|
100
|
-
* @type {MeshService}
|
|
105
|
+
* @type {InstanceType<typeof MeshService>}
|
|
101
106
|
* @private
|
|
102
107
|
*/
|
|
103
108
|
this._service = new MeshService({
|
|
@@ -106,7 +111,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
106
111
|
|
|
107
112
|
/**
|
|
108
113
|
* Transport layer
|
|
109
|
-
* @type {
|
|
114
|
+
* @type {any}
|
|
110
115
|
* @private
|
|
111
116
|
*/
|
|
112
117
|
this._transport = null;
|
|
@@ -175,7 +180,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
175
180
|
|
|
176
181
|
/**
|
|
177
182
|
* Channel manager reference
|
|
178
|
-
* @type {
|
|
183
|
+
* @type {any}
|
|
179
184
|
* @private
|
|
180
185
|
*/
|
|
181
186
|
this._channels = null;
|
|
@@ -197,7 +202,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
197
202
|
|
|
198
203
|
/**
|
|
199
204
|
* Starts the mesh network.
|
|
200
|
-
* @param {
|
|
205
|
+
* @param {any} [transport] - Optional custom transport
|
|
201
206
|
* @returns {Promise<void>}
|
|
202
207
|
*/
|
|
203
208
|
async start(transport) {
|
|
@@ -211,7 +216,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
211
216
|
const { RNBLEAdapter } = require('./transport/adapters');
|
|
212
217
|
const adapter = new RNBLEAdapter();
|
|
213
218
|
this._transport = new BLETransport(adapter);
|
|
214
|
-
} catch (e) {
|
|
219
|
+
} catch (/** @type {any} */ e) {
|
|
215
220
|
throw new MeshError(
|
|
216
221
|
'No transport provided and BLE adapter not available. ' +
|
|
217
222
|
'Either pass a transport to start(), install react-native-ble-plx for BLE, ' +
|
|
@@ -247,15 +252,15 @@ class MeshNetwork extends EventEmitter {
|
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
// Setup file transfer events
|
|
250
|
-
this._fileManager.on('sendProgress', (info) => this.emit('fileSendProgress', info));
|
|
251
|
-
this._fileManager.on('receiveProgress', (info) => this.emit('fileReceiveProgress', info));
|
|
252
|
-
this._fileManager.on('fileReceived', (info) => this.emit('fileReceived', info));
|
|
253
|
-
this._fileManager.on('transferFailed', (info) => this.emit('fileTransferFailed', info));
|
|
254
|
-
this._fileManager.on('transferCancelled', (info) => this.emit('fileTransferCancelled', info));
|
|
255
|
+
this._fileManager.on('sendProgress', (/** @type {any} */ info) => this.emit('fileSendProgress', info));
|
|
256
|
+
this._fileManager.on('receiveProgress', (/** @type {any} */ info) => this.emit('fileReceiveProgress', info));
|
|
257
|
+
this._fileManager.on('fileReceived', (/** @type {any} */ info) => this.emit('fileReceived', info));
|
|
258
|
+
this._fileManager.on('transferFailed', (/** @type {any} */ info) => this.emit('fileTransferFailed', info));
|
|
259
|
+
this._fileManager.on('transferCancelled', (/** @type {any} */ info) => this.emit('fileTransferCancelled', info));
|
|
255
260
|
|
|
256
261
|
// Start connection quality monitoring
|
|
257
262
|
this._connectionQuality.start();
|
|
258
|
-
this._connectionQuality.on('qualityChanged', (quality) => {
|
|
263
|
+
this._connectionQuality.on('qualityChanged', (/** @type {any} */ quality) => {
|
|
259
264
|
this.emit('connectionQualityChanged', quality);
|
|
260
265
|
});
|
|
261
266
|
|
|
@@ -347,9 +352,9 @@ class MeshNetwork extends EventEmitter {
|
|
|
347
352
|
// If peer is offline and store-forward is enabled, cache the message
|
|
348
353
|
if (this._storeForward && this._isPeerOffline(peerId)) {
|
|
349
354
|
const payload = this._encodeMessage(text);
|
|
350
|
-
await this._storeForward.cacheForOfflinePeer(peerId, payload, {
|
|
355
|
+
await this._storeForward.cacheForOfflinePeer(peerId, payload, /** @type {any} */ ({
|
|
351
356
|
needsEncryption: true
|
|
352
|
-
});
|
|
357
|
+
}));
|
|
353
358
|
this.emit('messageCached', { peerId, text });
|
|
354
359
|
return 'cached';
|
|
355
360
|
}
|
|
@@ -405,7 +410,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
405
410
|
|
|
406
411
|
/**
|
|
407
412
|
* Gets list of joined channels.
|
|
408
|
-
* @returns {
|
|
413
|
+
* @returns {any[]} Channels
|
|
409
414
|
*/
|
|
410
415
|
getChannels() {
|
|
411
416
|
return this._service.getChannels();
|
|
@@ -417,7 +422,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
417
422
|
|
|
418
423
|
/**
|
|
419
424
|
* Gets all known peers.
|
|
420
|
-
* @returns {
|
|
425
|
+
* @returns {any[]} Array of peers
|
|
421
426
|
*/
|
|
422
427
|
getPeers() {
|
|
423
428
|
return this._service.getPeers();
|
|
@@ -425,7 +430,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
425
430
|
|
|
426
431
|
/**
|
|
427
432
|
* Gets connected peers.
|
|
428
|
-
* @returns {
|
|
433
|
+
* @returns {any[]} Connected peers
|
|
429
434
|
*/
|
|
430
435
|
getConnectedPeers() {
|
|
431
436
|
return this._service.getConnectedPeers();
|
|
@@ -433,7 +438,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
433
438
|
|
|
434
439
|
/**
|
|
435
440
|
* Gets peers with secure sessions.
|
|
436
|
-
* @returns {
|
|
441
|
+
* @returns {any[]} Secured peers
|
|
437
442
|
*/
|
|
438
443
|
getSecuredPeers() {
|
|
439
444
|
return this._service.getSecuredPeers();
|
|
@@ -463,7 +468,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
463
468
|
|
|
464
469
|
/**
|
|
465
470
|
* Gets network health metrics.
|
|
466
|
-
* @returns {
|
|
471
|
+
* @returns {any} Health report
|
|
467
472
|
*/
|
|
468
473
|
getNetworkHealth() {
|
|
469
474
|
return this._monitor.generateHealthReport();
|
|
@@ -472,7 +477,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
472
477
|
/**
|
|
473
478
|
* Gets detailed health for a specific peer.
|
|
474
479
|
* @param {string} peerId - Peer ID
|
|
475
|
-
* @returns {
|
|
480
|
+
* @returns {any} Node health
|
|
476
481
|
*/
|
|
477
482
|
getPeerHealth(peerId) {
|
|
478
483
|
return this._monitor.getNodeHealth(peerId);
|
|
@@ -557,7 +562,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
557
562
|
* @param {Uint8Array} fileInfo.data - File data
|
|
558
563
|
* @param {string} fileInfo.name - File name
|
|
559
564
|
* @param {string} [fileInfo.mimeType] - MIME type
|
|
560
|
-
* @returns {
|
|
565
|
+
* @returns {Promise<any>} Transfer info with id and event emitter
|
|
561
566
|
*/
|
|
562
567
|
async sendFile(peerId, fileInfo) {
|
|
563
568
|
this._validateRunning();
|
|
@@ -567,17 +572,19 @@ class MeshNetwork extends EventEmitter {
|
|
|
567
572
|
throw new ValidationError('File must have data and name', 'E800');
|
|
568
573
|
}
|
|
569
574
|
|
|
575
|
+
/** @type {any} */
|
|
570
576
|
const transfer = this._fileManager.prepareSend(peerId, fileInfo);
|
|
571
577
|
|
|
572
578
|
// Send offer (JSON is OK for metadata, but use binary type marker)
|
|
573
579
|
const offerJson = JSON.stringify(transfer.offer);
|
|
574
|
-
const offerBytes =
|
|
580
|
+
const offerBytes = _encoder.encode(offerJson);
|
|
575
581
|
const offerPayload = new Uint8Array(1 + offerBytes.length);
|
|
576
582
|
offerPayload[0] = 0x01; // Binary type marker for OFFER
|
|
577
583
|
offerPayload.set(offerBytes, 1);
|
|
578
584
|
await this._service._sendRaw(peerId, offerPayload);
|
|
579
585
|
|
|
580
586
|
// Send chunks sequentially using binary protocol
|
|
587
|
+
const transferIdBytes = _encoder.encode(transfer.id);
|
|
581
588
|
for (let i = 0; i < transfer.chunks.length; i++) {
|
|
582
589
|
// Check if still running (handles app backgrounding)
|
|
583
590
|
if (this._state !== 'running') {
|
|
@@ -588,7 +595,6 @@ class MeshNetwork extends EventEmitter {
|
|
|
588
595
|
const chunk = transfer.chunks[i];
|
|
589
596
|
|
|
590
597
|
// Binary format: [type(1)] [transferIdLen(1)] [transferId(N)] [index(2)] [data(M)]
|
|
591
|
-
const transferIdBytes = new TextEncoder().encode(transfer.id);
|
|
592
598
|
const header = new Uint8Array(1 + 1 + transferIdBytes.length + 2);
|
|
593
599
|
let offset = 0;
|
|
594
600
|
header[offset++] = 0x02; // Binary type marker for CHUNK
|
|
@@ -605,13 +611,16 @@ class MeshNetwork extends EventEmitter {
|
|
|
605
611
|
|
|
606
612
|
// Add per-chunk timeout (10 seconds)
|
|
607
613
|
const sendPromise = this._service._sendRaw(peerId, payload);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
614
|
+
let timeoutId;
|
|
615
|
+
const timeoutPromise = new Promise((/** @type {any} */ _, /** @type {any} */ reject) => {
|
|
616
|
+
timeoutId = setTimeout(() => reject(new Error('Chunk send timeout')), 10000);
|
|
617
|
+
});
|
|
611
618
|
|
|
612
619
|
try {
|
|
613
620
|
await Promise.race([sendPromise, timeoutPromise]);
|
|
614
|
-
|
|
621
|
+
clearTimeout(timeoutId);
|
|
622
|
+
} catch (/** @type {any} */ err) {
|
|
623
|
+
clearTimeout(timeoutId);
|
|
615
624
|
this._fileManager.cancelTransfer(transfer.id);
|
|
616
625
|
this.emit('fileTransferFailed', {
|
|
617
626
|
id: transfer.id, name: fileInfo.name, error: err.message
|
|
@@ -627,7 +636,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
627
636
|
|
|
628
637
|
/**
|
|
629
638
|
* Gets active file transfers
|
|
630
|
-
* @returns {
|
|
639
|
+
* @returns {any} { outgoing: [], incoming: [] }
|
|
631
640
|
*/
|
|
632
641
|
getActiveTransfers() {
|
|
633
642
|
return this._fileManager.getActiveTransfers();
|
|
@@ -648,7 +657,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
648
657
|
/**
|
|
649
658
|
* Gets connection quality for a specific peer.
|
|
650
659
|
* @param {string} peerId - Peer ID
|
|
651
|
-
* @returns {
|
|
660
|
+
* @returns {any} Quality report
|
|
652
661
|
*/
|
|
653
662
|
getConnectionQuality(peerId) {
|
|
654
663
|
return this._connectionQuality.getQuality(peerId);
|
|
@@ -656,7 +665,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
656
665
|
|
|
657
666
|
/**
|
|
658
667
|
* Gets connection quality for all peers.
|
|
659
|
-
* @returns {
|
|
668
|
+
* @returns {any[]} Array of quality reports
|
|
660
669
|
*/
|
|
661
670
|
getAllConnectionQuality() {
|
|
662
671
|
return this._connectionQuality.getAllQuality();
|
|
@@ -668,7 +677,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
668
677
|
|
|
669
678
|
/**
|
|
670
679
|
* Gets the network status.
|
|
671
|
-
* @returns {
|
|
680
|
+
* @returns {any} Status
|
|
672
681
|
*/
|
|
673
682
|
getStatus() {
|
|
674
683
|
const identity = this._state === 'running' ? this._service.getIdentity() : null;
|
|
@@ -685,7 +694,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
685
694
|
|
|
686
695
|
/**
|
|
687
696
|
* Gets the identity information.
|
|
688
|
-
* @returns {
|
|
697
|
+
* @returns {any} Identity
|
|
689
698
|
*/
|
|
690
699
|
getIdentity() {
|
|
691
700
|
return this._service.getIdentity();
|
|
@@ -706,9 +715,9 @@ class MeshNetwork extends EventEmitter {
|
|
|
706
715
|
|
|
707
716
|
/**
|
|
708
717
|
* Merges configuration with defaults.
|
|
709
|
-
* @param {
|
|
710
|
-
* @param {
|
|
711
|
-
* @returns {
|
|
718
|
+
* @param {any} defaults - Default config
|
|
719
|
+
* @param {any} custom - Custom config
|
|
720
|
+
* @returns {any} Merged config
|
|
712
721
|
* @private
|
|
713
722
|
*/
|
|
714
723
|
_mergeConfig(defaults, custom) {
|
|
@@ -730,12 +739,12 @@ class MeshNetwork extends EventEmitter {
|
|
|
730
739
|
*/
|
|
731
740
|
_setupEventForwarding() {
|
|
732
741
|
// Forward service events with PRD-style naming
|
|
733
|
-
this._service.on(EVENTS.PEER_DISCOVERED, (peer) => {
|
|
742
|
+
this._service.on(EVENTS.PEER_DISCOVERED, (/** @type {any} */ peer) => {
|
|
734
743
|
this._monitor.trackPeerDiscovered(peer.id);
|
|
735
744
|
this.emit('peerDiscovered', peer);
|
|
736
745
|
});
|
|
737
746
|
|
|
738
|
-
this._service.on(EVENTS.PEER_CONNECTED, (peer) => {
|
|
747
|
+
this._service.on(EVENTS.PEER_CONNECTED, (/** @type {any} */ peer) => {
|
|
739
748
|
if (peer.rssi) {
|
|
740
749
|
this._connectionQuality.recordRssi(peer.id, peer.rssi);
|
|
741
750
|
}
|
|
@@ -746,13 +755,13 @@ class MeshNetwork extends EventEmitter {
|
|
|
746
755
|
}
|
|
747
756
|
});
|
|
748
757
|
|
|
749
|
-
this._service.on(EVENTS.PEER_DISCONNECTED, (peer) => {
|
|
758
|
+
this._service.on(EVENTS.PEER_DISCONNECTED, (/** @type {any} */ peer) => {
|
|
750
759
|
this._monitor.trackPeerDisconnected(peer.id);
|
|
751
760
|
this._connectionQuality.removePeer(peer.id);
|
|
752
761
|
this.emit('peerDisconnected', peer);
|
|
753
762
|
});
|
|
754
763
|
|
|
755
|
-
this._service.on('message', (message) => {
|
|
764
|
+
this._service.on('message', (/** @type {any} */ message) => {
|
|
756
765
|
this._monitor.trackMessageReceived(message.senderId);
|
|
757
766
|
this.emit('messageReceived', {
|
|
758
767
|
from: message.senderId,
|
|
@@ -762,7 +771,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
762
771
|
});
|
|
763
772
|
});
|
|
764
773
|
|
|
765
|
-
this._service.on('private-message', (message) => {
|
|
774
|
+
this._service.on('private-message', (/** @type {any} */ message) => {
|
|
766
775
|
this.emit('directMessage', {
|
|
767
776
|
from: message.senderId,
|
|
768
777
|
text: message.content,
|
|
@@ -770,7 +779,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
770
779
|
});
|
|
771
780
|
});
|
|
772
781
|
|
|
773
|
-
this._service.on('channel-message', (message) => {
|
|
782
|
+
this._service.on('channel-message', (/** @type {any} */ message) => {
|
|
774
783
|
this.emit('channelMessage', {
|
|
775
784
|
channel: message.channelId,
|
|
776
785
|
from: message.senderId,
|
|
@@ -779,22 +788,22 @@ class MeshNetwork extends EventEmitter {
|
|
|
779
788
|
});
|
|
780
789
|
});
|
|
781
790
|
|
|
782
|
-
this._service.on('message-delivered', (info) => {
|
|
791
|
+
this._service.on('message-delivered', (/** @type {any} */ info) => {
|
|
783
792
|
this._monitor.trackMessageDelivered(info.messageId);
|
|
784
793
|
this.emit('messageDelivered', info);
|
|
785
794
|
});
|
|
786
795
|
|
|
787
|
-
this._service.on('error', (error) => {
|
|
796
|
+
this._service.on('error', (/** @type {any} */ error) => {
|
|
788
797
|
this.emit('error', error);
|
|
789
798
|
});
|
|
790
799
|
|
|
791
800
|
// Forward monitor events
|
|
792
|
-
this._monitor.on('health-changed', (info) => {
|
|
801
|
+
this._monitor.on('health-changed', (/** @type {any} */ info) => {
|
|
793
802
|
this.emit('networkHealthChanged', info);
|
|
794
803
|
});
|
|
795
804
|
|
|
796
805
|
// Forward emergency events
|
|
797
|
-
this._emergencyManager.on('panic-wipe-completed', (result) => {
|
|
806
|
+
this._emergencyManager.on('panic-wipe-completed', (/** @type {any} */ result) => {
|
|
798
807
|
this.emit('dataWiped', result);
|
|
799
808
|
});
|
|
800
809
|
}
|
|
@@ -815,17 +824,18 @@ class MeshNetwork extends EventEmitter {
|
|
|
815
824
|
async _deliverCachedMessages(peerId) {
|
|
816
825
|
if (!this._storeForward) { return; }
|
|
817
826
|
|
|
818
|
-
const sendFn = async (payload) => {
|
|
827
|
+
const sendFn = async (/** @type {any} */ payload) => {
|
|
819
828
|
// Re-encrypt and send via the proper encrypted channel
|
|
820
829
|
try {
|
|
821
|
-
const text =
|
|
830
|
+
const text = _decoder.decode(payload);
|
|
822
831
|
await this._service.sendPrivateMessage(peerId, text);
|
|
823
|
-
} catch (e) {
|
|
832
|
+
} catch (/** @type {any} */ e) {
|
|
824
833
|
// Fallback to raw send if encryption fails
|
|
825
834
|
await this._service._sendRaw(peerId, payload);
|
|
826
835
|
}
|
|
827
836
|
};
|
|
828
837
|
|
|
838
|
+
/** @type {any} */
|
|
829
839
|
const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
|
|
830
840
|
|
|
831
841
|
if (result.delivered > 0) {
|
|
@@ -849,7 +859,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
849
859
|
// Clear store and forward cache
|
|
850
860
|
if (this._storeForward) {
|
|
851
861
|
this._emergencyManager.registerClearer(async () => {
|
|
852
|
-
this._storeForward
|
|
862
|
+
this._storeForward?.clear();
|
|
853
863
|
});
|
|
854
864
|
}
|
|
855
865
|
|
|
@@ -892,7 +902,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
892
902
|
* @private
|
|
893
903
|
*/
|
|
894
904
|
_encodeMessage(text) {
|
|
895
|
-
return
|
|
905
|
+
return _encoder.encode(text);
|
|
896
906
|
}
|
|
897
907
|
|
|
898
908
|
/**
|
|
@@ -936,7 +946,7 @@ class MeshNetwork extends EventEmitter {
|
|
|
936
946
|
}
|
|
937
947
|
|
|
938
948
|
// Check size limit (UTF-8 encoded)
|
|
939
|
-
const byteLength =
|
|
949
|
+
const byteLength = _encoder.encode(text).length;
|
|
940
950
|
if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
|
|
941
951
|
throw ValidationError.outOfRange('text', byteLength, {
|
|
942
952
|
min: 1,
|