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
@@ -20,7 +20,8 @@ class MeshError extends Error {
20
20
  * @param {Object|null} [details=null] - Additional error context
21
21
  */
22
22
  constructor(message, code = 'E900', details = null) {
23
- super(message);
23
+ const className = new.target ? new.target.name : 'MeshError';
24
+ super(`${className}: ${message}`);
24
25
 
25
26
  /**
26
27
  * Error name
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Expo config plugin for react-native-ble-mesh
5
+ * @module expo/withBLEMesh
6
+ *
7
+ * Automatically configures BLE permissions and background modes
8
+ * for iOS and Android when used with Expo.
9
+ *
10
+ * Usage in app.json:
11
+ * {
12
+ * "expo": {
13
+ * "plugins": [
14
+ * ["react-native-ble-mesh", {
15
+ * "bluetoothAlwaysPermission": "Chat with nearby devices via Bluetooth",
16
+ * "backgroundModes": ["bluetooth-central", "bluetooth-peripheral"]
17
+ * }]
18
+ * ]
19
+ * }
20
+ * }
21
+ */
22
+
23
+ /**
24
+ * Default configuration
25
+ * @constant {Object}
26
+ */
27
+ const DEFAULT_OPTIONS = {
28
+ /** iOS NSBluetoothAlwaysUsageDescription */
29
+ bluetoothAlwaysPermission: 'This app uses Bluetooth to communicate with nearby devices',
30
+ /** iOS UIBackgroundModes */
31
+ backgroundModes: ['bluetooth-central', 'bluetooth-peripheral'],
32
+ /** Android permissions */
33
+ androidPermissions: [
34
+ 'android.permission.BLUETOOTH_SCAN',
35
+ 'android.permission.BLUETOOTH_CONNECT',
36
+ 'android.permission.BLUETOOTH_ADVERTISE',
37
+ 'android.permission.ACCESS_FINE_LOCATION'
38
+ ]
39
+ };
40
+
41
+ /**
42
+ * Modifies the iOS Info.plist for BLE permissions.
43
+ * @param {Object} config - Expo config
44
+ * @param {Object} options - Plugin options
45
+ * @returns {Object} Modified config
46
+ */
47
+ function withBLEMeshIOS(config, options) {
48
+ const opts = { ...DEFAULT_OPTIONS, ...options };
49
+
50
+ if (!config.ios) { config.ios = {}; }
51
+ if (!config.ios.infoPlist) { config.ios.infoPlist = {}; }
52
+
53
+ // Add Bluetooth usage description
54
+ config.ios.infoPlist.NSBluetoothAlwaysUsageDescription = opts.bluetoothAlwaysPermission;
55
+ config.ios.infoPlist.NSBluetoothPeripheralUsageDescription = opts.bluetoothAlwaysPermission;
56
+
57
+ // Add background modes
58
+ const existingModes = config.ios.infoPlist.UIBackgroundModes || [];
59
+ const newModes = new Set([...existingModes, ...opts.backgroundModes]);
60
+ config.ios.infoPlist.UIBackgroundModes = Array.from(newModes);
61
+
62
+ return config;
63
+ }
64
+
65
+ /**
66
+ * Modifies the Android manifest for BLE permissions.
67
+ * @param {Object} config - Expo config
68
+ * @param {Object} options - Plugin options
69
+ * @returns {Object} Modified config
70
+ */
71
+ function withBLEMeshAndroid(config, options) {
72
+ const opts = { ...DEFAULT_OPTIONS, ...options };
73
+
74
+ if (!config.android) { config.android = {}; }
75
+ if (!config.android.permissions) { config.android.permissions = []; }
76
+
77
+ // Add BLE permissions (avoid duplicates)
78
+ const existingPerms = new Set(config.android.permissions);
79
+ for (const perm of opts.androidPermissions) {
80
+ existingPerms.add(perm);
81
+ }
82
+ config.android.permissions = Array.from(existingPerms);
83
+
84
+ return config;
85
+ }
86
+
87
+ /**
88
+ * Main Expo config plugin.
89
+ * @param {Object} config - Expo config
90
+ * @param {Object} [options={}] - Plugin options
91
+ * @returns {Object} Modified config
92
+ */
93
+ function withBLEMesh(config, options = {}) {
94
+ config = withBLEMeshIOS(config, options);
95
+ config = withBLEMeshAndroid(config, options);
96
+ return config;
97
+ }
98
+
99
+ module.exports = withBLEMesh;
100
+ module.exports.withBLEMeshIOS = withBLEMeshIOS;
101
+ module.exports.withBLEMeshAndroid = withBLEMeshAndroid;
102
+ module.exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
@@ -45,6 +45,8 @@ class AppStateManager {
45
45
  this._initialized = false;
46
46
  /** @private */
47
47
  this._AppState = null;
48
+ /** @private */
49
+ this._boundHandleStateChange = this._handleStateChange.bind(this);
48
50
  }
49
51
 
50
52
  /**
@@ -64,9 +66,15 @@ class AppStateManager {
64
66
  return false;
65
67
  }
66
68
 
69
+ // Remove old subscription before creating new one
70
+ if (this._subscription) {
71
+ this._subscription.remove();
72
+ this._subscription = null;
73
+ }
74
+
67
75
  this._subscription = this._AppState.addEventListener(
68
76
  'change',
69
- this._handleStateChange.bind(this)
77
+ this._boundHandleStateChange
70
78
  );
71
79
 
72
80
  this._initialized = true;
@@ -46,6 +46,9 @@ function useMesh(config = {}) {
46
46
 
47
47
  // Create mesh instance ref (persists across renders)
48
48
  const meshRef = useRef(null);
49
+ const mountedRef = useRef(true);
50
+ const stateHandlerRef = useRef(null);
51
+ const errorHandlerRef = useRef(null);
49
52
 
50
53
  // State
51
54
  const [state, setState] = useState('uninitialized');
@@ -64,19 +67,35 @@ function useMesh(config = {}) {
64
67
  // Initialize mesh
65
68
  const initialize = useCallback(async (transport) => {
66
69
  try {
70
+ if (!mountedRef.current) { return; }
67
71
  setState('initializing');
68
72
  setError(null);
69
73
 
70
74
  const mesh = getMesh();
71
75
 
76
+ // Remove old listeners if they exist (prevents accumulation on re-init)
77
+ if (stateHandlerRef.current) {
78
+ mesh.off('state-changed', stateHandlerRef.current);
79
+ }
80
+ if (errorHandlerRef.current) {
81
+ mesh.off('error', errorHandlerRef.current);
82
+ }
83
+
72
84
  // Setup state change listener
73
- mesh.on('state-changed', ({ newState }) => {
74
- setState(newState);
75
- });
85
+ stateHandlerRef.current = ({ newState }) => {
86
+ if (mountedRef.current) {
87
+ setState(newState);
88
+ }
89
+ };
76
90
 
77
- mesh.on('error', (err) => {
78
- setError(err);
79
- });
91
+ errorHandlerRef.current = (err) => {
92
+ if (mountedRef.current) {
93
+ setError(err);
94
+ }
95
+ };
96
+
97
+ mesh.on('state-changed', stateHandlerRef.current);
98
+ mesh.on('error', errorHandlerRef.current);
80
99
 
81
100
  // Initialize with storage
82
101
  await mesh.initialize({
@@ -90,8 +109,10 @@ function useMesh(config = {}) {
90
109
 
91
110
  return mesh;
92
111
  } catch (err) {
93
- setState('error');
94
- setError(err);
112
+ if (mountedRef.current) {
113
+ setState('error');
114
+ setError(err);
115
+ }
95
116
  throw err;
96
117
  }
97
118
  }, [getMesh, config.storage]);
@@ -102,7 +123,9 @@ function useMesh(config = {}) {
102
123
  try {
103
124
  await mesh.start(transport);
104
125
  } catch (err) {
105
- setError(err);
126
+ if (mountedRef.current) {
127
+ setError(err);
128
+ }
106
129
  throw err;
107
130
  }
108
131
  }, [getMesh]);
@@ -114,7 +137,9 @@ function useMesh(config = {}) {
114
137
  try {
115
138
  await mesh.stop();
116
139
  } catch (err) {
117
- setError(err);
140
+ if (mountedRef.current) {
141
+ setError(err);
142
+ }
118
143
  }
119
144
  }
120
145
  }, []);
@@ -129,16 +154,25 @@ function useMesh(config = {}) {
129
154
  // Ignore destroy errors
130
155
  }
131
156
  meshRef.current = null;
132
- setState('destroyed');
157
+ if (mountedRef.current) {
158
+ setState('destroyed');
159
+ }
133
160
  }
134
161
  }, []);
135
162
 
136
163
  // Cleanup on unmount
137
164
  useEffect(() => {
138
165
  return () => {
166
+ // Immediately mark as unmounted to prevent state updates
167
+ mountedRef.current = false;
168
+
139
169
  if (meshRef.current) {
140
- meshRef.current.destroy().catch(() => {});
141
- meshRef.current = null;
170
+ const mesh = meshRef.current;
171
+ meshRef.current = null; // Null ref immediately to prevent new operations
172
+
173
+ mesh.destroy().catch(() => {
174
+ // Ignore cleanup errors
175
+ });
142
176
  }
143
177
  };
144
178
  }, []);
@@ -58,11 +58,11 @@ function useMessages(mesh, options = {}) {
58
58
 
59
59
  setMessages(prev => {
60
60
  const updated = [msg, ...prev];
61
- // Trim to max messages
62
61
  if (updated.length > maxMessages) {
63
- const removed = updated.slice(maxMessages);
64
- removed.forEach(m => messageIdRef.current.delete(m.id));
65
- return updated.slice(0, maxMessages);
62
+ for (let i = maxMessages; i < updated.length; i++) {
63
+ messageIdRef.current.delete(updated[i].id);
64
+ }
65
+ updated.length = maxMessages;
66
66
  }
67
67
  return updated;
68
68
  });
@@ -114,6 +114,8 @@ function useMessages(mesh, options = {}) {
114
114
  mesh.off('broadcast-received', handleBroadcast);
115
115
  mesh.off('private-message-received', handlePrivate);
116
116
  mesh.off('channel-message-received', handleChannel);
117
+ // Clear dedup Set on unmount
118
+ messageIdRef.current.clear();
117
119
  };
118
120
  }, [mesh, addMessage]);
119
121
 
@@ -35,18 +35,23 @@ function usePeers(mesh) {
35
35
  throw new Error('usePeers requires React. Install react as a dependency.');
36
36
  }
37
37
 
38
- const { useState, useEffect, useCallback, useMemo } = React;
38
+ const { useState, useEffect, useCallback, useMemo, useRef } = React;
39
39
 
40
40
  const [peers, setPeers] = useState([]);
41
- const [lastUpdate, setLastUpdate] = useState(Date.now());
41
+ const lastUpdateRef = useRef(Date.now());
42
42
 
43
43
  // Update peers from mesh
44
44
  const refreshPeers = useCallback(() => {
45
45
  if (mesh) {
46
46
  try {
47
47
  const allPeers = mesh.getPeers();
48
- setPeers(allPeers);
49
- setLastUpdate(Date.now());
48
+ setPeers(prev => {
49
+ if (prev.length === allPeers.length && prev.every((p, i) => p.id === allPeers[i]?.id && p.connectionState === allPeers[i]?.connectionState)) {
50
+ return prev;
51
+ }
52
+ return allPeers;
53
+ });
54
+ lastUpdateRef.current = Date.now();
50
55
  } catch (e) {
51
56
  // Mesh might not be ready
52
57
  }
@@ -84,10 +89,9 @@ function usePeers(mesh) {
84
89
  return peers.filter(p => p.connectionState === 'secured');
85
90
  }, [peers]);
86
91
 
87
- // Get single peer by ID
88
- const getPeer = useCallback((peerId) => {
89
- return peers.find(p => p.id === peerId);
90
- }, [peers]);
92
+ // Get single peer by ID (O(1) lookup via peerMap)
93
+ const peerMap = useMemo(() => new Map(peers.map(p => [p.id, p])), [peers]);
94
+ const getPeer = useCallback((peerId) => peerMap.get(peerId), [peerMap]);
91
95
 
92
96
  // Check if peer is connected
93
97
  const isConnected = useCallback((peerId) => {
@@ -104,7 +108,7 @@ function usePeers(mesh) {
104
108
  getPeer,
105
109
  isConnected,
106
110
  refresh: refreshPeers,
107
- lastUpdate
111
+ lastUpdate: lastUpdateRef.current
108
112
  };
109
113
  }
110
114
 
package/src/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  * @fileoverview BLE Mesh Network Library
3
3
  * @module rn-ble-mesh
4
4
  * @description Production-ready BLE Mesh Network with Noise Protocol security
5
- *
6
- * This is the definitive React Native library for BitChat-compatible
5
+ *
6
+ * This is the definitive React Native library for BitChat-compatible
7
7
  * decentralized mesh networking.
8
8
  */
9
9
 
@@ -16,9 +16,7 @@ const { MeshNetwork, BATTERY_MODE, PANIC_TRIGGER, HEALTH_STATUS } = require('./M
16
16
  const {
17
17
  MeshService,
18
18
  EmergencyManager,
19
- BatteryOptimizer,
20
- SessionManager,
21
- HandshakeManager,
19
+ BatteryOptimizer
22
20
  } = require('./service');
23
21
 
24
22
  // Constants
@@ -27,7 +25,7 @@ const constants = require('./constants');
27
25
  // Errors
28
26
  const errors = require('./errors');
29
27
 
30
- // Crypto
28
+ // Crypto — pluggable provider system (tweetnacl / quick-crypto / expo-crypto)
31
29
  const crypto = require('./crypto');
32
30
 
33
31
  // Protocol
@@ -51,6 +49,10 @@ const audio = require('./service/audio');
51
49
  // Text (from service module)
52
50
  const text = require('./service/text');
53
51
 
52
+ // File transfer
53
+ const file = require('./service/file');
54
+ const { FileManager, FileChunker, FileAssembler, FileMessage, FILE_MESSAGE_TYPE, FILE_TRANSFER_STATE } = file;
55
+
54
56
  // React Native hooks
55
57
  const hooks = require('./hooks');
56
58
 
@@ -133,7 +135,7 @@ const { useMesh, usePeers, useMessages, AppStateManager } = hooks;
133
135
 
134
136
  // New PRD-specified components
135
137
  const { StoreAndForwardManager } = mesh;
136
- const { NetworkMonitor } = mesh;
138
+ const { NetworkMonitor, ConnectionQuality, QUALITY_LEVEL } = mesh;
137
139
  const { MessageCompressor, compress, decompress } = utils;
138
140
 
139
141
  module.exports = {
@@ -156,6 +158,10 @@ module.exports = {
156
158
  // Store and Forward
157
159
  StoreAndForwardManager,
158
160
 
161
+ // Connection Quality
162
+ ConnectionQuality,
163
+ QUALITY_LEVEL,
164
+
159
165
  // Compression
160
166
  MessageCompressor,
161
167
  compress,
@@ -186,7 +192,7 @@ module.exports = {
186
192
  // Errors
187
193
  ...errors,
188
194
 
189
- // Crypto primitives and Noise Protocol
195
+ // Crypto provider system
190
196
  crypto,
191
197
 
192
198
  // Protocol serialization
@@ -229,6 +235,15 @@ module.exports = {
229
235
  useMessages,
230
236
  AppStateManager,
231
237
 
238
+ // File transfer
239
+ FileManager,
240
+ FileChunker,
241
+ FileAssembler,
242
+ FileMessage,
243
+ FILE_MESSAGE_TYPE,
244
+ FILE_TRANSFER_STATE,
245
+ file,
246
+
232
247
  // Hooks module
233
248
  hooks
234
249
  };
@@ -13,6 +13,15 @@
13
13
  const FNV_PRIME = 0x01000193;
14
14
  const FNV_OFFSET = 0x811c9dc5;
15
15
 
16
+ // Cached TextEncoder singleton (avoids per-call allocation)
17
+ let _encoder = null;
18
+ function _getEncoder() {
19
+ if (!_encoder && typeof TextEncoder !== 'undefined') {
20
+ _encoder = new TextEncoder();
21
+ }
22
+ return _encoder;
23
+ }
24
+
16
25
  /**
17
26
  * Bloom filter for probabilistic set membership testing
18
27
  * @class BloomFilter
@@ -56,6 +65,13 @@ class BloomFilter {
56
65
  * @private
57
66
  */
58
67
  this._count = 0;
68
+
69
+ /**
70
+ * Running count of set bits (avoids O(n) scan in getFillRatio)
71
+ * @type {number}
72
+ * @private
73
+ */
74
+ this._setBitCount = 0;
59
75
  }
60
76
 
61
77
  /**
@@ -90,72 +106,50 @@ class BloomFilter {
90
106
  return item;
91
107
  }
92
108
  if (typeof item === 'string') {
93
- const encoder = new TextEncoder();
94
- return encoder.encode(item);
109
+ const encoder = _getEncoder();
110
+ if (encoder) {
111
+ return encoder.encode(item);
112
+ }
113
+ // Fallback for environments without TextEncoder
114
+ const bytes = new Uint8Array(item.length);
115
+ for (let i = 0; i < item.length; i++) {
116
+ bytes[i] = item.charCodeAt(i) & 0xff;
117
+ }
118
+ return bytes;
95
119
  }
96
120
  throw new Error('Item must be string or Uint8Array');
97
121
  }
98
122
 
99
- /**
100
- * Computes hash positions for an item
101
- * @param {string|Uint8Array} item - Item to hash
102
- * @returns {number[]} Array of bit positions
103
- * @private
104
- */
105
- _getPositions(item) {
106
- const data = this._toBytes(item);
107
- const positions = [];
108
- for (let i = 0; i < this.hashCount; i++) {
109
- const hash = this._fnv1a(data, i);
110
- positions.push(hash % this.size);
111
- }
112
- return positions;
113
- }
114
-
115
- /**
116
- * Sets a bit at the given position
117
- * @param {number} position - Bit position
118
- * @private
119
- */
120
- _setBit(position) {
121
- const byteIndex = Math.floor(position / 8);
122
- const bitIndex = position % 8;
123
- this._bits[byteIndex] |= (1 << bitIndex);
124
- }
125
-
126
- /**
127
- * Gets a bit at the given position
128
- * @param {number} position - Bit position
129
- * @returns {boolean} True if bit is set
130
- * @private
131
- */
132
- _getBit(position) {
133
- const byteIndex = Math.floor(position / 8);
134
- const bitIndex = position % 8;
135
- return (this._bits[byteIndex] & (1 << bitIndex)) !== 0;
136
- }
137
-
138
123
  /**
139
124
  * Adds an item to the filter
125
+ * Inlined position computation to avoid intermediate array allocation
140
126
  * @param {string|Uint8Array} item - Item to add
141
127
  */
142
128
  add(item) {
143
- const positions = this._getPositions(item);
144
- for (const pos of positions) {
145
- this._setBit(pos);
129
+ const data = this._toBytes(item);
130
+ for (let i = 0; i < this.hashCount; i++) {
131
+ const pos = this._fnv1a(data, i) % this.size;
132
+ const byteIndex = pos >> 3;
133
+ const mask = 1 << (pos & 7);
134
+ if (!(this._bits[byteIndex] & mask)) {
135
+ this._bits[byteIndex] |= mask;
136
+ this._setBitCount++;
137
+ }
146
138
  }
147
139
  this._count++;
148
140
  }
149
141
 
150
142
  /**
151
143
  * Tests if an item might be in the filter
144
+ * Inlined position computation to avoid intermediate array allocation
152
145
  * @param {string|Uint8Array} item - Item to test
153
146
  * @returns {boolean} True if item might be present, false if definitely absent
154
147
  */
155
148
  mightContain(item) {
156
- const positions = this._getPositions(item);
157
- for (const pos of positions) {
158
- if (!this._getBit(pos)) {
149
+ const data = this._toBytes(item);
150
+ for (let i = 0; i < this.hashCount; i++) {
151
+ const pos = this._fnv1a(data, i) % this.size;
152
+ if (!(this._bits[pos >> 3] & (1 << (pos & 7)))) {
159
153
  return false;
160
154
  }
161
155
  }
@@ -168,22 +162,15 @@ class BloomFilter {
168
162
  clear() {
169
163
  this._bits.fill(0);
170
164
  this._count = 0;
165
+ this._setBitCount = 0;
171
166
  }
172
167
 
173
168
  /**
174
- * Gets the fill ratio of the filter
169
+ * Gets the fill ratio of the filter (O(1) using running count)
175
170
  * @returns {number} Ratio of set bits to total bits (0-1)
176
171
  */
177
172
  getFillRatio() {
178
- let setBits = 0;
179
- for (let i = 0; i < this._bits.length; i++) {
180
- let byte = this._bits[i];
181
- while (byte) {
182
- setBits += byte & 1;
183
- byte >>>= 1;
184
- }
185
- }
186
- return setBits / this.size;
173
+ return this._setBitCount / this.size;
187
174
  }
188
175
 
189
176
  /**
@@ -55,6 +55,13 @@ class DedupManager {
55
55
  */
56
56
  this._cache = new MessageCache(config.cacheSize);
57
57
 
58
+ /**
59
+ * Old Bloom filter kept during grace period after reset
60
+ * @type {BloomFilter|null}
61
+ * @private
62
+ */
63
+ this._oldBloomFilter = null;
64
+
58
65
  /**
59
66
  * Auto-reset threshold for Bloom filter
60
67
  * @type {number}
@@ -62,6 +69,13 @@ class DedupManager {
62
69
  */
63
70
  this._autoResetThreshold = config.autoResetThreshold;
64
71
 
72
+ /**
73
+ * Grace period timer ID for bloom filter reset
74
+ * @type {ReturnType<typeof setTimeout>|null}
75
+ * @private
76
+ */
77
+ this._graceTimer = null;
78
+
65
79
  /**
66
80
  * Statistics for monitoring
67
81
  * @type {Object}
@@ -85,20 +99,25 @@ class DedupManager {
85
99
  isDuplicate(messageId) {
86
100
  this._stats.checks++;
87
101
 
88
- // Quick check with Bloom filter
89
- if (!this._bloomFilter.mightContain(messageId)) {
90
- return false;
91
- }
92
- this._stats.bloomPositives++;
93
-
94
- // Confirm with exact cache lookup (handles false positives)
102
+ // Check cache first (most accurate)
95
103
  if (this._cache.has(messageId)) {
96
104
  this._stats.cacheHits++;
97
105
  this._stats.duplicates++;
98
106
  return true;
99
107
  }
100
108
 
101
- // Bloom filter false positive
109
+ // Check current bloom filter
110
+ if (this._bloomFilter.mightContain(messageId)) {
111
+ this._stats.bloomPositives++;
112
+ return true;
113
+ }
114
+
115
+ // Check old bloom filter if in grace period
116
+ if (this._oldBloomFilter && this._oldBloomFilter.mightContain(messageId)) {
117
+ this._stats.bloomPositives++;
118
+ return true;
119
+ }
120
+
102
121
  return false;
103
122
  }
104
123
 
@@ -135,20 +154,45 @@ class DedupManager {
135
154
  * @private
136
155
  */
137
156
  _resetBloomFilter() {
138
- this._bloomFilter.clear();
157
+ // Create a new bloom filter instead of clearing the old one
158
+ // Keep the old filter active for checking during transition
159
+ const oldFilter = this._bloomFilter;
160
+ this._bloomFilter = new BloomFilter(
161
+ oldFilter.size || MESH_CONFIG.BLOOM_FILTER_SIZE,
162
+ oldFilter.hashCount || MESH_CONFIG.BLOOM_HASH_COUNT
163
+ );
139
164
  this._stats.resets++;
140
165
 
141
- // Re-add all cached entries to Bloom filter
166
+ // Re-add all cached entries to new filter
142
167
  const entries = this._cache.getAll();
143
168
  for (const messageId of entries) {
144
169
  this._bloomFilter.add(messageId);
145
170
  }
171
+
172
+ // Keep old filter for a grace period by checking both
173
+ this._oldBloomFilter = oldFilter;
174
+
175
+ // Clear any existing grace timer before starting a new one
176
+ if (this._graceTimer) {
177
+ clearTimeout(this._graceTimer);
178
+ }
179
+
180
+ // Clear old filter after grace period
181
+ this._graceTimer = setTimeout(() => {
182
+ this._oldBloomFilter = null;
183
+ this._graceTimer = null;
184
+ }, 60000); // 1 minute grace
146
185
  }
147
186
 
148
187
  /**
149
188
  * Resets both the Bloom filter and cache
150
189
  */
151
190
  reset() {
191
+ if (this._graceTimer) {
192
+ clearTimeout(this._graceTimer);
193
+ this._graceTimer = null;
194
+ }
195
+ this._oldBloomFilter = null;
152
196
  this._bloomFilter.clear();
153
197
  this._cache.clear();
154
198
  this._stats.resets++;
@@ -207,6 +251,19 @@ class DedupManager {
207
251
  getSeenTimestamp(messageId) {
208
252
  return this._cache.getTimestamp(messageId);
209
253
  }
254
+
255
+ /**
256
+ * Destroys the dedup manager and cleans up resources
257
+ */
258
+ destroy() {
259
+ if (this._graceTimer) {
260
+ clearTimeout(this._graceTimer);
261
+ this._graceTimer = null;
262
+ }
263
+ this._oldBloomFilter = null;
264
+ this._bloomFilter.clear();
265
+ this._cache.clear();
266
+ }
210
267
  }
211
268
 
212
269
  module.exports = DedupManager;