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