react-native-ble-mesh 2.0.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +2 -2
  2. package/docs/OPTIMIZATION.md +165 -52
  3. package/package.json +1 -1
  4. package/src/MeshNetwork.js +63 -53
  5. package/src/constants/audio.js +4 -4
  6. package/src/constants/ble.js +1 -1
  7. package/src/constants/crypto.js +1 -1
  8. package/src/constants/errors.js +2 -2
  9. package/src/constants/events.js +1 -1
  10. package/src/constants/protocol.js +2 -2
  11. package/src/crypto/AutoCrypto.js +16 -3
  12. package/src/crypto/CryptoProvider.js +17 -17
  13. package/src/crypto/providers/ExpoCryptoProvider.js +15 -9
  14. package/src/crypto/providers/QuickCryptoProvider.js +41 -12
  15. package/src/crypto/providers/TweetNaClProvider.js +10 -8
  16. package/src/errors/AudioError.js +2 -1
  17. package/src/errors/ConnectionError.js +2 -2
  18. package/src/errors/CryptoError.js +1 -1
  19. package/src/errors/HandshakeError.js +2 -2
  20. package/src/errors/MeshError.js +4 -4
  21. package/src/errors/MessageError.js +2 -2
  22. package/src/errors/ValidationError.js +3 -3
  23. package/src/expo/withBLEMesh.js +10 -10
  24. package/src/hooks/AppStateManager.js +11 -2
  25. package/src/hooks/useMesh.js +23 -10
  26. package/src/hooks/useMessages.js +17 -16
  27. package/src/hooks/usePeers.js +19 -14
  28. package/src/index.js +2 -2
  29. package/src/mesh/dedup/BloomFilter.js +45 -57
  30. package/src/mesh/dedup/DedupManager.js +36 -8
  31. package/src/mesh/dedup/MessageCache.js +3 -0
  32. package/src/mesh/fragment/Assembler.js +5 -4
  33. package/src/mesh/fragment/Fragmenter.js +3 -3
  34. package/src/mesh/monitor/ConnectionQuality.js +59 -25
  35. package/src/mesh/monitor/NetworkMonitor.js +80 -28
  36. package/src/mesh/peer/Peer.js +9 -11
  37. package/src/mesh/peer/PeerDiscovery.js +18 -19
  38. package/src/mesh/peer/PeerManager.js +29 -17
  39. package/src/mesh/router/MessageRouter.js +28 -20
  40. package/src/mesh/router/PathFinder.js +10 -13
  41. package/src/mesh/router/RouteTable.js +25 -14
  42. package/src/mesh/store/StoreAndForwardManager.js +32 -24
  43. package/src/protocol/deserializer.js +9 -10
  44. package/src/protocol/header.js +13 -7
  45. package/src/protocol/message.js +18 -14
  46. package/src/protocol/serializer.js +9 -12
  47. package/src/protocol/validator.js +29 -10
  48. package/src/service/BatteryOptimizer.js +22 -18
  49. package/src/service/EmergencyManager.js +18 -25
  50. package/src/service/HandshakeManager.js +112 -18
  51. package/src/service/MeshService.js +106 -22
  52. package/src/service/SessionManager.js +50 -13
  53. package/src/service/audio/AudioManager.js +80 -38
  54. package/src/service/audio/buffer/FrameBuffer.js +7 -8
  55. package/src/service/audio/buffer/JitterBuffer.js +1 -1
  56. package/src/service/audio/codec/LC3Codec.js +18 -19
  57. package/src/service/audio/codec/LC3Decoder.js +10 -10
  58. package/src/service/audio/codec/LC3Encoder.js +11 -9
  59. package/src/service/audio/session/AudioSession.js +14 -17
  60. package/src/service/audio/session/VoiceMessage.js +15 -22
  61. package/src/service/audio/transport/AudioFragmenter.js +17 -9
  62. package/src/service/audio/transport/AudioFramer.js +8 -12
  63. package/src/service/file/FileAssembler.js +4 -2
  64. package/src/service/file/FileChunker.js +1 -1
  65. package/src/service/file/FileManager.js +26 -20
  66. package/src/service/file/FileMessage.js +7 -12
  67. package/src/service/text/TextManager.js +75 -42
  68. package/src/service/text/broadcast/BroadcastManager.js +14 -17
  69. package/src/service/text/channel/Channel.js +10 -14
  70. package/src/service/text/channel/ChannelManager.js +10 -10
  71. package/src/service/text/message/TextMessage.js +12 -19
  72. package/src/service/text/message/TextSerializer.js +2 -2
  73. package/src/storage/AsyncStorageAdapter.js +17 -14
  74. package/src/storage/MemoryStorage.js +11 -8
  75. package/src/storage/MessageStore.js +77 -32
  76. package/src/storage/Storage.js +9 -9
  77. package/src/transport/BLETransport.js +27 -16
  78. package/src/transport/MockTransport.js +7 -2
  79. package/src/transport/MultiTransport.js +43 -11
  80. package/src/transport/Transport.js +9 -9
  81. package/src/transport/WiFiDirectTransport.js +26 -20
  82. package/src/transport/adapters/BLEAdapter.js +19 -19
  83. package/src/transport/adapters/NodeBLEAdapter.js +24 -23
  84. package/src/transport/adapters/RNBLEAdapter.js +14 -11
  85. package/src/utils/EventEmitter.js +15 -16
  86. package/src/utils/LRUCache.js +10 -4
  87. package/src/utils/RateLimiter.js +1 -1
  88. package/src/utils/bytes.js +12 -10
  89. package/src/utils/compression.js +10 -8
  90. package/src/utils/encoding.js +39 -8
  91. package/src/utils/retry.js +11 -13
  92. package/src/utils/time.js +9 -4
  93. package/src/utils/validation.js +1 -1
