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.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +70 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +659 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +79 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +125 -0
- package/src/crypto/providers/QuickCryptoProvider.js +134 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/useMesh.js +30 -9
- package/src/hooks/useMessages.js +2 -0
- package/src/index.js +23 -8
- package/src/mesh/dedup/DedupManager.js +36 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +408 -0
- package/src/mesh/monitor/NetworkMonitor.js +327 -316
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/PeerManager.js +6 -1
- package/src/mesh/router/MessageRouter.js +26 -15
- package/src/mesh/router/RouteTable.js +7 -1
- package/src/mesh/store/StoreAndForwardManager.js +295 -297
- package/src/mesh/store/index.js +1 -1
- package/src/service/BatteryOptimizer.js +282 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +167 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +77 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/transport/BLETransport.js +131 -9
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +305 -0
- package/src/transport/WiFiDirectTransport.js +295 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/compression.js +291 -291
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
71
|
-
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
super();
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
74
|
* Configuration
|
|
75
75
|
* @type {Object}
|
|
76
76
|
* @private
|
|
77
77
|
*/
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
this._totalSize = 0;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
95
|
* Total message count
|
|
96
96
|
* @type {number}
|
|
97
97
|
* @private
|
|
98
98
|
*/
|
|
99
|
-
|
|
99
|
+
this._totalCount = 0;
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
/**
|
|
102
102
|
* Cleanup timer
|
|
103
103
|
* @type {number|null}
|
|
104
104
|
* @private
|
|
105
105
|
*/
|
|
106
|
-
|
|
106
|
+
this._cleanupTimer = null;
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
109
|
* Statistics
|
|
110
110
|
* @type {Object}
|
|
111
111
|
* @private
|
|
112
112
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
if (!(encryptedPayload instanceof Uint8Array)) {
|
|
143
|
+
throw ValidationError.invalidType('encryptedPayload', encryptedPayload, 'Uint8Array');
|
|
144
|
+
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
// Add to cache
|
|
182
|
+
recipientCache.push(cached);
|
|
183
|
+
this._totalSize += cached.size;
|
|
184
|
+
this._totalCount++;
|
|
185
|
+
this._stats.messagesCached++;
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
this.emit(EVENTS.MESSAGE_CACHED || 'message-cached', {
|
|
188
|
+
messageId,
|
|
189
|
+
recipientId,
|
|
190
|
+
expiresAt: cached.expiresAt
|
|
191
|
+
});
|
|
192
192
|
|
|
193
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
clearRecipientCache(recipientId) {
|
|
294
|
+
const cache = this._cache.get(recipientId);
|
|
295
|
+
if (!cache) {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
const count = cache.length;
|
|
300
|
+
const size = cache.reduce((sum, m) => sum + m.size, 0);
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
this._cache.delete(recipientId);
|
|
303
|
+
this._totalSize -= size;
|
|
304
|
+
this._totalCount -= count;
|
|
305
305
|
|
|
306
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
if (validMessages.length > 0) {
|
|
330
|
+
this._cache.set(recipientId, validMessages);
|
|
331
|
+
} else {
|
|
332
|
+
this._cache.delete(recipientId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
339
335
|
|
|
340
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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;
|