react-native-ble-mesh 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -172
  2. package/docs/IOS-BACKGROUND-BLE.md +231 -0
  3. package/docs/OPTIMIZATION.md +183 -0
  4. package/docs/SPEC-v2.1.md +308 -0
  5. package/package.json +1 -1
  6. package/src/MeshNetwork.js +667 -465
  7. package/src/constants/index.js +1 -0
  8. package/src/crypto/AutoCrypto.js +90 -0
  9. package/src/crypto/CryptoProvider.js +99 -0
  10. package/src/crypto/index.js +15 -63
  11. package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
  12. package/src/crypto/providers/QuickCryptoProvider.js +158 -0
  13. package/src/crypto/providers/TweetNaClProvider.js +124 -0
  14. package/src/crypto/providers/index.js +11 -0
  15. package/src/errors/MeshError.js +2 -1
  16. package/src/expo/withBLEMesh.js +102 -0
  17. package/src/hooks/AppStateManager.js +9 -1
  18. package/src/hooks/useMesh.js +47 -13
  19. package/src/hooks/useMessages.js +6 -4
  20. package/src/hooks/usePeers.js +13 -9
  21. package/src/index.js +23 -8
  22. package/src/mesh/dedup/BloomFilter.js +44 -57
  23. package/src/mesh/dedup/DedupManager.js +67 -10
  24. package/src/mesh/fragment/Assembler.js +5 -0
  25. package/src/mesh/fragment/Fragmenter.js +1 -1
  26. package/src/mesh/index.js +1 -1
  27. package/src/mesh/monitor/ConnectionQuality.js +433 -0
  28. package/src/mesh/monitor/NetworkMonitor.js +376 -320
  29. package/src/mesh/monitor/index.js +7 -3
  30. package/src/mesh/peer/Peer.js +5 -2
  31. package/src/mesh/peer/PeerManager.js +21 -4
  32. package/src/mesh/router/MessageRouter.js +38 -19
  33. package/src/mesh/router/RouteTable.js +24 -8
  34. package/src/mesh/store/StoreAndForwardManager.js +305 -296
  35. package/src/mesh/store/index.js +1 -1
  36. package/src/protocol/deserializer.js +9 -10
  37. package/src/protocol/header.js +13 -7
  38. package/src/protocol/message.js +15 -3
  39. package/src/protocol/serializer.js +7 -10
  40. package/src/protocol/validator.js +23 -5
  41. package/src/service/BatteryOptimizer.js +285 -278
  42. package/src/service/EmergencyManager.js +224 -214
  43. package/src/service/HandshakeManager.js +163 -13
  44. package/src/service/MeshService.js +72 -6
  45. package/src/service/SessionManager.js +79 -2
  46. package/src/service/audio/AudioManager.js +8 -2
  47. package/src/service/file/FileAssembler.js +106 -0
  48. package/src/service/file/FileChunker.js +79 -0
  49. package/src/service/file/FileManager.js +307 -0
  50. package/src/service/file/FileMessage.js +122 -0
  51. package/src/service/file/index.js +15 -0
  52. package/src/service/text/TextManager.js +21 -15
  53. package/src/service/text/broadcast/BroadcastManager.js +16 -0
  54. package/src/storage/MessageStore.js +55 -2
  55. package/src/transport/BLETransport.js +141 -10
  56. package/src/transport/MockTransport.js +1 -1
  57. package/src/transport/MultiTransport.js +330 -0
  58. package/src/transport/WiFiDirectTransport.js +296 -0
  59. package/src/transport/adapters/NodeBLEAdapter.js +34 -0
  60. package/src/transport/adapters/RNBLEAdapter.js +56 -1
  61. package/src/transport/index.js +6 -0
  62. package/src/utils/EventEmitter.js +6 -9
  63. package/src/utils/bytes.js +12 -10
  64. package/src/utils/compression.js +293 -291
  65. package/src/utils/encoding.js +33 -8
  66. package/src/crypto/aead.js +0 -189
  67. package/src/crypto/chacha20.js +0 -181
  68. package/src/crypto/hkdf.js +0 -187
  69. package/src/crypto/hmac.js +0 -143
  70. package/src/crypto/keys/KeyManager.js +0 -271
  71. package/src/crypto/keys/KeyPair.js +0 -216
  72. package/src/crypto/keys/SecureStorage.js +0 -219
  73. package/src/crypto/keys/index.js +0 -32
  74. package/src/crypto/noise/handshake.js +0 -410
  75. package/src/crypto/noise/index.js +0 -27
  76. package/src/crypto/noise/session.js +0 -253
  77. package/src/crypto/noise/state.js +0 -268
  78. package/src/crypto/poly1305.js +0 -113
  79. package/src/crypto/sha256.js +0 -240
  80. package/src/crypto/x25519.js +0 -154