@@ -21,15 +21,27 @@ const STATE = Object.freeze({
21
21
  * @extends EventEmitter
22
22
  */
23
23
  class HandshakeManager extends EventEmitter {
24
+ /**
25
+ * @param {any} keyManager
26
+ * @param {any} sessionManager
27
+ */
24
28
  constructor(keyManager, sessionManager) {
25
29
  super();
26
30
  if (!keyManager || !sessionManager) { throw new Error('keyManager and sessionManager required'); }
31
+ /** @type {any} */
27
32
  this._keyManager = keyManager;
33
+ /** @type {any} */
28
34
  this._sessionManager = sessionManager;
35
+ /** @type {Map<string, any>} */
29
36
  this._pending = new Map();
37
+ /** @type {number} */
30
38
  this._timeout = MESH_CONFIG.HANDSHAKE_TIMEOUT_MS;
31
39
  }
32
40
 
41
+ /**
42
+ * @param {string} peerId
43
+ * @param {any} transport
44
+ */
33
45
  async initiateHandshake(peerId, transport) {
34
46
  if (this._pending.has(peerId)) { throw HandshakeError.alreadyInProgress(peerId); }
35
47
 
@@ -44,12 +56,18 @@ class HandshakeManager extends EventEmitter {
44
56
  hs.step = 1;
45
57
  this.emit(EVENTS.HANDSHAKE_PROGRESS, { peerId, step: 1, role: 'initiator' });
46
58
  return await this._waitForCompletion(peerId);
47
- } catch (err) {
59
+ } catch (/** @type {any} */ err) {
48
60
  this._fail(peerId, err);
49
61
  throw err;
50
62
  }
51
63
  }
52
64
 
65
+ /**
66
+ * @param {string} peerId
67
+ * @param {number} type
68
+ * @param {Uint8Array} payload
69
+ * @param {any} transport
70
+ */
53
71
  async handleIncomingHandshake(peerId, type, payload, transport) {
54
72
  try {
55
73
  if (type === MESSAGE_TYPE.HANDSHAKE_INIT) {
@@ -62,12 +80,15 @@ class HandshakeManager extends EventEmitter {
62
80
  return await this._onFinal(peerId, payload);
63
81
  }
64
82
  throw HandshakeError.handshakeFailed(peerId, null, { reason: 'Unknown type' });
65
- } catch (err) {
83
+ } catch (/** @type {any} */ err) {
66
84
  this._fail(peerId, err);
67
85
  throw err;
68
86
  }
69
87
  }
70
88
 
89
+ /**
90
+ * @param {string} peerId
91
+ */
71
92
  cancelHandshake(peerId) {
72
93
  const hs = this._pending.get(peerId);
73
94
  if (!hs) { return; }
@@ -77,9 +98,20 @@ class HandshakeManager extends EventEmitter {
77
98
  this.emit(EVENTS.HANDSHAKE_FAILED, { peerId, reason: 'cancelled' });
78
99
  }
79
100
 
101
+ /**
102
+ * @param {string} peerId
103
+ * @returns {boolean}
104
+ */
80
105
  isHandshakePending(peerId) { return this._pending.has(peerId); }
106
+ /** @returns {number} */
81
107
  getPendingCount() { return this._pending.size; }
82
108
 
109
+ /**
110
+ * @param {string} peerId
111
+ * @param {boolean} isInitiator
112
+ * @returns {any}
113
+ * @private
114
+ */
83
115
  _createState(peerId, isInitiator) {
84
116
  const kp = this._keyManager.getStaticKeyPair();
85
117
  return {
@@ -88,11 +120,18 @@ class HandshakeManager extends EventEmitter {
88
120
  };
89
121
  }
90
122
 
123
+ /**
124
+ * @param {any} keyPair
125
+ * @param {boolean} isInitiator
126
+ * @returns {any}
127
+ * @private
128
+ */
91
129
  _createNoise(keyPair, isInitiator) {
92
130
  // Get crypto provider from keyManager if available
93
131
  const provider = this._keyManager.provider;
94
132
 
95
133
  // Generate ephemeral key pair for this handshake
134
+ /** @type {any} */
96
135
  let ephemeralKeyPair;
97
136
  if (provider && typeof provider.generateKeyPair === 'function') {
98
137
  ephemeralKeyPair = provider.generateKeyPair();
@@ -101,11 +140,18 @@ class HandshakeManager extends EventEmitter {
101
140
  ephemeralKeyPair = { publicKey: randomBytes(32), secretKey: randomBytes(32) };
102
141
  }
103
142
 
143
+ /** @type {Uint8Array | null} */
104
144
  let remoteEphemeralPublic = null;
145
+ /** @type {Uint8Array | null} */
105
146
  let sharedSecret = null;
147
+ /** @type {any} */
106
148
  let sessionKeys = null;
107
149
  let complete = false;
108
150
 
151
+ /**
152
+ * @param {Uint8Array} secret
153
+ * @returns {any}
154
+ */
109
155
  const deriveSessionKeys = (secret) => {
110
156
  // Derive send/receive keys from shared secret
111
157
  // Use hash to derive two different keys from the secret
@@ -125,6 +171,7 @@ class HandshakeManager extends EventEmitter {
125
171
  return ephemeralKeyPair.publicKey;
126
172
  },
127
173
 
174
+ /** @param {Uint8Array} msg */
128
175
  readMessage1: (msg) => {
129
176
  // Responder receives initiator's ephemeral public key
130
177
  remoteEphemeralPublic = new Uint8Array(msg);
@@ -137,10 +184,12 @@ class HandshakeManager extends EventEmitter {
137
184
  } else {
138
185
  throw new Error('Crypto provider required for secure handshake. Install tweetnacl: npm install tweetnacl');
139
186
  }
187
+ // @ts-ignore
140
188
  sessionKeys = deriveSessionKeys(sharedSecret);
141
189
  return ephemeralKeyPair.publicKey;
142
190
  },
143
191
 
192
+ /** @param {Uint8Array} msg */
144
193
  readMessage2: (msg) => {
145
194
  // Initiator receives responder's ephemeral public key and derives shared secret
146
195
  remoteEphemeralPublic = new Uint8Array(msg);
@@ -149,6 +198,7 @@ class HandshakeManager extends EventEmitter {
149
198
  } else {
150
199
  throw new Error('Crypto provider required for secure handshake. Install tweetnacl: npm install tweetnacl');
151
200
  }
201
+ // @ts-ignore
152
202
  sessionKeys = deriveSessionKeys(sharedSecret);
153
203
  },
154
204
 
@@ -158,6 +208,7 @@ class HandshakeManager extends EventEmitter {
158
208
  return ephemeralKeyPair.publicKey;
159
209
  },
160
210
 
211
+ /** @param {any} _msg */
161
212
  readMessage3: (_msg) => {
162
213
  // Responder confirms handshake completion
163
214
  complete = true;
@@ -177,32 +228,30 @@ class HandshakeManager extends EventEmitter {
177
228
  let sendNonce = 0;
178
229
  let recvNonce = 0;
179
230
 
231
+ // Pre-allocate nonce buffers per direction to avoid per-call allocation
232
+ const sendNonceBuf = new Uint8Array(24);
233
+ const sendNonceView = new DataView(sendNonceBuf.buffer);
234
+ const recvNonceBuf = new Uint8Array(24);
235
+ const recvNonceView = new DataView(recvNonceBuf.buffer);
236
+
180
237
  return {
238
+ /** @param {Uint8Array} plaintext */
181
239
  encrypt: (plaintext) => {
182
240
  if (provider && typeof provider.encrypt === 'function') {
183
- // Use proper AEAD encryption with nonce
184
- const nonce = new Uint8Array(24); // tweetnacl uses 24-byte nonces
185
- const view = new DataView(nonce.buffer);
186
241
  // Store counter in last 8 bytes of nonce
187
- if (nonce.byteLength >= 8) {
188
- view.setUint32(nonce.byteLength - 8, 0, true);
189
- view.setUint32(nonce.byteLength - 4, sendNonce++, true);
190
- }
191
- return provider.encrypt(sendKey, nonce, plaintext);
242
+ sendNonceView.setUint32(16, 0, true);
243
+ sendNonceView.setUint32(20, sendNonce++, true);
244
+ return provider.encrypt(sendKey, sendNonceBuf, plaintext);
192
245
  }
193
246
  throw new Error('Crypto provider required for encryption');
194
247
  },
195
248
 
249
+ /** @param {Uint8Array} ciphertext */
196
250
  decrypt: (ciphertext) => {
197
251
  if (provider && typeof provider.decrypt === 'function') {
198
- // Use proper AEAD decryption with nonce
199
- const nonce = new Uint8Array(24);
200
- const view = new DataView(nonce.buffer);
201
- if (nonce.byteLength >= 8) {
202
- view.setUint32(nonce.byteLength - 8, 0, true);
203
- view.setUint32(nonce.byteLength - 4, recvNonce++, true);
204
- }
205
- return provider.decrypt(recvKey, nonce, ciphertext);
252
+ recvNonceView.setUint32(16, 0, true);
253
+ recvNonceView.setUint32(20, recvNonce++, true);
254
+ return provider.decrypt(recvKey, recvNonceBuf, ciphertext);
206
255
  }
207
256
  throw new Error('Crypto provider required for encryption');
208
257
  },
@@ -219,6 +268,12 @@ class HandshakeManager extends EventEmitter {
219
268
  };
220
269
  }
221
270
 
271
+ /**
272
+ * @param {string} peerId
273
+ * @param {Uint8Array} payload
274
+ * @param {any} transport
275
+ * @private
276
+ */
222
277
  async _onInit(peerId, payload, transport) {
223
278
  const existing = this._pending.get(peerId);
224
279
 
@@ -249,6 +304,12 @@ class HandshakeManager extends EventEmitter {
249
304
  return null;
250
305
  }
251
306
 
307
+ /**
308
+ * @param {string} peerId
309
+ * @param {Uint8Array} payload
310
+ * @param {any} transport
311
+ * @private
312
+ */
252
313
  async _onResponse(peerId, payload, transport) {
253
314
  const hs = this._pending.get(peerId);
254
315
  if (!hs || !hs.isInitiator) {
@@ -262,6 +323,11 @@ class HandshakeManager extends EventEmitter {
262
323
  return this._complete(peerId, hs);
263
324
  }
264
325
 
326
+ /**
327
+ * @param {string} peerId
328
+ * @param {Uint8Array} payload
329
+ * @private
330
+ */
265
331
  async _onFinal(peerId, payload) {
266
332
  const hs = this._pending.get(peerId);
267
333
  if (!hs || hs.isInitiator) { throw HandshakeError.invalidState(peerId, 3); }
@@ -270,6 +336,11 @@ class HandshakeManager extends EventEmitter {
270
336
  return this._complete(peerId, hs);
271
337
  }
272
338
 
339
+ /**
340
+ * @param {string} peerId
341
+ * @param {any} hs
342
+ * @private
343
+ */
273
344
  _complete(peerId, hs) {
274
345
  if (hs.timer) { clearTimeout(hs.timer); }
275
346
  const session = hs.noise.getSession();
@@ -283,6 +354,11 @@ class HandshakeManager extends EventEmitter {
283
354
  return session;
284
355
  }
285
356
 
357
+ /**
358
+ * @param {string} peerId
359
+ * @param {any} error
360
+ * @private
361
+ */
286
362
  _fail(peerId, error) {
287
363
  const hs = this._pending.get(peerId);
288
364
  if (hs) {
@@ -293,6 +369,10 @@ class HandshakeManager extends EventEmitter {
293
369
  this.emit(EVENTS.HANDSHAKE_FAILED, { peerId, error: error.message, step: hs?.step });
294
370
  }
295
371
 
372
+ /**
373
+ * @param {string} peerId
374
+ * @private
375
+ */
296
376
  _setTimeout(peerId) {
297
377
  const hs = this._pending.get(peerId);
298
378
  if (!hs) { return; }
@@ -303,6 +383,10 @@ class HandshakeManager extends EventEmitter {
303
383
  }, this._timeout);
304
384
  }
305
385
 
386
+ /**
387
+ * @param {string} peerId
388
+ * @private
389
+ */
306
390
  _waitForCompletion(peerId) {
307
391
  return new Promise((resolve, reject) => {
308
392
  const hs = this._pending.get(peerId);
@@ -313,6 +397,11 @@ class HandshakeManager extends EventEmitter {
313
397
  });
314
398
  }
315
399
 
400
+ /**
401
+ * @param {any} localKey
402
+ * @param {any} remoteId
403
+ * @private
404
+ */
316
405
  _compareKeys(localKey, remoteId) {
317
406
  // Simple string/byte comparison for deterministic tie-breaking
318
407
  const localStr = typeof localKey === 'string' ? localKey : Array.from(localKey).join(',');
@@ -320,6 +409,11 @@ class HandshakeManager extends EventEmitter {
320
409
  return localStr < remoteStr ? -1 : localStr > remoteStr ? 1 : 0;
321
410
  }
322
411
 
412
+ /**
413
+ * @param {number} type
414
+ * @param {Uint8Array} payload
415
+ * @private
416
+ */
323
417
  _wrap(type, payload) {
324
418
  const w = new Uint8Array(1 + payload.length);
325
419
  w[0] = type;
@@ -19,21 +19,38 @@ const { TextManager, ChannelManager } = require('./text');
19
19
  * @extends EventEmitter
20
20
  */
21
21
  class MeshService extends EventEmitter {
22
+ /**
23
+ * @param {any} [config]
24
+ */
22
25
  constructor(config = {}) {
23
26
  super();
27
+ /** @type {any} */
24
28
  this._config = { displayName: 'Anonymous', ...config };
29
+ /** @type {string} */
25
30
  this._state = SERVICE_STATE.UNINITIALIZED;
31
+ /** @type {any} */
26
32
  this._transport = null;
33
+ /** @type {any} */
27
34
  this._keyManager = null;
35
+ /** @type {SessionManager | null} */
28
36
  this._sessionManager = null;
37
+ /** @type {HandshakeManager | null} */
29
38
  this._handshakeManager = null;
39
+ /** @type {any} */
30
40
  this._channelManager = null;
41
+ /** @type {any} */
31
42
  this._peerManager = null;
43
+ /** @type {any} */
32
44
  this._audioManager = null;
45
+ /** @type {any} */
33
46
  this._textManager = null;
47
+ /** @type {number} */
34
48
  this._messageCounter = 0;
35
49
  }
36
50
 
51
+ /**
52
+ * @param {any} [options]
53
+ */
37
54
  async initialize(options = {}) {
38
55
  if (this._state !== SERVICE_STATE.UNINITIALIZED) {
39
56
  throw new MeshError('Service already initialized', ERROR_CODE.E002);
@@ -47,12 +64,15 @@ class MeshService extends EventEmitter {
47
64
  this._setupEventForwarding();
48
65
  this._setState(SERVICE_STATE.READY);
49
66
  this.emit(EVENTS.INITIALIZED);
50
- } catch (err) {
67
+ } catch (/** @type {any} */ err) {
51
68
  this._setState(SERVICE_STATE.ERROR);
52
69
  throw new MeshError(`Initialization failed: ${err.message}`, ERROR_CODE.E001);
53
70
  }
54
71
  }
55
72
 
73
+ /**
74
+ * @param {any} transport
75
+ */
56
76
  async start(transport) {
57
77
  this._validateState([SERVICE_STATE.READY, SERVICE_STATE.SUSPENDED]);
58
78
  if (!transport) { throw new ValidationError('Transport is required', ERROR_CODE.E802); }
@@ -87,31 +107,52 @@ class MeshService extends EventEmitter {
87
107
  };
88
108
  }
89
109
 
110
+ /**
111
+ * @param {string} name
112
+ */
90
113
  setDisplayName(name) { this._config.displayName = name; }
91
114
  exportIdentity() { return this._keyManager?.exportIdentity() || null; }
115
+ /**
116
+ * @param {any} identity
117
+ */
92
118
  importIdentity(identity) { this._keyManager?.importIdentity(identity); }
93
119
 
94
120
  getPeers() { return this._peerManager?.getAllPeers() || []; }
121
+ /**
122
+ * @param {string} id
123
+ */
95
124
  getPeer(id) { return this._peerManager?.getPeer(id); }
96
125
  getConnectedPeers() { return this._peerManager?.getConnectedPeers() || []; }
97
126
  getSecuredPeers() { return this._sessionManager?.getAllSessionPeerIds() || []; }
98
127
 
128
+ /**
129
+ * @param {string} peerId
130
+ */
99
131
  async initiateHandshake(peerId) {
100
132
  this._validateState([SERVICE_STATE.ACTIVE]);
101
- return this._handshakeManager.initiateHandshake(peerId, this._transport);
133
+ return this._handshakeManager?.initiateHandshake(peerId, this._transport);
102
134
  }
103
135
 
136
+ /**
137
+ * @param {string} id
138
+ */
104
139
  blockPeer(id) {
105
140
  this._peerManager?.blockPeer(id);
106
141
  this.emit(EVENTS.PEER_BLOCKED, { peerId: id });
107
142
  }
108
143
 
144
+ /**
145
+ * @param {string} id
146
+ */
109
147
  unblockPeer(id) {
110
148
  this._peerManager?.unblockPeer(id);
111
149
  this.emit(EVENTS.PEER_UNBLOCKED, { peerId: id });
112
150
  }
113
151
 
114
152
  // Text messaging methods
153
+ /**
154
+ * @param {string} content
155
+ */
115
156
  sendBroadcast(content) {
116
157
  this._validateState([SERVICE_STATE.ACTIVE]);
117
158
  if (this._textManager) {
@@ -130,26 +171,34 @@ class MeshService extends EventEmitter {
130
171
  return messageId;
131
172
  }
132
173
 
174
+ /**
175
+ * @param {string} peerId
176
+ * @param {string} content
177
+ */
133
178
  async sendPrivateMessage(peerId, content) {
134
179
  this._validateState([SERVICE_STATE.ACTIVE]);
135
180
  if (this._textManager) {
136
181
  return this._textManager.sendPrivateMessage(peerId, content);
137
182
  }
138
- if (!this._sessionManager.hasSession(peerId)) { await this.initiateHandshake(peerId); }
183
+ if (!this._sessionManager?.hasSession(peerId)) { await this.initiateHandshake(peerId); }
139
184
  const messageId = this._generateMessageId();
140
185
  const plaintext = new TextEncoder().encode(content);
141
- const ciphertext = this._sessionManager.encryptFor(peerId, plaintext);
186
+ const ciphertext = this._sessionManager?.encryptFor(peerId, plaintext);
142
187
  await this._transport.send(peerId, ciphertext);
143
188
  this.emit(EVENTS.PRIVATE_MESSAGE_SENT, { messageId, peerId });
144
189
  return messageId;
145
190
  }
146
191
 
192
+ /**
193
+ * @param {string} channelId
194
+ * @param {string} content
195
+ */
147
196
  sendChannelMessage(channelId, content) {
148
197
  this._validateState([SERVICE_STATE.ACTIVE]);
149
198
  if (this._textManager) {
150
199
  return this._textManager.sendChannelMessage(channelId, content);
151
200
  }
152
- if (!this._channelManager.isInChannel(channelId)) {
201
+ if (!this._channelManager?.isInChannel(channelId)) {
153
202
  throw new MeshError('Not in channel', ERROR_CODE.E602);
154
203
  }
155
204
  const messageId = this._generateMessageId();
@@ -157,28 +206,38 @@ class MeshService extends EventEmitter {
157
206
  return messageId;
158
207
  }
159
208
 
209
+ /**
210
+ * @param {string} channelId
211
+ * @param {string} [password]
212
+ */
160
213
  joinChannel(channelId, password) {
161
214
  if (this._textManager) {
162
215
  return this._textManager.joinChannel(channelId, password);
163
216
  }
164
- this._channelManager.joinChannel(channelId, password);
217
+ this._channelManager?.joinChannel(channelId, password);
165
218
  }
166
219
 
220
+ /**
221
+ * @param {string} channelId
222
+ */
167
223
  leaveChannel(channelId) {
168
224
  if (this._textManager) {
169
225
  return this._textManager.leaveChannel(channelId);
170
226
  }
171
- this._channelManager.leaveChannel(channelId);
227
+ this._channelManager?.leaveChannel(channelId);
172
228
  }
173
229
 
174
230
  getChannels() {
175
231
  if (this._textManager) {
176
232
  return this._textManager.getChannels();
177
233
  }
178
- return this._channelManager.getChannels();
234
+ return this._channelManager?.getChannels() || [];
179
235
  }
180
236
 
181
237
  // Text manager methods
238
+ /**
239
+ * @param {any} [options]
240
+ */
182
241
  async initializeText(options = {}) {
183
242
  this._validateState([SERVICE_STATE.READY, SERVICE_STATE.ACTIVE]);
184
243
  if (this._textManager) {
@@ -192,6 +251,9 @@ class MeshService extends EventEmitter {
192
251
  getTextManager() { return this._textManager; }
193
252
 
194
253
  // Audio methods
254
+ /**
255
+ * @param {any} [options]
256
+ */
195
257
  async initializeAudio(options = {}) {
196
258
  this._validateState([SERVICE_STATE.READY, SERVICE_STATE.ACTIVE]);
197
259
  if (this._audioManager) {
@@ -204,6 +266,10 @@ class MeshService extends EventEmitter {
204
266
 
205
267
  getAudioManager() { return this._audioManager; }
206
268
 
269
+ /**
270
+ * @param {string} peerId
271
+ * @param {any} voiceMessage
272
+ */
207
273
  async sendVoiceMessage(peerId, voiceMessage) {
208
274
  this._validateState([SERVICE_STATE.ACTIVE]);
209
275
  if (!this._audioManager) {
@@ -212,6 +278,9 @@ class MeshService extends EventEmitter {
212
278
  return this._audioManager.sendVoiceMessage(peerId, voiceMessage);
213
279
  }
214
280
 
281
+ /**
282
+ * @param {string} peerId
283
+ */
215
284
  async requestAudioStream(peerId) {
216
285
  this._validateState([SERVICE_STATE.ACTIVE]);
217
286
  if (!this._audioManager) {
@@ -228,7 +297,7 @@ class MeshService extends EventEmitter {
228
297
  EVENTS.CHANNEL_JOINED, EVENTS.CHANNEL_LEFT, EVENTS.CHANNEL_MESSAGE,
229
298
  EVENTS.CHANNEL_MEMBER_JOINED, EVENTS.CHANNEL_MEMBER_LEFT
230
299
  ];
231
- textEvents.forEach(e => this._textManager.on(e, d => this.emit(e, d)));
300
+ textEvents.forEach((/** @type {string} */ e) => this._textManager.on(e, (/** @type {any} */ d) => this.emit(e, d)));
232
301
  }
233
302
 
234
303
  _setupAudioEventForwarding() {
@@ -237,9 +306,13 @@ class MeshService extends EventEmitter {
237
306
  EVENTS.AUDIO_STREAM_REQUEST, EVENTS.AUDIO_STREAM_STARTED, EVENTS.AUDIO_STREAM_ENDED,
238
307
  EVENTS.VOICE_MESSAGE_RECEIVED, EVENTS.VOICE_MESSAGE_SENT, EVENTS.VOICE_MESSAGE_PROGRESS
239
308
  ];
240
- audioEvents.forEach(e => this._audioManager.on(e, d => this.emit(e, d)));
309
+ audioEvents.forEach((/** @type {string} */ e) => this._audioManager.on(e, (/** @type {any} */ d) => this.emit(e, d)));
241
310
  }
242
311
 
312
+ /**
313
+ * @param {string} peerId
314
+ * @param {Uint8Array} data
315
+ */
243
316
  async _sendRaw(peerId, data) {
244
317
  if (this._state === SERVICE_STATE.DESTROYED || !this._transport) {
245
318
  return; // Silently ignore sends after destroy
@@ -251,8 +324,8 @@ class MeshService extends EventEmitter {
251
324
  return {
252
325
  state: this._state, identity: this.getIdentity(),
253
326
  peerCount: this.getConnectedPeers().length, securedPeerCount: this.getSecuredPeers().length,
254
- channelCount: this._channelManager?.getChannels().length || 0,
255
- sessionCount: this._sessionManager?.getAllSessionPeerIds().length || 0,
327
+ channelCount: this._channelManager?.getChannels()?.length || 0,
328
+ sessionCount: this._sessionManager?.getAllSessionPeerIds()?.length || 0,
256
329
  hasTextManager: !!this._textManager,
257
330
  hasAudioManager: !!this._audioManager
258
331
  };
@@ -260,12 +333,18 @@ class MeshService extends EventEmitter {
260
333
 
261
334
  getState() { return this._state; }
262
335
 
336
+ /**
337
+ * @param {string} newState
338
+ */
263
339
  _setState(newState) {
264
340
  const oldState = this._state;
265
341
  this._state = newState;
266
342
  this.emit(EVENTS.STATE_CHANGED, { oldState, newState });
267
343
  }
268
344
 
345
+ /**
346
+ * @param {string[]} allowed
347
+ */
269
348
  _validateState(allowed) {
270
349
  if (!allowed.includes(this._state)) {
271
350
  throw new MeshError(`Invalid state: ${this._state}`, ERROR_CODE.E003);
@@ -276,13 +355,15 @@ class MeshService extends EventEmitter {
276
355
 
277
356
  _createKeyManager() {
278
357
  const { createProvider } = require('../crypto/AutoCrypto');
358
+ /** @type {any} */
279
359
  let provider;
360
+ /** @type {any} */
280
361
  let keyPair;
281
362
 
282
363
  try {
283
364
  provider = createProvider('auto');
284
365
  keyPair = provider.generateKeyPair();
285
- } catch (e) {
366
+ } catch (/** @type {any} */ e) {
286
367
  // If no crypto provider is available, return a minimal fallback
287
368
  // that generates random keys using basic randomBytes
288
369
  const { randomBytes } = require('../utils/bytes');
@@ -291,7 +372,7 @@ class MeshService extends EventEmitter {
291
372
  getStaticKeyPair: () => keyPair,
292
373
  getPublicKey: () => keyPair.publicKey,
293
374
  exportIdentity: () => ({ publicKey: Array.from(keyPair.publicKey) }),
294
- importIdentity: (id) => {
375
+ importIdentity: (/** @type {any} */ id) => {
295
376
  if (id && id.publicKey) {
296
377
  keyPair.publicKey = new Uint8Array(id.publicKey);
297
378
  }
@@ -310,7 +391,7 @@ class MeshService extends EventEmitter {
310
391
  publicKey: Array.from(keyPair.publicKey),
311
392
  secretKey: Array.from(keyPair.secretKey)
312
393
  }),
313
- importIdentity: (id) => {
394
+ importIdentity: (/** @type {any} */ id) => {
314
395
  if (id && id.publicKey) {
315
396
  keyPair.publicKey = new Uint8Array(id.publicKey);
316
397
  }
@@ -322,7 +403,7 @@ class MeshService extends EventEmitter {
322
403
  }
323
404
 
324
405
  _setupEventForwarding() {
325
- const fwd = (em, evts) => evts.forEach(e => em.on(e, d => this.emit(e, d)));
406
+ const fwd = (/** @type {any} */ em, /** @type {string[]} */ evts) => evts.forEach((/** @type {string} */ e) => em.on(e, (/** @type {any} */ d) => this.emit(e, d)));
326
407
  fwd(this._handshakeManager, [EVENTS.HANDSHAKE_STARTED, EVENTS.HANDSHAKE_PROGRESS,
327
408
  EVENTS.HANDSHAKE_COMPLETE, EVENTS.HANDSHAKE_FAILED]);
328
409
  fwd(this._channelManager, [EVENTS.CHANNEL_JOINED, EVENTS.CHANNEL_LEFT,
@@ -330,15 +411,18 @@ class MeshService extends EventEmitter {
330
411
  }
331
412
 
332
413
  _setupTransportListeners() {
333
- this._transport.on('message', d => this._handleIncoming(d));
334
- this._transport.on('peerConnected', d => this.emit(EVENTS.PEER_CONNECTED, d));
335
- this._transport.on('peerDisconnected', d => this.emit(EVENTS.PEER_DISCONNECTED, d));
414
+ this._transport.on('message', (/** @type {any} */ d) => this._handleIncoming(d));
415
+ this._transport.on('peerConnected', (/** @type {any} */ d) => this.emit(EVENTS.PEER_CONNECTED, d));
416
+ this._transport.on('peerDisconnected', (/** @type {any} */ d) => this.emit(EVENTS.PEER_DISCONNECTED, d));
336
417
  }
337
418
 
419
+ /**
420
+ * @param {any} param0
421
+ */
338
422
  _handleIncoming({ peerId, data }) {
339
423
  const type = data[0], payload = data.subarray(1);
340
424
  if (type >= MESSAGE_TYPE.HANDSHAKE_INIT && type <= MESSAGE_TYPE.HANDSHAKE_FINAL) {
341
- this._handshakeManager.handleIncomingHandshake(peerId, type, payload, this._transport);
425
+ this._handshakeManager?.handleIncomingHandshake(peerId, type, payload, this._transport);
342
426
  } else if (type === MESSAGE_TYPE.CHANNEL_MESSAGE) {
343
427
  if (this._textManager) {
344
428
  this._textManager.handleIncomingMessage(peerId, type, payload);
@@ -351,7 +435,7 @@ class MeshService extends EventEmitter {
351
435
  const parsed = JSON.parse(decoded);
352
436
  channelId = parsed.channelId || '';
353
437
  content = parsed.content ? new TextEncoder().encode(parsed.content) : payload;
354
- } catch (e) {
438
+ } catch (/** @type {any} */ e) {
355
439
  // If not JSON, try to extract channelId as length-prefixed string
356
440
  if (payload.length > 1) {
357
441
  const channelIdLen = payload[0];
@@ -361,7 +445,7 @@ class MeshService extends EventEmitter {
361
445
  }
362
446
  }
363
447
  }
364
- this._channelManager.handleChannelMessage({ channelId, senderId: peerId, content });
448
+ this._channelManager?.handleChannelMessage({ channelId, senderId: peerId, content });
365
449
  }
366
450
  } else if (type >= MESSAGE_TYPE.VOICE_MESSAGE_START && type <= MESSAGE_TYPE.AUDIO_STREAM_END) {
367
451
  if (this._audioManager) {