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,7 +3,7 @@
3
3
  /**
4
4
  * @fileoverview Store and Forward Manager for offline peer message caching
5
5
  * @module mesh/store/StoreAndForwardManager
6
- *
6
+ *
7
7
  * Caches messages for offline recipients and delivers them upon reconnection.
8
8
  * Implements the BitChat protocol store-and-forward specification.
9
9
  */
@@ -17,16 +17,16 @@ const { ValidationError } = require('../../errors');
17
17
  * @constant {Object}
18
18
  */
19
19
  const DEFAULT_CONFIG = Object.freeze({
20
- /** Maximum number of cached messages per recipient */
21
- maxMessagesPerRecipient: 100,
22
- /** Maximum total cached messages */
23
- maxTotalMessages: 1000,
24
- /** Maximum cache size in bytes */
25
- maxCacheSizeBytes: 10 * 1024 * 1024, // 10MB
26
- /** Default retention period in ms (24 hours) */
27
- retentionMs: 24 * 60 * 60 * 1000,
28
- /** Cleanup interval in ms (5 minutes) */
29
- cleanupIntervalMs: 5 * 60 * 1000,
20
+ /** Maximum number of cached messages per recipient */
21
+ maxMessagesPerRecipient: 100,
22
+ /** Maximum total cached messages */
23
+ maxTotalMessages: 1000,
24
+ /** Maximum cache size in bytes */
25
+ maxCacheSizeBytes: 10 * 1024 * 1024, // 10MB
26
+ /** Default retention period in ms (24 hours) */
27
+ retentionMs: 24 * 60 * 60 * 1000,
28
+ /** Cleanup interval in ms (5 minutes) */
29
+ cleanupIntervalMs: 5 * 60 * 1000
30
30
  });
31
31
 
32
32
  /**
@@ -43,22 +43,22 @@ const DEFAULT_CONFIG = Object.freeze({
43
43
 
44
44
  /**
45
45
  * Store and Forward Manager for offline peer message delivery.
46
- *
46
+ *
47
47
  * @class StoreAndForwardManager
48
48
  * @extends EventEmitter
49
49
  * @example
50
50
  * const manager = new StoreAndForwardManager({
51
51
  * retentionMs: 12 * 60 * 60 * 1000, // 12 hours
52
52
  * });
53
- *
53
+ *
54
54
  * // Cache a message for offline peer
55
55
  * await manager.cacheForOfflinePeer(peerId, encryptedMessage);
56
- *
56
+ *
57
57
  * // When peer comes online
58
58
  * await manager.deliverCachedMessages(peerId, sendFn);
59
59
  */