@@ -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, 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');
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
- /** 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
- },
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
- constructor(config = {}) {
88
- super();
93
+ constructor(config = {}) {
94
+ super();
89
95
 
90
- /**
96
+ /**
91
97
  * Configuration
92
98
  * @type {Object}
93
99
  * @private
94
100
  */
95
- this._config = this._mergeConfig(DEFAULT_CONFIG, config);
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
- this._service = new MeshService({
103
- displayName: this._config.nickname,
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
- this._transport = null;
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
- 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
- /**
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
- this._monitor = new NetworkMonitor();
136
+ this._monitor = new NetworkMonitor();
131
137
 
132
- /**
138
+ /**
133
139
  * Battery optimizer
134
140
  * @type {BatteryOptimizer}
135
141
  * @private
136
142
  */
137
- this._batteryOptimizer = new BatteryOptimizer({
138
- initialMode: this._config.batteryMode,
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
- this._emergencyManager = new EmergencyManager();
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
- this._compressor = new MessageCompressor({
154
- threshold: this._config.compression.threshold,
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
- this._channels = null;
186
+ this._channels = null;
163
187
 
164
- /**
188
+ /**
165
189
  * Network state
166
190
  * @type {string}
167
191
  * @private
168
192
  */
169
- this._state = 'stopped';
193
+ this._state = 'stopped';
170
194
 
171
- // Setup event forwarding
172
- this._setupEventForwarding();
173
- }
195
+ // Setup event forwarding
196
+ this._setupEventForwarding();
197
+ }
174
198
 
175
- // ============================================================================
176
- // Lifecycle Methods
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
- 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
- /**
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
- async stop() {
221
- if (this._state !== 'running') {
222
- return;
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
- async destroy() {
235
- await this.stop();
236
- await this._service.destroy();
298
+ async destroy() {
299
+ await this.stop();
300
+ await this._service.destroy();
237
301
 
238
- if (this._storeForward) {
239
- this._storeForward.destroy();
240
- }
241
- this._monitor.destroy();
242
- this._batteryOptimizer.destroy();
243
- this._emergencyManager.destroy();
302
+ this._connectionQuality.destroy();
303
+ this._fileManager.destroy();
244
304
 
245
- this.removeAllListeners();
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
- // Messaging Methods
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
- async broadcast(text) {
259
- this._validateRunning();
260
- this._validateMessageText(text);
325
+ async broadcast(text) {
326
+ this._validateRunning();
327
+ this._validateMessageText(text);
261
328
 
262
- const messageId = await this._service.sendBroadcast(text);
263
- this._monitor.trackMessageSent('broadcast', messageId);
329
+ const messageId = await this._service.sendBroadcast(text);
330
+ this._monitor.trackMessageSent('broadcast', messageId);
264
331
 
265
- return messageId;
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
- 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
- }
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
- // Channel Methods
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
- async joinChannel(channelName, password) {
307
- this._validateRunning();
375
+ async joinChannel(channelName, password) {
376
+ this._validateRunning();
308
377
 
309
- const normalized = this._normalizeChannelName(channelName);
310
- await this._service.joinChannel(normalized, password);
378
+ const normalized = this._normalizeChannelName(channelName);
379
+ await this._service.joinChannel(normalized, password);
311
380
 
312
- this.emit('channelJoined', { channel: normalized });
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
- async leaveChannel(channelName) {
321
- this._validateRunning();
389
+ async leaveChannel(channelName) {
390
+ this._validateRunning();
322
391
 
323
- const normalized = this._normalizeChannelName(channelName);
324
- await this._service.leaveChannel(normalized);
392
+ const normalized = this._normalizeChannelName(channelName);
393
+ await this._service.leaveChannel(normalized);
325
394
 
326
- this.emit('channelLeft', { channel: normalized });
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
- async sendToChannel(channelName, text) {
336
- this._validateRunning();
404
+ async sendToChannel(channelName, text) {
405
+ this._validateRunning();
337
406
 
338
- const normalized = this._normalizeChannelName(channelName);
339
- return await this._service.sendChannelMessage(normalized, text);
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
- getChannels() {
347
- return this._service.getChannels();
348
- }
415
+ getChannels() {
416
+ return this._service.getChannels();
417
+ }
349
418
 
350
- // ============================================================================
351
- // Peer Methods
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
- getPeers() {
359
- return this._service.getPeers();
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
- getConnectedPeers() {
367
- return this._service.getConnectedPeers();
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
- getSecuredPeers() {
375
- return this._service.getSecuredPeers();
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
- blockPeer(peerId) {
383
- this._service.blockPeer(peerId);
384
- this.emit('peerBlocked', { peerId });
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
- unblockPeer(peerId) {
392
- this._service.unblockPeer(peerId);
393
- this.emit('peerUnblocked', { peerId });
394
- }
460
+ unblockPeer(peerId) {
461
+ this._service.unblockPeer(peerId);
462
+ this.emit('peerUnblocked', { peerId });
463
+ }
395
464
 
396
- // ============================================================================
397
- // Network Health Methods
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
- getNetworkHealth() {
405
- return this._monitor.generateHealthReport();
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
- getPeerHealth(peerId) {
414
- return this._monitor.getNodeHealth(peerId);
415
- }
482
+ getPeerHealth(peerId) {
483
+ return this._monitor.getNodeHealth(peerId);
484
+ }
416
485
 
417
- // ============================================================================
418
- // Battery Management
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
- async setBatteryMode(mode) {
427
- await this._batteryOptimizer.setMode(mode);
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
- getBatteryMode() {
435
- return this._batteryOptimizer.getMode();
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
- updateBatteryLevel(level, charging = false) {
444
- this._batteryOptimizer.updateBatteryLevel(level, charging);
445
- }
512
+ updateBatteryLevel(level, charging = false) {
513
+ this._batteryOptimizer.updateBatteryLevel(level, charging);
514
+ }
446
515
 
447
- // ============================================================================
448
- // Security Methods
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
- enablePanicMode(options = {}) {
458
- this._emergencyManager.enablePanicMode(options);
459
- this.emit('panicModeEnabled');
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
- disablePanicMode() {
466
- this._emergencyManager.disablePanicMode();
467
- this.emit('panicModeDisabled');
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
- registerPanicTap() {
474
- this._emergencyManager.registerTap();
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
- async wipeAllData() {
482
- return await this._emergencyManager.wipeAllData();
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
- // Status Methods
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
- 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
- /**
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
- getIdentity() {
510
- return this._service.getIdentity();
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
- setNickname(nickname) {
518
- this._config.nickname = nickname;
519
- this._service.setDisplayName(nickname);
520
- }
706
+ setNickname(nickname) {
707
+ this._config.nickname = nickname;
708
+ this._service.setDisplayName(nickname);
709
+ }
521
710
 
522
- // ============================================================================
523
- // Private Methods
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
- _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
- /**
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
- _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
- /**
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
- _setupStoreAndForward() {
620
- // Nothing extra needed - handled in peerConnected event
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
- async _deliverCachedMessages(peerId) {
629
- if (!this._storeForward) return;
823
+ async _deliverCachedMessages(peerId) {
824
+ if (!this._storeForward) { return; }
630
825
 
631
- const sendFn = async (payload) => {
632
- await this._service._sendRaw(peerId, payload);
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
- const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
837
+ const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
636
838
 
637
- if (result.delivered > 0) {
638
- this.emit('cachedMessagesDelivered', {
639
- peerId,
640
- delivered: result.delivered,
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
- _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
- }
851
+ _registerPanicClearers() {
852
+ // Clear service data
853
+ this._emergencyManager.registerClearer(async () => {
854
+ await this._service.destroy();
855
+ });
661
856
 
662
- // Clear monitor data
663
- this._emergencyManager.registerClearer(() => {
664
- this._monitor.reset();
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
- _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
- }
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
- _isPeerOffline(peerId) {
690
- const peer = this._service.getPeer(peerId);
691
- return !peer || !peer.isConnected;
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
- _encodeMessage(text) {
701
- return new TextEncoder().encode(text);
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
- _normalizeChannelName(name) {
711
- return name.startsWith('#') ? name : `#${name}`;
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
- static get MAX_MESSAGE_SIZE() {
720
- return 1024 * 1024;
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
- _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
- }
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
- _validatePeerId(peerId) {
761
- if (peerId === null || peerId === undefined) {
762
- throw ValidationError.missingArgument('peerId');
763
- }
962
+ _validatePeerId(peerId) {
963
+ if (peerId === null || peerId === undefined) {
964
+ throw ValidationError.missingArgument('peerId');
965
+ }
764
966
 
765
- if (typeof peerId !== 'string') {
766
- throw ValidationError.invalidType('peerId', peerId, 'string');
767
- }
967
+ if (typeof peerId !== 'string') {
968
+ throw ValidationError.invalidType('peerId', peerId, 'string');
969
+ }
768
970
 
769
- if (peerId.trim().length === 0) {
770
- throw ValidationError.invalidArgument('peerId', peerId, {
771
- reason: 'Peer ID cannot be empty or whitespace only',
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
- MeshNetwork,
784
- BATTERY_MODE,
785
- PANIC_TRIGGER,
786
- HEALTH_STATUS,
985
+ MeshNetwork,
986
+ BATTERY_MODE,
987
+ PANIC_TRIGGER,
988
+ HEALTH_STATUS
787
989
  };