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.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +183 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +667 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +90 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
- package/src/crypto/providers/QuickCryptoProvider.js +158 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/AppStateManager.js +9 -1
- package/src/hooks/useMesh.js +47 -13
- package/src/hooks/useMessages.js +6 -4
- package/src/hooks/usePeers.js +13 -9
- package/src/index.js +23 -8
- package/src/mesh/dedup/BloomFilter.js +44 -57
- package/src/mesh/dedup/DedupManager.js +67 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/fragment/Fragmenter.js +1 -1
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +433 -0
- package/src/mesh/monitor/NetworkMonitor.js +376 -320
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/Peer.js +5 -2
- package/src/mesh/peer/PeerManager.js +21 -4
- package/src/mesh/router/MessageRouter.js +38 -19
- package/src/mesh/router/RouteTable.js +24 -8
- package/src/mesh/store/StoreAndForwardManager.js +305 -296
- package/src/mesh/store/index.js +1 -1
- 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 +285 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +163 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +79 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/TextManager.js +21 -15
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/storage/MessageStore.js +55 -2
- package/src/transport/BLETransport.js +141 -10
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +330 -0
- package/src/transport/WiFiDirectTransport.js +296 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/EventEmitter.js +6 -9
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +293 -291
- package/src/utils/encoding.js +33 -8
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
package/src/MeshNetwork.js
CHANGED
|
@@ -3,60 +3,66 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview MeshNetwork - Simplified High-Level API
|
|
5
5
|
* @module MeshNetwork
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Primary entry point for react-native-ble-mesh as specified in the PRD.
|
|
8
8
|
* Provides a developer-friendly API for BitChat-compatible mesh networking.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* Target: Setup-to-first-message in <15 minutes, <10 lines for basic messaging.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const EventEmitter = require('./utils/EventEmitter');
|
|
14
14
|
const { MeshService } = require('./service');
|
|
15
|
-
const { BLETransport
|
|
15
|
+
const { BLETransport } = require('./transport');
|
|
16
16
|
const { MemoryStorage } = require('./storage');
|
|
17
17
|
const { StoreAndForwardManager } = require('./mesh/store');
|
|
18
|
-
const { NetworkMonitor, HEALTH_STATUS } = require('./mesh/monitor');
|
|
18
|
+
const { NetworkMonitor, HEALTH_STATUS, ConnectionQuality } = require('./mesh/monitor');
|
|
19
19
|
const { BatteryOptimizer, BATTERY_MODE } = require('./service/BatteryOptimizer');
|
|
20
20
|
const { EmergencyManager, PANIC_TRIGGER } = require('./service/EmergencyManager');
|
|
21
21
|
const { MessageCompressor } = require('./utils/compression');
|
|
22
|
-
const { EVENTS } = require('./constants');
|
|
23
22
|
const { ValidationError, MeshError } = require('./errors');
|
|
23
|
+
const { FileManager } = require('./service/file');
|
|
24
|
+
const { SERVICE_STATE, EVENTS } = require('./constants');
|
|
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();
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* Default MeshNetwork configuration
|
|
27
33
|
* @constant {Object}
|
|
28
34
|
*/
|
|
29
35
|
const DEFAULT_CONFIG = Object.freeze({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
/** Display name for this node */
|
|
37
|
+
nickname: 'Anonymous',
|
|
38
|
+
/** Battery mode: 'high' | 'balanced' | 'low' | 'auto' */
|
|
39
|
+
batteryMode: BATTERY_MODE.BALANCED,
|
|
40
|
+
/** Encryption settings */
|
|
41
|
+
encryption: {
|
|
42
|
+
level: 'standard',
|
|
43
|
+
rotateKeysAfter: 1000
|
|
44
|
+
},
|
|
45
|
+
/** Routing configuration */
|
|
46
|
+
routing: {
|
|
47
|
+
maxHops: 7,
|
|
48
|
+
bloomFilterSize: 10000
|
|
49
|
+
},
|
|
50
|
+
/** Compression settings */
|
|
51
|
+
compression: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
threshold: 100
|
|
54
|
+
},
|
|
55
|
+
/** Store and forward settings */
|
|
56
|
+
storeAndForward: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
retentionHours: 24,
|
|
59
|
+
maxCachedMessages: 1000
|
|
60
|
+
}
|
|
55
61
|
});
|
|
56
62
|
|
|
57
63
|
/**
|
|
58
64
|
* MeshNetwork - High-level API for BitChat-compatible mesh networking.
|
|
59
|
-
*
|
|
65
|
+
*
|
|
60
66
|
* @class MeshNetwork
|
|
61
67
|
* @extends EventEmitter
|
|
62
68
|
* @example
|
|
@@ -65,16 +71,16 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
65
71
|
* nickname: 'Alice',
|
|
66
72
|
* batteryMode: 'balanced',
|
|
67
73
|
* });
|
|
68
|
-
*
|
|
74
|
+
*
|
|
69
75
|
* await mesh.start();
|
|
70
76
|
* await mesh.broadcast('Hello mesh!');
|
|
71
|
-
*
|
|
77
|
+
*
|
|
72
78
|
* mesh.on('messageReceived', (message) => {
|
|
73
79
|
* console.log(`${message.from}: ${message.text}`);
|
|
74
80
|
* });
|
|
75
81
|
*/
|
|
76
82
|
class MeshNetwork extends EventEmitter {
|
|
77
|
-
|
|
83
|
+
/**
|
|
78
84
|
* Creates a new MeshNetwork instance.
|
|
79
85
|
* @param {Object} [config={}] - Network configuration
|
|
80
86
|
* @param {string} [config.nickname='Anonymous'] - Display name
|
|
@@ -84,694 +90,890 @@ class MeshNetwork extends EventEmitter {
|
|
|
84
90
|
* @param {Object} [config.compression] - Compression settings
|
|
85
91
|
* @param {Object} [config.storeAndForward] - Store and forward settings
|
|
86
92
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
constructor(config = {}) {
|
|
94
|
+
super();
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
/**
|
|
91
97
|
* Configuration
|
|
92
98
|
* @type {Object}
|
|
93
99
|
* @private
|
|
94
100
|
*/
|
|
95
|
-
|
|
101
|
+
this._config = this._mergeConfig(DEFAULT_CONFIG, config);
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
/**
|
|
98
104
|
* Underlying MeshService
|
|
99
105
|
* @type {MeshService}
|
|
100
106
|
* @private
|
|
101
107
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
this._service = new MeshService({
|
|
109
|
+
displayName: this._config.nickname
|
|
110
|
+
});
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
/**
|
|
107
113
|
* Transport layer
|
|
108
114
|
* @type {Transport|null}
|
|
109
115
|
* @private
|
|
110
116
|
*/
|
|
111
|
-
|
|
117
|
+
this._transport = null;
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
/**
|
|
114
120
|
* Store and forward manager
|
|
115
121
|
* @type {StoreAndForwardManager|null}
|
|
116
122
|
* @private
|
|
117
123
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
this._storeForward = this._config.storeAndForward.enabled
|
|
125
|
+
? new StoreAndForwardManager({
|
|
126
|
+
retentionMs: this._config.storeAndForward.retentionHours * 60 * 60 * 1000,
|
|
127
|
+
maxTotalMessages: this._config.storeAndForward.maxCachedMessages
|
|
128
|
+
})
|
|
129
|
+
: null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
126
132
|
* Network monitor
|
|
127
133
|
* @type {NetworkMonitor}
|
|
128
134
|
* @private
|
|
129
135
|
*/
|
|
130
|
-
|
|
136
|
+
this._monitor = new NetworkMonitor();
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
/**
|
|
133
139
|
* Battery optimizer
|
|
134
140
|
* @type {BatteryOptimizer}
|
|
135
141
|
* @private
|
|
136
142
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
this._batteryOptimizer = new BatteryOptimizer({
|
|
144
|
+
initialMode: this._config.batteryMode
|
|
145
|
+
});
|
|
140
146
|
|
|
141
|
-
|
|
147
|
+
/**
|
|
142
148
|
* Emergency manager
|
|
143
149
|
* @type {EmergencyManager}
|
|
144
150
|
* @private
|
|
145
151
|
*/
|
|
146
|
-
|
|
152
|
+
this._emergencyManager = new EmergencyManager();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* File transfer manager
|
|
156
|
+
* @type {FileManager}
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
this._fileManager = new FileManager({
|
|
160
|
+
chunkSize: this._config.fileTransfer?.chunkSize,
|
|
161
|
+
maxFileSize: this._config.fileTransfer?.maxFileSize,
|
|
162
|
+
transferTimeoutMs: this._config.fileTransfer?.timeoutMs
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Connection quality tracker
|
|
167
|
+
* @type {ConnectionQuality}
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
this._connectionQuality = new ConnectionQuality(this._config.qualityConfig || {});
|
|
147
171
|
|
|
148
|
-
|
|
172
|
+
/**
|
|
149
173
|
* Message compressor
|
|
150
174
|
* @type {MessageCompressor}
|
|
151
175
|
* @private
|
|
152
176
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
this._compressor = new MessageCompressor({
|
|
178
|
+
threshold: this._config.compression.threshold
|
|
179
|
+
});
|
|
156
180
|
|
|
157
|
-
|
|
181
|
+
/**
|
|
158
182
|
* Channel manager reference
|
|
159
183
|
* @type {Object|null}
|
|
160
184
|
* @private
|
|
161
185
|
*/
|
|
162
|
-
|
|
186
|
+
this._channels = null;
|
|
163
187
|
|
|
164
|
-
|
|
188
|
+
/**
|
|
165
189
|
* Network state
|
|
166
190
|
* @type {string}
|
|
167
191
|
* @private
|
|
168
192
|
*/
|
|
169
|
-
|
|
193
|
+
this._state = 'stopped';
|
|
170
194
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
// Setup event forwarding
|
|
196
|
+
this._setupEventForwarding();
|
|
197
|
+
}
|
|
174
198
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Lifecycle Methods
|
|
201
|
+
// ============================================================================
|
|
178
202
|
|
|
179
|
-
|
|
203
|
+
/**
|
|
180
204
|
* Starts the mesh network.
|
|
181
205
|
* @param {Object} [transport] - Optional custom transport
|
|
182
206
|
* @returns {Promise<void>}
|
|
183
207
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
async start(transport) {
|
|
209
|
+
if (this._state === 'running') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Create transport if not provided
|
|
214
|
+
if (!transport) {
|
|
215
|
+
try {
|
|
216
|
+
const { RNBLEAdapter } = require('./transport/adapters');
|
|
217
|
+
const adapter = new RNBLEAdapter();
|
|
218
|
+
this._transport = new BLETransport(adapter);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
throw new MeshError(
|
|
221
|
+
'No transport provided and BLE adapter not available. ' +
|
|
222
|
+
'Either pass a transport to start(), install react-native-ble-plx for BLE, ' +
|
|
223
|
+
'or use MockTransport for testing.',
|
|
224
|
+
'E900',
|
|
225
|
+
{ cause: e.message }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
this._transport = transport;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Initialize the service (only on first start)
|
|
233
|
+
if (this._service._state === SERVICE_STATE.UNINITIALIZED) {
|
|
234
|
+
await this._service.initialize({
|
|
235
|
+
storage: new MemoryStorage()
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Connect battery optimizer to transport
|
|
240
|
+
this._batteryOptimizer.setTransport(this._transport);
|
|
241
|
+
await this._batteryOptimizer.setMode(this._config.batteryMode);
|
|
242
|
+
|
|
243
|
+
// Register data clearers for panic mode
|
|
244
|
+
this._registerPanicClearers();
|
|
245
|
+
|
|
246
|
+
// Start the service
|
|
247
|
+
await this._service.start(this._transport);
|
|
248
|
+
|
|
249
|
+
// Setup store and forward
|
|
250
|
+
if (this._storeForward) {
|
|
251
|
+
this._setupStoreAndForward();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Setup file transfer events
|
|
255
|
+
this._fileManager.on('sendProgress', (info) => this.emit('fileSendProgress', info));
|
|
256
|
+
this._fileManager.on('receiveProgress', (info) => this.emit('fileReceiveProgress', info));
|
|
257
|
+
this._fileManager.on('fileReceived', (info) => this.emit('fileReceived', info));
|
|
258
|
+
this._fileManager.on('transferFailed', (info) => this.emit('fileTransferFailed', info));
|
|
259
|
+
this._fileManager.on('transferCancelled', (info) => this.emit('fileTransferCancelled', info));
|
|
260
|
+
|
|
261
|
+
// Start connection quality monitoring
|
|
262
|
+
this._connectionQuality.start();
|
|
263
|
+
this._connectionQuality.on('qualityChanged', (quality) => {
|
|
264
|
+
this.emit('connectionQualityChanged', quality);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
this._state = 'running';
|
|
268
|
+
this.emit('started');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
217
272
|
* Stops the mesh network.
|
|
218
273
|
* @returns {Promise<void>}
|
|
219
274
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
await this._service.stop();
|
|
226
|
-
this._state = 'stopped';
|
|
227
|
-
this.emit('stopped');
|
|
275
|
+
async stop() {
|
|
276
|
+
if (this._state !== 'running') {
|
|
277
|
+
return;
|
|
228
278
|
}
|
|
229
279
|
|
|
230
|
-
|
|
280
|
+
// Clean up listeners added in start()
|
|
281
|
+
this._fileManager.removeAllListeners('sendProgress');
|
|
282
|
+
this._fileManager.removeAllListeners('receiveProgress');
|
|
283
|
+
this._fileManager.removeAllListeners('fileReceived');
|
|
284
|
+
this._fileManager.removeAllListeners('transferFailed');
|
|
285
|
+
this._fileManager.removeAllListeners('transferCancelled');
|
|
286
|
+
this._connectionQuality.removeAllListeners('qualityChanged');
|
|
287
|
+
this._connectionQuality.stop();
|
|
288
|
+
|
|
289
|
+
await this._service.stop();
|
|
290
|
+
this._state = 'stopped';
|
|
291
|
+
this.emit('stopped');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
231
295
|
* Destroys the mesh network and cleans up resources.
|
|
232
296
|
* @returns {Promise<void>}
|
|
233
297
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
298
|
+
async destroy() {
|
|
299
|
+
await this.stop();
|
|
300
|
+
await this._service.destroy();
|
|
237
301
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
this._monitor.destroy();
|
|
242
|
-
this._batteryOptimizer.destroy();
|
|
243
|
-
this._emergencyManager.destroy();
|
|
302
|
+
this._connectionQuality.destroy();
|
|
303
|
+
this._fileManager.destroy();
|
|
244
304
|
|
|
245
|
-
|
|
305
|
+
if (this._storeForward) {
|
|
306
|
+
this._storeForward.destroy();
|
|
246
307
|
}
|
|
308
|
+
this._monitor.destroy();
|
|
309
|
+
this._batteryOptimizer.destroy();
|
|
310
|
+
this._emergencyManager.destroy();
|
|
247
311
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// ============================================================================
|
|
312
|
+
this.removeAllListeners();
|
|
313
|
+
}
|
|
251
314
|
|
|
252
|
-
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Messaging Methods
|
|
317
|
+
// ============================================================================
|
|
318
|
+
|
|
319
|
+
/**
|
|
253
320
|
* Broadcasts a message to all peers.
|
|
254
321
|
* @param {string} text - Message text
|
|
255
322
|
* @returns {Promise<string>} Message ID
|
|
256
323
|
* @throws {Error} If text is invalid
|
|
257
324
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
325
|
+
async broadcast(text) {
|
|
326
|
+
this._validateRunning();
|
|
327
|
+
this._validateMessageText(text);
|
|
261
328
|
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
const messageId = await this._service.sendBroadcast(text);
|
|
330
|
+
this._monitor.trackMessageSent('broadcast', messageId);
|
|
264
331
|
|
|
265
|
-
|
|
266
|
-
|
|
332
|
+
return messageId;
|
|
333
|
+
}
|
|
267
334
|
|
|
268
|
-
|
|
335
|
+
/**
|
|
269
336
|
* Sends a direct encrypted message to a specific peer.
|
|
270
337
|
* @param {string} peerId - Target peer ID or nickname
|
|
271
338
|
* @param {string} text - Message text
|
|
272
339
|
* @returns {Promise<string>} Message ID
|
|
273
340
|
* @throws {Error} If peerId or text is invalid
|
|
274
341
|
*/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
342
|
+
async sendDirect(peerId, text) {
|
|
343
|
+
this._validateRunning();
|
|
344
|
+
this._validatePeerId(peerId);
|
|
345
|
+
this._validateMessageText(text);
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const messageId = await this._service.sendPrivateMessage(peerId, text);
|
|
349
|
+
this._monitor.trackMessageSent(peerId, messageId);
|
|
350
|
+
return messageId;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// If peer is offline and store-forward is enabled, cache the message
|
|
353
|
+
if (this._storeForward && this._isPeerOffline(peerId)) {
|
|
354
|
+
const payload = this._encodeMessage(text);
|
|
355
|
+
await this._storeForward.cacheForOfflinePeer(peerId, payload, {
|
|
356
|
+
needsEncryption: true
|
|
357
|
+
});
|
|
358
|
+
this.emit('messageCached', { peerId, text });
|
|
359
|
+
return 'cached';
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
294
362
|
}
|
|
363
|
+
}
|
|
295
364
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// Channel Methods
|
|
367
|
+
// ============================================================================
|
|
299
368
|
|
|
300
|
-
|
|
369
|
+
/**
|
|
301
370
|
* Joins a channel (IRC-style topic-based group chat).
|
|
302
371
|
* @param {string} channelName - Channel name (e.g., '#general')
|
|
303
372
|
* @param {string} [password] - Optional password
|
|
304
373
|
* @returns {Promise<void>}
|
|
305
374
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
375
|
+
async joinChannel(channelName, password) {
|
|
376
|
+
this._validateRunning();
|
|
308
377
|
|
|
309
|
-
|
|
310
|
-
|
|
378
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
379
|
+
await this._service.joinChannel(normalized, password);
|
|
311
380
|
|
|
312
|
-
|
|
313
|
-
|
|
381
|
+
this.emit('channelJoined', { channel: normalized });
|
|
382
|
+
}
|
|
314
383
|
|
|
315
|
-
|
|
384
|
+
/**
|
|
316
385
|
* Leaves a channel.
|
|
317
386
|
* @param {string} channelName - Channel name
|
|
318
387
|
* @returns {Promise<void>}
|
|
319
388
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
389
|
+
async leaveChannel(channelName) {
|
|
390
|
+
this._validateRunning();
|
|
322
391
|
|
|
323
|
-
|
|
324
|
-
|
|
392
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
393
|
+
await this._service.leaveChannel(normalized);
|
|
325
394
|
|
|
326
|
-
|
|
327
|
-
|
|
395
|
+
this.emit('channelLeft', { channel: normalized });
|
|
396
|
+
}
|
|
328
397
|
|
|
329
|
-
|
|
398
|
+
/**
|
|
330
399
|
* Sends a message to a channel.
|
|
331
400
|
* @param {string} channelName - Channel name
|
|
332
401
|
* @param {string} text - Message text
|
|
333
402
|
* @returns {Promise<string>} Message ID
|
|
334
403
|
*/
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
async sendToChannel(channelName, text) {
|
|
405
|
+
this._validateRunning();
|
|
337
406
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
408
|
+
return await this._service.sendChannelMessage(normalized, text);
|
|
409
|
+
}
|
|
341
410
|
|
|
342
|
-
|
|
411
|
+
/**
|
|
343
412
|
* Gets list of joined channels.
|
|
344
413
|
* @returns {Object[]} Channels
|
|
345
414
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
415
|
+
getChannels() {
|
|
416
|
+
return this._service.getChannels();
|
|
417
|
+
}
|
|
349
418
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// Peer Methods
|
|
421
|
+
// ============================================================================
|
|
353
422
|
|
|
354
|
-
|
|
423
|
+
/**
|
|
355
424
|
* Gets all known peers.
|
|
356
425
|
* @returns {Object[]} Array of peers
|
|
357
426
|
*/
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
427
|
+
getPeers() {
|
|
428
|
+
return this._service.getPeers();
|
|
429
|
+
}
|
|
361
430
|
|
|
362
|
-
|
|
431
|
+
/**
|
|
363
432
|
* Gets connected peers.
|
|
364
433
|
* @returns {Object[]} Connected peers
|
|
365
434
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
435
|
+
getConnectedPeers() {
|
|
436
|
+
return this._service.getConnectedPeers();
|
|
437
|
+
}
|
|
369
438
|
|
|
370
|
-
|
|
439
|
+
/**
|
|
371
440
|
* Gets peers with secure sessions.
|
|
372
441
|
* @returns {Object[]} Secured peers
|
|
373
442
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
443
|
+
getSecuredPeers() {
|
|
444
|
+
return this._service.getSecuredPeers();
|
|
445
|
+
}
|
|
377
446
|
|
|
378
|
-
|
|
447
|
+
/**
|
|
379
448
|
* Blocks a peer.
|
|
380
449
|
* @param {string} peerId - Peer ID
|
|
381
450
|
*/
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
451
|
+
blockPeer(peerId) {
|
|
452
|
+
this._service.blockPeer(peerId);
|
|
453
|
+
this.emit('peerBlocked', { peerId });
|
|
454
|
+
}
|
|
386
455
|
|
|
387
|
-
|
|
456
|
+
/**
|
|
388
457
|
* Unblocks a peer.
|
|
389
458
|
* @param {string} peerId - Peer ID
|
|
390
459
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
460
|
+
unblockPeer(peerId) {
|
|
461
|
+
this._service.unblockPeer(peerId);
|
|
462
|
+
this.emit('peerUnblocked', { peerId });
|
|
463
|
+
}
|
|
395
464
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
465
|
+
// ============================================================================
|
|
466
|
+
// Network Health Methods
|
|
467
|
+
// ============================================================================
|
|
399
468
|
|
|
400
|
-
|
|
469
|
+
/**
|
|
401
470
|
* Gets network health metrics.
|
|
402
471
|
* @returns {Object} Health report
|
|
403
472
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
473
|
+
getNetworkHealth() {
|
|
474
|
+
return this._monitor.generateHealthReport();
|
|
475
|
+
}
|
|
407
476
|
|
|
408
|
-
|
|
477
|
+
/**
|
|
409
478
|
* Gets detailed health for a specific peer.
|
|
410
479
|
* @param {string} peerId - Peer ID
|
|
411
480
|
* @returns {Object|null} Node health
|
|
412
481
|
*/
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
482
|
+
getPeerHealth(peerId) {
|
|
483
|
+
return this._monitor.getNodeHealth(peerId);
|
|
484
|
+
}
|
|
416
485
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// Battery Management
|
|
488
|
+
// ============================================================================
|
|
420
489
|
|
|
421
|
-
|
|
490
|
+
/**
|
|
422
491
|
* Sets the battery mode.
|
|
423
492
|
* @param {string} mode - 'high' | 'balanced' | 'low' | 'auto'
|
|
424
493
|
* @returns {Promise<void>}
|
|
425
494
|
*/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
495
|
+
async setBatteryMode(mode) {
|
|
496
|
+
await this._batteryOptimizer.setMode(mode);
|
|
497
|
+
}
|
|
429
498
|
|
|
430
|
-
|
|
499
|
+
/**
|
|
431
500
|
* Gets the current battery mode.
|
|
432
501
|
* @returns {string} Current mode
|
|
433
502
|
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
503
|
+
getBatteryMode() {
|
|
504
|
+
return this._batteryOptimizer.getMode();
|
|
505
|
+
}
|
|
437
506
|
|
|
438
|
-
|
|
507
|
+
/**
|
|
439
508
|
* Updates battery level (for auto mode).
|
|
440
509
|
* @param {number} level - Battery level (0-100)
|
|
441
510
|
* @param {boolean} [charging=false] - Whether charging
|
|
442
511
|
*/
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
512
|
+
updateBatteryLevel(level, charging = false) {
|
|
513
|
+
this._batteryOptimizer.updateBatteryLevel(level, charging);
|
|
514
|
+
}
|
|
446
515
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
516
|
+
// ============================================================================
|
|
517
|
+
// Security Methods
|
|
518
|
+
// ============================================================================
|
|
450
519
|
|
|
451
|
-
|
|
520
|
+
/**
|
|
452
521
|
* Enables panic mode for emergency data wipe.
|
|
453
522
|
* @param {Object} [options={}] - Panic mode options
|
|
454
523
|
* @param {string} [options.trigger='triple_tap'] - Trigger type
|
|
455
524
|
* @param {Function} [options.onWipe] - Callback after wipe
|
|
456
525
|
*/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
526
|
+
enablePanicMode(options = {}) {
|
|
527
|
+
this._emergencyManager.enablePanicMode(options);
|
|
528
|
+
this.emit('panicModeEnabled');
|
|
529
|
+
}
|
|
461
530
|
|
|
462
|
-
|
|
531
|
+
/**
|
|
463
532
|
* Disables panic mode.
|
|
464
533
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
534
|
+
disablePanicMode() {
|
|
535
|
+
this._emergencyManager.disablePanicMode();
|
|
536
|
+
this.emit('panicModeDisabled');
|
|
537
|
+
}
|
|
469
538
|
|
|
470
|
-
|
|
539
|
+
/**
|
|
471
540
|
* Registers a tap for panic mode detection.
|
|
472
541
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
542
|
+
registerPanicTap() {
|
|
543
|
+
this._emergencyManager.registerTap();
|
|
544
|
+
}
|
|
476
545
|
|
|
477
|
-
|
|
546
|
+
/**
|
|
478
547
|
* Manually triggers data wipe.
|
|
479
548
|
* @returns {Promise<Object>} Wipe result
|
|
480
549
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
550
|
+
async wipeAllData() {
|
|
551
|
+
return await this._emergencyManager.wipeAllData();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// File Sharing Methods
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Sends a file to a specific peer.
|
|
560
|
+
* @param {string} peerId - Target peer ID
|
|
561
|
+
* @param {Object} fileInfo - File information
|
|
562
|
+
* @param {Uint8Array} fileInfo.data - File data
|
|
563
|
+
* @param {string} fileInfo.name - File name
|
|
564
|
+
* @param {string} [fileInfo.mimeType] - MIME type
|
|
565
|
+
* @returns {Object} Transfer info with id and event emitter
|
|
566
|
+
*/
|
|
567
|
+
async sendFile(peerId, fileInfo) {
|
|
568
|
+
this._validateRunning();
|
|
569
|
+
this._validatePeerId(peerId);
|
|
570
|
+
|
|
571
|
+
if (!fileInfo || !fileInfo.data || !fileInfo.name) {
|
|
572
|
+
throw new ValidationError('File must have data and name', 'E800');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const transfer = this._fileManager.prepareSend(peerId, fileInfo);
|
|
576
|
+
|
|
577
|
+
// Send offer (JSON is OK for metadata, but use binary type marker)
|
|
578
|
+
const offerJson = JSON.stringify(transfer.offer);
|
|
579
|
+
const offerBytes = _encoder.encode(offerJson);
|
|
580
|
+
const offerPayload = new Uint8Array(1 + offerBytes.length);
|
|
581
|
+
offerPayload[0] = 0x01; // Binary type marker for OFFER
|
|
582
|
+
offerPayload.set(offerBytes, 1);
|
|
583
|
+
await this._service._sendRaw(peerId, offerPayload);
|
|
584
|
+
|
|
585
|
+
// Send chunks sequentially using binary protocol
|
|
586
|
+
const transferIdBytes = _encoder.encode(transfer.id);
|
|
587
|
+
for (let i = 0; i < transfer.chunks.length; i++) {
|
|
588
|
+
// Check if still running (handles app backgrounding)
|
|
589
|
+
if (this._state !== 'running') {
|
|
590
|
+
this._fileManager.cancelTransfer(transfer.id);
|
|
591
|
+
throw new MeshError('File transfer cancelled: network stopped', 'E900');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const chunk = transfer.chunks[i];
|
|
595
|
+
|
|
596
|
+
// Binary format: [type(1)] [transferIdLen(1)] [transferId(N)] [index(2)] [data(M)]
|
|
597
|
+
const header = new Uint8Array(1 + 1 + transferIdBytes.length + 2);
|
|
598
|
+
let offset = 0;
|
|
599
|
+
header[offset++] = 0x02; // Binary type marker for CHUNK
|
|
600
|
+
header[offset++] = transferIdBytes.length;
|
|
601
|
+
header.set(transferIdBytes, offset);
|
|
602
|
+
offset += transferIdBytes.length;
|
|
603
|
+
header[offset++] = (chunk.index >> 8) & 0xFF;
|
|
604
|
+
header[offset++] = chunk.index & 0xFF;
|
|
605
|
+
|
|
606
|
+
// Combine header + chunk data
|
|
607
|
+
const payload = new Uint8Array(header.length + chunk.data.length);
|
|
608
|
+
payload.set(header, 0);
|
|
609
|
+
payload.set(chunk.data, header.length);
|
|
610
|
+
|
|
611
|
+
// Add per-chunk timeout (10 seconds)
|
|
612
|
+
const sendPromise = this._service._sendRaw(peerId, payload);
|
|
613
|
+
let timeoutId;
|
|
614
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
615
|
+
timeoutId = setTimeout(() => reject(new Error('Chunk send timeout')), 10000);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
await Promise.race([sendPromise, timeoutPromise]);
|
|
620
|
+
clearTimeout(timeoutId);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
clearTimeout(timeoutId);
|
|
623
|
+
this._fileManager.cancelTransfer(transfer.id);
|
|
624
|
+
this.emit('fileTransferFailed', {
|
|
625
|
+
id: transfer.id, name: fileInfo.name, error: err.message
|
|
626
|
+
});
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this._fileManager.markChunkSent(transfer.id, i);
|
|
483
631
|
}
|
|
484
632
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// ============================================================================
|
|
633
|
+
return { id: transfer.id, name: fileInfo.name };
|
|
634
|
+
}
|
|
488
635
|
|
|
489
|
-
|
|
636
|
+
/**
|
|
637
|
+
* Gets active file transfers
|
|
638
|
+
* @returns {Object} { outgoing: [], incoming: [] }
|
|
639
|
+
*/
|
|
640
|
+
getActiveTransfers() {
|
|
641
|
+
return this._fileManager.getActiveTransfers();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Cancels a file transfer
|
|
646
|
+
* @param {string} transferId - Transfer ID
|
|
647
|
+
*/
|
|
648
|
+
cancelTransfer(transferId) {
|
|
649
|
+
this._fileManager.cancelTransfer(transferId);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// Connection Quality Methods
|
|
654
|
+
// ============================================================================
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Gets connection quality for a specific peer.
|
|
658
|
+
* @param {string} peerId - Peer ID
|
|
659
|
+
* @returns {Object|null} Quality report
|
|
660
|
+
*/
|
|
661
|
+
getConnectionQuality(peerId) {
|
|
662
|
+
return this._connectionQuality.getQuality(peerId);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Gets connection quality for all peers.
|
|
667
|
+
* @returns {Object[]} Array of quality reports
|
|
668
|
+
*/
|
|
669
|
+
getAllConnectionQuality() {
|
|
670
|
+
return this._connectionQuality.getAllQuality();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Status Methods
|
|
675
|
+
// ============================================================================
|
|
676
|
+
|
|
677
|
+
/**
|
|
490
678
|
* Gets the network status.
|
|
491
679
|
* @returns {Object} Status
|
|
492
680
|
*/
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
681
|
+
getStatus() {
|
|
682
|
+
const identity = this._state === 'running' ? this._service.getIdentity() : null;
|
|
683
|
+
return {
|
|
684
|
+
state: this._state,
|
|
685
|
+
identity,
|
|
686
|
+
peers: this._state === 'running' ? this.getPeers().length : 0,
|
|
687
|
+
connectedPeers: this._state === 'running' ? this.getConnectedPeers().length : 0,
|
|
688
|
+
channels: this._state === 'running' ? this.getChannels().length : 0,
|
|
689
|
+
health: this._state === 'running' ? this.getNetworkHealth() : null,
|
|
690
|
+
batteryMode: this.getBatteryMode()
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
506
695
|
* Gets the identity information.
|
|
507
696
|
* @returns {Object} Identity
|
|
508
697
|
*/
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
698
|
+
getIdentity() {
|
|
699
|
+
return this._service.getIdentity();
|
|
700
|
+
}
|
|
512
701
|
|
|
513
|
-
|
|
702
|
+
/**
|
|
514
703
|
* Sets the display name/nickname.
|
|
515
704
|
* @param {string} nickname - New nickname
|
|
516
705
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
706
|
+
setNickname(nickname) {
|
|
707
|
+
this._config.nickname = nickname;
|
|
708
|
+
this._service.setDisplayName(nickname);
|
|
709
|
+
}
|
|
521
710
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// Private Methods
|
|
713
|
+
// ============================================================================
|
|
525
714
|
|
|
526
|
-
|
|
715
|
+
/**
|
|
527
716
|
* Merges configuration with defaults.
|
|
528
717
|
* @param {Object} defaults - Default config
|
|
529
718
|
* @param {Object} custom - Custom config
|
|
530
719
|
* @returns {Object} Merged config
|
|
531
720
|
* @private
|
|
532
721
|
*/
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
722
|
+
_mergeConfig(defaults, custom) {
|
|
723
|
+
return {
|
|
724
|
+
...defaults,
|
|
725
|
+
...custom,
|
|
726
|
+
encryption: { ...defaults.encryption, ...custom.encryption },
|
|
727
|
+
routing: { ...defaults.routing, ...custom.routing },
|
|
728
|
+
compression: { ...defaults.compression, ...custom.compression },
|
|
729
|
+
storeAndForward: { ...defaults.storeAndForward, ...custom.storeAndForward },
|
|
730
|
+
fileTransfer: { ...defaults.fileTransfer, ...custom.fileTransfer },
|
|
731
|
+
qualityConfig: { ...defaults.qualityConfig, ...custom.qualityConfig }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
545
736
|
* Sets up event forwarding from underlying services.
|
|
546
737
|
* @private
|
|
547
738
|
*/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
739
|
+
_setupEventForwarding() {
|
|
740
|
+
// Forward service events with PRD-style naming
|
|
741
|
+
this._service.on(EVENTS.PEER_DISCOVERED, (peer) => {
|
|
742
|
+
this._monitor.trackPeerDiscovered(peer.id);
|
|
743
|
+
this.emit('peerDiscovered', peer);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
this._service.on(EVENTS.PEER_CONNECTED, (peer) => {
|
|
747
|
+
if (peer.rssi) {
|
|
748
|
+
this._connectionQuality.recordRssi(peer.id, peer.rssi);
|
|
749
|
+
}
|
|
750
|
+
this.emit('peerConnected', peer);
|
|
751
|
+
// Deliver cached messages
|
|
752
|
+
if (this._storeForward && this._storeForward.hasCachedMessages(peer.id)) {
|
|
753
|
+
this._deliverCachedMessages(peer.id);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
this._service.on(EVENTS.PEER_DISCONNECTED, (peer) => {
|
|
758
|
+
this._monitor.trackPeerDisconnected(peer.id);
|
|
759
|
+
this._connectionQuality.removePeer(peer.id);
|
|
760
|
+
this.emit('peerDisconnected', peer);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
this._service.on('message', (message) => {
|
|
764
|
+
this._monitor.trackMessageReceived(message.senderId);
|
|
765
|
+
this.emit('messageReceived', {
|
|
766
|
+
from: message.senderId,
|
|
767
|
+
text: message.content,
|
|
768
|
+
timestamp: message.timestamp,
|
|
769
|
+
type: message.type
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
this._service.on('private-message', (message) => {
|
|
774
|
+
this.emit('directMessage', {
|
|
775
|
+
from: message.senderId,
|
|
776
|
+
text: message.content,
|
|
777
|
+
timestamp: message.timestamp
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
this._service.on('channel-message', (message) => {
|
|
782
|
+
this.emit('channelMessage', {
|
|
783
|
+
channel: message.channelId,
|
|
784
|
+
from: message.senderId,
|
|
785
|
+
text: message.content,
|
|
786
|
+
timestamp: message.timestamp
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
this._service.on('message-delivered', (info) => {
|
|
791
|
+
this._monitor.trackMessageDelivered(info.messageId);
|
|
792
|
+
this.emit('messageDelivered', info);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
this._service.on('error', (error) => {
|
|
796
|
+
this.emit('error', error);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Forward monitor events
|
|
800
|
+
this._monitor.on('health-changed', (info) => {
|
|
801
|
+
this.emit('networkHealthChanged', info);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Forward emergency events
|
|
805
|
+
this._emergencyManager.on('panic-wipe-completed', (result) => {
|
|
806
|
+
this.emit('dataWiped', result);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
616
811
|
* Sets up store and forward integration.
|
|
617
812
|
* @private
|
|
618
813
|
*/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
814
|
+
_setupStoreAndForward() {
|
|
815
|
+
// Nothing extra needed - handled in peerConnected event
|
|
816
|
+
}
|
|
622
817
|
|
|
623
|
-
|
|
818
|
+
/**
|
|
624
819
|
* Delivers cached messages to a peer.
|
|
625
820
|
* @param {string} peerId - Peer ID
|
|
626
821
|
* @private
|
|
627
822
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
823
|
+
async _deliverCachedMessages(peerId) {
|
|
824
|
+
if (!this._storeForward) { return; }
|
|
630
825
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
826
|
+
const sendFn = async (payload) => {
|
|
827
|
+
// Re-encrypt and send via the proper encrypted channel
|
|
828
|
+
try {
|
|
829
|
+
const text = _decoder.decode(payload);
|
|
830
|
+
await this._service.sendPrivateMessage(peerId, text);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
// Fallback to raw send if encryption fails
|
|
833
|
+
await this._service._sendRaw(peerId, payload);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
634
836
|
|
|
635
|
-
|
|
837
|
+
const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
|
|
636
838
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
839
|
+
if (result.delivered > 0) {
|
|
840
|
+
this.emit('cachedMessagesDelivered', {
|
|
841
|
+
peerId,
|
|
842
|
+
delivered: result.delivered
|
|
843
|
+
});
|
|
643
844
|
}
|
|
845
|
+
}
|
|
644
846
|
|
|
645
|
-
|
|
847
|
+
/**
|
|
646
848
|
* Registers data clearers for panic mode.
|
|
647
849
|
* @private
|
|
648
850
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// Clear store and forward cache
|
|
656
|
-
if (this._storeForward) {
|
|
657
|
-
this._emergencyManager.registerClearer(async () => {
|
|
658
|
-
this._storeForward.clear();
|
|
659
|
-
});
|
|
660
|
-
}
|
|
851
|
+
_registerPanicClearers() {
|
|
852
|
+
// Clear service data
|
|
853
|
+
this._emergencyManager.registerClearer(async () => {
|
|
854
|
+
await this._service.destroy();
|
|
855
|
+
});
|
|
661
856
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
857
|
+
// Clear store and forward cache
|
|
858
|
+
if (this._storeForward) {
|
|
859
|
+
this._emergencyManager.registerClearer(async () => {
|
|
860
|
+
this._storeForward.clear();
|
|
861
|
+
});
|
|
666
862
|
}
|
|
667
863
|
|
|
668
|
-
|
|
864
|
+
// Clear monitor data
|
|
865
|
+
this._emergencyManager.registerClearer(() => {
|
|
866
|
+
this._monitor.reset();
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
669
871
|
* Validates that network is running.
|
|
670
872
|
* @throws {MeshError} If not running
|
|
671
873
|
* @private
|
|
672
874
|
*/
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
875
|
+
_validateRunning() {
|
|
876
|
+
if (this._state !== 'running') {
|
|
877
|
+
throw new MeshError(
|
|
878
|
+
'MeshNetwork is not running. Call start() first.',
|
|
879
|
+
'E900',
|
|
880
|
+
{ state: this._state }
|
|
881
|
+
);
|
|
681
882
|
}
|
|
883
|
+
}
|
|
682
884
|
|
|
683
|
-
|
|
885
|
+
/**
|
|
684
886
|
* Checks if a peer is offline.
|
|
685
887
|
* @param {string} peerId - Peer ID
|
|
686
888
|
* @returns {boolean} True if offline
|
|
687
889
|
* @private
|
|
688
890
|
*/
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
891
|
+
_isPeerOffline(peerId) {
|
|
892
|
+
const peer = this._service.getPeer(peerId);
|
|
893
|
+
return !peer || !peer.isConnected;
|
|
894
|
+
}
|
|
693
895
|
|
|
694
|
-
|
|
896
|
+
/**
|
|
695
897
|
* Encodes a message for storage.
|
|
696
898
|
* @param {string} text - Message text
|
|
697
899
|
* @returns {Uint8Array} Encoded payload
|
|
698
900
|
* @private
|
|
699
901
|
*/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
902
|
+
_encodeMessage(text) {
|
|
903
|
+
return _encoder.encode(text);
|
|
904
|
+
}
|
|
703
905
|
|
|
704
|
-
|
|
906
|
+
/**
|
|
705
907
|
* Normalizes channel name (adds # if missing).
|
|
706
908
|
* @param {string} name - Channel name
|
|
707
909
|
* @returns {string} Normalized name
|
|
708
910
|
* @private
|
|
709
911
|
*/
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
912
|
+
_normalizeChannelName(name) {
|
|
913
|
+
return name.startsWith('#') ? name : `#${name}`;
|
|
914
|
+
}
|
|
713
915
|
|
|
714
|
-
|
|
916
|
+
/**
|
|
715
917
|
* Maximum allowed message size in bytes (1MB).
|
|
716
918
|
* @constant {number}
|
|
717
919
|
* @private
|
|
718
920
|
*/
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
921
|
+
static get MAX_MESSAGE_SIZE() {
|
|
922
|
+
return 1024 * 1024;
|
|
923
|
+
}
|
|
722
924
|
|
|
723
|
-
|
|
925
|
+
/**
|
|
724
926
|
* Validates message text input.
|
|
725
927
|
* @param {string} text - Message text to validate
|
|
726
928
|
* @throws {ValidationError} If text is invalid
|
|
727
929
|
* @private
|
|
728
930
|
*/
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (typeof text !== 'string') {
|
|
735
|
-
throw ValidationError.invalidType('text', text, 'string');
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (text.length === 0) {
|
|
739
|
-
throw ValidationError.invalidArgument('text', text, {
|
|
740
|
-
reason: 'Message text cannot be empty',
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Check size limit (UTF-8 encoded)
|
|
745
|
-
const byteLength = new TextEncoder().encode(text).length;
|
|
746
|
-
if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
|
|
747
|
-
throw ValidationError.outOfRange('text', byteLength, {
|
|
748
|
-
min: 1,
|
|
749
|
-
max: MeshNetwork.MAX_MESSAGE_SIZE,
|
|
750
|
-
});
|
|
751
|
-
}
|
|
931
|
+
_validateMessageText(text) {
|
|
932
|
+
if (text === null || text === undefined) {
|
|
933
|
+
throw ValidationError.missingArgument('text');
|
|
752
934
|
}
|
|
753
935
|
|
|
754
|
-
|
|
936
|
+
if (typeof text !== 'string') {
|
|
937
|
+
throw ValidationError.invalidType('text', text, 'string');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (text.length === 0) {
|
|
941
|
+
throw ValidationError.invalidArgument('text', text, {
|
|
942
|
+
reason: 'Message text cannot be empty'
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Check size limit (UTF-8 encoded)
|
|
947
|
+
const byteLength = _encoder.encode(text).length;
|
|
948
|
+
if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
|
|
949
|
+
throw ValidationError.outOfRange('text', byteLength, {
|
|
950
|
+
min: 1,
|
|
951
|
+
max: MeshNetwork.MAX_MESSAGE_SIZE
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
755
957
|
* Validates peer ID input.
|
|
756
958
|
* @param {string} peerId - Peer ID to validate
|
|
757
959
|
* @throws {ValidationError} If peerId is invalid
|
|
758
960
|
* @private
|
|
759
961
|
*/
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
962
|
+
_validatePeerId(peerId) {
|
|
963
|
+
if (peerId === null || peerId === undefined) {
|
|
964
|
+
throw ValidationError.missingArgument('peerId');
|
|
965
|
+
}
|
|
764
966
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
967
|
+
if (typeof peerId !== 'string') {
|
|
968
|
+
throw ValidationError.invalidType('peerId', peerId, 'string');
|
|
969
|
+
}
|
|
768
970
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
971
|
+
if (peerId.trim().length === 0) {
|
|
972
|
+
throw ValidationError.invalidArgument('peerId', peerId, {
|
|
973
|
+
reason: 'Peer ID cannot be empty or whitespace only'
|
|
974
|
+
});
|
|
774
975
|
}
|
|
976
|
+
}
|
|
775
977
|
}
|
|
776
978
|
|
|
777
979
|
// Export convenience constants
|
|
@@ -780,8 +982,8 @@ MeshNetwork.PanicTrigger = PANIC_TRIGGER;
|
|
|
780
982
|
MeshNetwork.HealthStatus = HEALTH_STATUS;
|
|
781
983
|
|
|
782
984
|
module.exports = {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
985
|
+
MeshNetwork,
|
|
986
|
+
BATTERY_MODE,
|
|
987
|
+
PANIC_TRIGGER,
|
|
988
|
+
HEALTH_STATUS
|
|
787
989
|
};
|