60
60
  class StoreAndForwardManager extends EventEmitter {
61
- /**
61
+ /**
62
62
  * Creates a new StoreAndForwardManager instance.
63
63
  * @param {Object} [options={}] - Configuration options
64
64
  * @param {number} [options.maxMessagesPerRecipient=100] - Max messages per recipient
@@ -67,63 +67,63 @@ class StoreAndForwardManager extends EventEmitter {
67
67
  * @param {number} [options.retentionMs=86400000] - Message retention (24h)
68
68
  * @param {number} [options.cleanupIntervalMs=300000] - Cleanup interval (5min)
69
69
  */
70
- constructor(options = {}) {
71
- super();
70
+ constructor(options = {}) {
71
+ super();
72
72
 
73
- /**
73
+ /**
74
74
  * Configuration
75
75
  * @type {Object}
76
76
  * @private
77
77
  */
78
- this._config = { ...DEFAULT_CONFIG, ...options };
78
+ this._config = { ...DEFAULT_CONFIG, ...options };
79
79
 
80
- /**
80
+ /**
81
81
  * Message cache by recipient ID
82
82
  * @type {Map<string, CachedMessage[]>}
83
83
  * @private
84
84
  */
85
- this._cache = new Map();
85
+ this._cache = new Map();
86
86
 
87
- /**
87
+ /**
88
88
  * Total cached size in bytes
89
89
  * @type {number}
90
90
  * @private
91
91
  */
92
- this._totalSize = 0;
92
+ this._totalSize = 0;
93
93
 
94
- /**
94
+ /**
95
95
  * Total message count
96
96
  * @type {number}
97
97
  * @private
98
98
  */
99
- this._totalCount = 0;
99
+ this._totalCount = 0;
100
100
 
101
- /**
101
+ /**
102
102
  * Cleanup timer
103
103
  * @type {number|null}
104
104
  * @private
105
105
  */
106
- this._cleanupTimer = null;
106
+ this._cleanupTimer = null;
107
107
 
108
- /**
108
+ /**
109
109
  * Statistics
110
110
  * @type {Object}
111
111
  * @private
112
112
  */
113
- this._stats = {
114
- messagesCached: 0,
115
- messagesDelivered: 0,
116
- messagesExpired: 0,
117
- messagesDropped: 0,
118
- deliveryAttempts: 0,
119
- deliveryFailures: 0,
120
- };
121
-
122
- // Start periodic cleanup
123
- this._startCleanupTimer();
124
- }
125
-
126
- /**
113
+ this._stats = {
114
+ messagesCached: 0,
115
+ messagesDelivered: 0,
116
+ messagesExpired: 0,
117
+ messagesDropped: 0,
118
+ deliveryAttempts: 0,
119
+ deliveryFailures: 0
120
+ };
121
+
122
+ // Start periodic cleanup
123
+ this._startCleanupTimer();
124
+ }
125
+
126
+ /**
127
127
  * Caches a message for an offline peer.
128
128
  * @param {string} recipientId - Recipient peer ID
129
129
  * @param {Uint8Array} encryptedPayload - Encrypted message payload
@@ -132,345 +132,343 @@ class StoreAndForwardManager extends EventEmitter {
132
132
  * @param {number} [options.ttlMs] - Custom TTL in ms
133
133
  * @returns {Promise<string>} Cached message ID
134
134
  */
135
- async cacheForOfflinePeer(recipientId, encryptedPayload, options = {}) {
136
- if (!recipientId || typeof recipientId !== 'string') {
137
- throw ValidationError.invalidArgument('recipientId', recipientId, {
138
- reason: 'recipientId must be a non-empty string',
139
- });
140
- }
135
+ async cacheForOfflinePeer(recipientId, encryptedPayload, options = {}) {
136
+ if (!recipientId || typeof recipientId !== 'string') {
137
+ throw ValidationError.invalidArgument('recipientId', recipientId, {
138
+ reason: 'recipientId must be a non-empty string'
139
+ });
140
+ }
141
141
 
142
- if (!(encryptedPayload instanceof Uint8Array)) {
143
- throw ValidationError.invalidType('encryptedPayload', encryptedPayload, 'Uint8Array');
144
- }
142
+ if (!(encryptedPayload instanceof Uint8Array)) {
143
+ throw ValidationError.invalidType('encryptedPayload', encryptedPayload, 'Uint8Array');
144
+ }
145
145
 
146
- const messageId = options.messageId || this._generateId();
147
- const now = Date.now();
148
- const ttl = options.ttlMs || this._config.retentionMs;
149
-
150
- const cached = {
151
- id: messageId,
152
- recipientId,
153
- encryptedPayload,
154
- timestamp: now,
155
- expiresAt: now + ttl,
156
- attempts: 0,
157
- size: encryptedPayload.length,
158
- };
159
-
160
- // Check if we need to make room
161
- await this._ensureCapacity(cached.size);
162
-
163
- // Get or create recipient cache
164
- if (!this._cache.has(recipientId)) {
165
- this._cache.set(recipientId, []);
166
- }
146
+ const messageId = options.messageId || this._generateId();
147
+ const now = Date.now();
148
+ const ttl = options.ttlMs || this._config.retentionMs;
149
+
150
+ const cached = {
151
+ id: messageId,
152
+ recipientId,
153
+ encryptedPayload,
154
+ timestamp: now,
155
+ expiresAt: now + ttl,
156
+ attempts: 0,
157
+ size: encryptedPayload.length
158
+ };
159
+
160
+ // Check if we need to make room
161
+ await this._ensureCapacity(cached.size);
162
+
163
+ // Get or create recipient cache
164
+ if (!this._cache.has(recipientId)) {
165
+ this._cache.set(recipientId, []);
166
+ }
167
167
 
168
- const recipientCache = this._cache.get(recipientId);
169
-
170
- // Check per-recipient limit
171
- if (recipientCache.length >= this._config.maxMessagesPerRecipient) {
172
- // Remove oldest message for this recipient
173
- const oldest = recipientCache.shift();
174
- if (oldest) {
175
- this._totalSize -= oldest.size;
176
- this._totalCount--;
177
- this._stats.messagesDropped++;
178
- }
179
- }
168
+ const recipientCache = this._cache.get(recipientId);
169
+
170
+ // Check per-recipient limit
171
+ if (recipientCache.length >= this._config.maxMessagesPerRecipient) {
172
+ // Remove oldest message for this recipient
173
+ const oldest = recipientCache.shift();
174
+ if (oldest) {
175
+ this._totalSize -= oldest.size;
176
+ this._totalCount--;
177
+ this._stats.messagesDropped++;
178
+ }
179
+ }
180
180
 
181
- // Add to cache
182
- recipientCache.push(cached);
183
- this._totalSize += cached.size;
184
- this._totalCount++;
185
- this._stats.messagesCached++;
181
+ // Add to cache
182
+ recipientCache.push(cached);
183
+ this._totalSize += cached.size;
184
+ this._totalCount++;
185
+ this._stats.messagesCached++;
186
186
 
187
- this.emit(EVENTS.MESSAGE_CACHED || 'message-cached', {
188
- messageId,
189
- recipientId,
190
- expiresAt: cached.expiresAt,
191
- });
187
+ this.emit(EVENTS.MESSAGE_CACHED || 'message-cached', {
188
+ messageId,
189
+ recipientId,
190
+ expiresAt: cached.expiresAt
191
+ });
192
192
 
193
- return messageId;
194
- }
193
+ return messageId;
194
+ }
195
195
 
196
- /**
196
+ /**
197
197
  * Delivers all cached messages to a peer that came online.
198
198
  * @param {string} recipientId - Recipient peer ID
199
199
  * @param {Function} sendFn - Async function to send message: (payload) => Promise<void>
200
200
  * @returns {Promise<Object>} Delivery result with counts
201
201
  */
202
- async deliverCachedMessages(recipientId, sendFn) {
203
- if (!this._cache.has(recipientId)) {
204
- return { delivered: 0, failed: 0 };
205
- }
206
-
207
- const messages = this._cache.get(recipientId);
208
- const results = { delivered: 0, failed: 0, remaining: [] };
209
-
210
- for (const msg of messages) {
211
- // Skip expired messages
212
- if (Date.now() > msg.expiresAt) {
213
- this._totalSize -= msg.size;
214
- this._totalCount--;
215
- this._stats.messagesExpired++;
216
- continue;
217
- }
218
-
219
- msg.attempts++;
220
- this._stats.deliveryAttempts++;
221
-
222
- try {
223
- await sendFn(msg.encryptedPayload);
224
- results.delivered++;
225
- this._stats.messagesDelivered++;
226
- this._totalSize -= msg.size;
227
- this._totalCount--;
228
-
229
- this.emit(EVENTS.MESSAGE_DELIVERED || 'message-delivered', {
230
- messageId: msg.id,
231
- recipientId,
232
- });
233
- } catch (error) {
234
- results.failed++;
235
- this._stats.deliveryFailures++;
236
- results.remaining.push(msg);
237
-
238
- this.emit(EVENTS.MESSAGE_DELIVERY_FAILED || 'message-delivery-failed', {
239
- messageId: msg.id,
240
- recipientId,
241
- error: error.message,
242
- attempts: msg.attempts,
243
- });
244
- }
245
- }
202
+ async deliverCachedMessages(recipientId, sendFn) {
203
+ if (!this._cache.has(recipientId)) {
204
+ return { delivered: 0, failed: 0 };
205
+ }
246
206
 
247
- // Update cache with remaining messages
248
- if (results.remaining.length > 0) {
249
- this._cache.set(recipientId, results.remaining);
250
- } else {
251
- this._cache.delete(recipientId);
252
- }
207
+ const messages = this._cache.get(recipientId);
208
+ const results = { delivered: 0, failed: 0, remaining: [] };
209
+
210
+ for (const msg of messages) {
211
+ // Skip expired messages
212
+ if (Date.now() > msg.expiresAt) {
213
+ this._totalSize -= msg.size;
214
+ this._totalCount--;
215
+ this._stats.messagesExpired++;
216
+ continue;
217
+ }
218
+
219
+ msg.attempts++;
220
+ this._stats.deliveryAttempts++;
221
+
222
+ try {
223
+ await sendFn(msg.encryptedPayload);
224
+ results.delivered++;
225
+ this._stats.messagesDelivered++;
226
+ this._totalSize -= msg.size;
227
+ this._totalCount--;
228
+
229
+ this.emit(EVENTS.MESSAGE_DELIVERED || 'message-delivered', {
230
+ messageId: msg.id,
231
+ recipientId
232
+ });
233
+ } catch (error) {
234
+ results.failed++;
235
+ this._stats.deliveryFailures++;
236
+ results.remaining.push(msg);
237
+
238
+ this.emit(EVENTS.MESSAGE_DELIVERY_FAILED || 'message-delivery-failed', {
239
+ messageId: msg.id,
240
+ recipientId,
241
+ error: error.message,
242
+ attempts: msg.attempts
243
+ });
244
+ }
245
+ }
253
246
 
254
- return {
255
- delivered: results.delivered,
256
- failed: results.failed,
257
- };
247
+ // Update cache with remaining messages
248
+ if (results.remaining.length > 0) {
249
+ this._cache.set(recipientId, results.remaining);
250
+ } else {
251
+ this._cache.delete(recipientId);
258
252
  }
259
253
 
260
- /**
254
+ return {
255
+ delivered: results.delivered,
256
+ failed: results.failed
257
+ };
258
+ }
259
+
260
+ /**
261
261
  * Checks if there are cached messages for a peer.
262
262
  * @param {string} recipientId - Recipient peer ID
263
263
  * @returns {boolean} True if cached messages exist
264
264
  */
265
- hasCachedMessages(recipientId) {
266
- const cache = this._cache.get(recipientId);
267
- return !!(cache && cache.length > 0);
268
- }
265
+ hasCachedMessages(recipientId) {
266
+ const cache = this._cache.get(recipientId);
267
+ return !!(cache && cache.length > 0);
268
+ }
269
269
 
270
- /**
270
+ /**
271
271
  * Gets count of cached messages for a peer.
272
272
  * @param {string} recipientId - Recipient peer ID
273
273
  * @returns {number} Message count
274
274
  */
275
- getCachedCount(recipientId) {
276
- const cache = this._cache.get(recipientId);
277
- return cache ? cache.length : 0;
278
- }
275
+ getCachedCount(recipientId) {
276
+ const cache = this._cache.get(recipientId);
277
+ return cache ? cache.length : 0;
278
+ }
279
279
 
280
- /**
280
+ /**
281
281
  * Gets all recipient IDs with cached messages.
282
282
  * @returns {string[]} Array of recipient IDs
283
283
  */
284
- getRecipientsWithCache() {
285
- return Array.from(this._cache.keys());
286
- }
284
+ getRecipientsWithCache() {
285
+ return Array.from(this._cache.keys());
286
+ }
287
287
 
288
- /**
288
+ /**
289
289
  * Removes all cached messages for a specific recipient.
290
290
  * @param {string} recipientId - Recipient peer ID
291
291
  * @returns {number} Number of messages removed
292
292
  */
293
- clearRecipientCache(recipientId) {
294
- const cache = this._cache.get(recipientId);
295
- if (!cache) {
296
- return 0;
297
- }
293
+ clearRecipientCache(recipientId) {
294
+ const cache = this._cache.get(recipientId);
295
+ if (!cache) {
296
+ return 0;
297
+ }
298
298
 
299
- const count = cache.length;
300
- const size = cache.reduce((sum, m) => sum + m.size, 0);
299
+ const count = cache.length;
300
+ const size = cache.reduce((sum, m) => sum + m.size, 0);
301
301
 
302
- this._cache.delete(recipientId);
303
- this._totalSize -= size;
304
- this._totalCount -= count;
302
+ this._cache.delete(recipientId);
303
+ this._totalSize -= size;
304
+ this._totalCount -= count;
305
305
 
306
- return count;
307
- }
306
+ return count;
307
+ }
308
308
 
309
- /**
309
+ /**
310
310
  * Prunes expired messages from all caches.
311
311
  * @returns {Promise<number>} Number of messages pruned
312
312
  */
313
- async pruneExpiredMessages() {
314
- const now = Date.now();
315
- let pruned = 0;
316
-
317
- for (const [recipientId, messages] of this._cache) {
318
- const validMessages = messages.filter(msg => {
319
- if (msg.expiresAt <= now) {
320
- this._totalSize -= msg.size;
321
- this._totalCount--;
322
- this._stats.messagesExpired++;
323
- pruned++;
324
- return false;
325
- }
326
- return true;
327
- });
328
-
329
- if (validMessages.length > 0) {
330
- this._cache.set(recipientId, validMessages);
331
- } else {
332
- this._cache.delete(recipientId);
333
- }
313
+ async pruneExpiredMessages() {
314
+ const now = Date.now();
315
+ let pruned = 0;
316
+
317
+ for (const [recipientId, messages] of this._cache) {
318
+ const validMessages = messages.filter(msg => {
319
+ if (msg.expiresAt <= now) {
320
+ this._totalSize -= msg.size;
321
+ this._totalCount--;
322
+ this._stats.messagesExpired++;
323
+ pruned++;
324
+ return false;
334
325
  }
326
+ return true;
327
+ });
335
328
 
336
- if (pruned > 0) {
337
- this.emit('messages-pruned', { count: pruned });
338
- }
329
+ if (validMessages.length > 0) {
330
+ this._cache.set(recipientId, validMessages);
331
+ } else {
332
+ this._cache.delete(recipientId);
333
+ }
334
+ }
339
335
 
340
- return pruned;
336
+ if (pruned > 0) {
337
+ this.emit('messages-pruned', { count: pruned });
341
338
  }
342
339
 
343
- /**
340
+ return pruned;
341
+ }
342
+
343
+ /**
344
344
  * Gets store and forward statistics.
345
345
  * @returns {Object} Statistics
346
346
  */
347
- getStats() {
348
- return {
349
- ...this._stats,
350
- totalCached: this._totalCount,
351
- totalSizeBytes: this._totalSize,
352
- recipientCount: this._cache.size,
353
- cacheUtilization: this._totalSize / this._config.maxCacheSizeBytes,
354
- };
355
- }
356
-
357
- /**
347
+ getStats() {
348
+ return {
349
+ ...this._stats,
350
+ totalCached: this._totalCount,
351
+ totalSizeBytes: this._totalSize,
352
+ recipientCount: this._cache.size,
353
+ cacheUtilization: this._totalSize / this._config.maxCacheSizeBytes
354
+ };
355
+ }
356
+
357
+ /**
358
358
  * Resets statistics.
359
359
  */
360
- resetStats() {
361
- this._stats = {
362
- messagesCached: 0,
363
- messagesDelivered: 0,
364
- messagesExpired: 0,
365
- messagesDropped: 0,
366
- deliveryAttempts: 0,
367
- deliveryFailures: 0,
368
- };
369
- }
370
-
371
- /**
360
+ resetStats() {
361
+ this._stats = {
362
+ messagesCached: 0,
363
+ messagesDelivered: 0,
364
+ messagesExpired: 0,
365
+ messagesDropped: 0,
366
+ deliveryAttempts: 0,
367
+ deliveryFailures: 0
368
+ };
369
+ }
370
+
371
+ /**
372
372
  * Clears all cached messages.
373
373
  */
374
- clear() {
375
- this._cache.clear();
376
- this._totalSize = 0;
377
- this._totalCount = 0;
378
- }
374
+ clear() {
375
+ this._cache.clear();
376
+ this._totalSize = 0;
377
+ this._totalCount = 0;
378
+ }
379
379
 
380
- /**
380
+ /**
381
381
  * Destroys the manager and cleans up resources.
382
382
  */
383
- destroy() {
384
- this._stopCleanupTimer();
385
- this.clear();
386
- this.removeAllListeners();
387
- }
383
+ destroy() {
384
+ this._stopCleanupTimer();
385
+ this.clear();
386
+ this.removeAllListeners();
387
+ }
388
388
 
389
- /**
389
+ /**
390
390
  * Ensures capacity for new message.
391
391
  * @param {number} requiredSize - Size needed
392
392
  * @private
393
393
  */
394
- async _ensureCapacity(requiredSize) {
395
- // Check size limit
396
- while (this._totalSize + requiredSize > this._config.maxCacheSizeBytes) {
397
- if (this._totalCount === 0) break;
398
- this._removeOldestMessage();
399
- }
394
+ async _ensureCapacity(requiredSize) {
395
+ // Check size limit
396
+ while (this._totalSize + requiredSize > this._config.maxCacheSizeBytes) {
397
+ if (this._totalCount === 0) { break; }
398
+ this._removeOldestMessage();
399
+ }
400
400
 
401
- // Check count limit
402
- while (this._totalCount >= this._config.maxTotalMessages) {
403
- this._removeOldestMessage();
404
- }
401
+ // Check count limit
402
+ while (this._totalCount >= this._config.maxTotalMessages) {
403
+ this._removeOldestMessage();
405
404
  }
405
+ }
406
406
 
407
- /**
407
+ /**
408
408
  * Removes the oldest message from cache.
409
409
  * @private
410
410
  */
411
- _removeOldestMessage() {
412
- let oldestTime = Infinity;
413
- let oldestRecipient = null;
414
-
415
- for (const [recipientId, messages] of this._cache) {
416
- if (messages.length > 0 && messages[0].timestamp < oldestTime) {
417
- oldestTime = messages[0].timestamp;
418
- oldestRecipient = recipientId;
419
- }
420
- }
411
+ _removeOldestMessage() {
412
+ let oldestTime = Infinity;
413
+ let oldestRecipient = null;
414
+
415
+ for (const [recipientId, messages] of this._cache) {
416
+ if (messages.length > 0 && messages[0].timestamp < oldestTime) {
417
+ oldestTime = messages[0].timestamp;
418
+ oldestRecipient = recipientId;
419
+ }
420
+ }
421
421
 
422
- if (oldestRecipient) {
423
- const messages = this._cache.get(oldestRecipient);
424
- const oldest = messages.shift();
425
- if (oldest) {
426
- this._totalSize -= oldest.size;
427
- this._totalCount--;
428
- this._stats.messagesDropped++;
429
- }
430
- if (messages.length === 0) {
431
- this._cache.delete(oldestRecipient);
432
- }
433
- }
422
+ if (oldestRecipient) {
423
+ const messages = this._cache.get(oldestRecipient);
424
+ const oldest = messages.shift();
425
+ if (oldest) {
426
+ this._totalSize -= oldest.size;
427
+ this._totalCount--;
428
+ this._stats.messagesDropped++;
429
+ }
430
+ if (messages.length === 0) {
431
+ this._cache.delete(oldestRecipient);
432
+ }
434
433
  }
434
+ }
435
435
 
436
- /**
436
+ /**
437
437
  * Generates a unique message ID.
438
438
  * @returns {string} Message ID
439
439
  * @private
440
440
  */
441
- _generateId() {
442
- const bytes = new Uint8Array(16);
443
- if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
444
- crypto.getRandomValues(bytes);
445
- } else {
446
- for (let i = 0; i < 16; i++) {
447
- bytes[i] = Math.floor(Math.random() * 256);
448
- }
449
- }
450
- return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
451
- }
441
+ _generateId() {
442
+ const { randomBytes } = require('../../utils/bytes');
443
+ const bytes = randomBytes(16);
444
+ return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
445
+ }
452
446
 
453
- /**
447
+ /**
454
448
  * Starts the cleanup timer.
455
449
  * @private
456
450
  */
457
- _startCleanupTimer() {
458
- this._cleanupTimer = setInterval(
459
- () => this.pruneExpiredMessages(),
460
- this._config.cleanupIntervalMs
461
- );
451
+ _startCleanupTimer() {
452
+ this._cleanupTimer = setInterval(
453
+ () => this.pruneExpiredMessages(),
454
+ this._config.cleanupIntervalMs
455
+ );
456
+ // Allow Node.js process to exit even if timer is active (important for tests)
457
+ if (this._cleanupTimer && typeof this._cleanupTimer.unref === 'function') {
458
+ this._cleanupTimer.unref();
462
459
  }
460
+ }
463
461
 
464
- /**
462
+ /**
465
463
  * Stops the cleanup timer.
466
464
  * @private
467
465
  */
468
- _stopCleanupTimer() {
469
- if (this._cleanupTimer) {
470
- clearInterval(this._cleanupTimer);
471
- this._cleanupTimer = null;
472
- }
466
+ _stopCleanupTimer() {
467
+ if (this._cleanupTimer) {
468
+ clearInterval(this._cleanupTimer);
469
+ this._cleanupTimer = null;
473
470
  }
471
+ }
474
472
  }
475
473
 
476
474
  module.exports = StoreAndForwardManager;