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
@@ -22,7 +22,7 @@
22
22
 
23
23
  /**
24
24
  * Default configuration
25
- * @constant {Object}
25
+ * @constant {any}
26
26
  */
27
27
  const DEFAULT_OPTIONS = {
28
28
  /** iOS NSBluetoothAlwaysUsageDescription */
@@ -40,9 +40,9 @@ const DEFAULT_OPTIONS = {
40
40
 
41
41
  /**
42
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
43
+ * @param {any} config - Expo config
44
+ * @param {any} options - Plugin options
45
+ * @returns {any} Modified config
46
46
  */
47
47
  function withBLEMeshIOS(config, options) {
48
48
  const opts = { ...DEFAULT_OPTIONS, ...options };
@@ -64,9 +64,9 @@ function withBLEMeshIOS(config, options) {
64
64
 
65
65
  /**
66
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
67
+ * @param {any} config - Expo config
68
+ * @param {any} options - Plugin options
69
+ * @returns {any} Modified config
70
70
  */
71
71
  function withBLEMeshAndroid(config, options) {
72
72
  const opts = { ...DEFAULT_OPTIONS, ...options };
@@ -86,9 +86,9 @@ function withBLEMeshAndroid(config, options) {
86
86
 
87
87
  /**
88
88
  * Main Expo config plugin.
89
- * @param {Object} config - Expo config
90
- * @param {Object} [options={}] - Plugin options
91
- * @returns {Object} Modified config
89
+ * @param {any} config - Expo config
90
+ * @param {any} [options={}] - Plugin options
91
+ * @returns {any} Modified config
92
92
  */
93
93
  function withBLEMesh(config, options = {}) {
94
94
  config = withBLEMeshIOS(config, options);
@@ -22,7 +22,7 @@
22
22
  class AppStateManager {
23
23
  /**
24
24
  * Creates a new AppStateManager
25
- * @param {MeshService} mesh - MeshService instance to manage
25
+ * @param {any} mesh - MeshService instance to manage
26
26
  * @param {Object} [options] - Configuration options
27
27
  * @param {string} [options.backgroundMode='ULTRA_POWER_SAVER'] - Power mode for background
28
28
  * @param {string} [options.foregroundMode='BALANCED'] - Power mode for foreground
@@ -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
  /**
@@ -56,6 +58,7 @@ class AppStateManager {
56
58
 
57
59
  // Try to get AppState from React Native
58
60
  try {
61
+ // @ts-ignore
59
62
  const { AppState } = require('react-native');
60
63
  this._AppState = AppState;
61
64
  } catch (e) {
@@ -64,9 +67,15 @@ class AppStateManager {
64
67
  return false;
65
68
  }
66
69
 
70
+ // Remove old subscription before creating new one
71
+ if (this._subscription) {
72
+ this._subscription.remove();
73
+ this._subscription = null;
74
+ }
75
+
67
76
  this._subscription = this._AppState.addEventListener(
68
77
  'change',
69
- this._handleStateChange.bind(this)
78
+ this._boundHandleStateChange
70
79
  );
71
80
 
72
81
  this._initialized = true;
@@ -9,10 +9,8 @@
9
9
  * React hook for managing MeshService lifecycle in React Native apps.
10
10
  * Handles initialization, cleanup, and state management.
11
11
  *
12
- * @param {Object} [config] - MeshService configuration
13
- * @param {string} [config.displayName] - Display name for this node
14
- * @param {Object} [config.storage] - Storage adapter
15
- * @returns {Object} Mesh state and controls
12
+ * @param {any} [config] - MeshService configuration
13
+ * @returns {any} Mesh state and controls
16
14
  *
17
15
  * @example
18
16
  * function App() {
@@ -35,6 +33,7 @@ function useMesh(config = {}) {
35
33
  // This hook requires React - check if available
36
34
  let React;
37
35
  try {
36
+ // @ts-ignore
38
37
  React = require('react');
39
38
  } catch (e) {
40
39
  throw new Error('useMesh requires React. Install react as a dependency.');
@@ -47,6 +46,8 @@ function useMesh(config = {}) {
47
46
  // Create mesh instance ref (persists across renders)
48
47
  const meshRef = useRef(null);
49
48
  const mountedRef = useRef(true);
49
+ const stateHandlerRef = useRef(null);
50
+ const errorHandlerRef = useRef(null);
50
51
 
51
52
  // State
52
53
  const [state, setState] = useState('uninitialized');
@@ -63,7 +64,7 @@ function useMesh(config = {}) {
63
64
  }, [config.displayName]);
64
65
 
65
66
  // Initialize mesh
66
- const initialize = useCallback(async (transport) => {
67
+ const initialize = useCallback(async (/** @type {any} */ transport) => {
67
68
  try {
68
69
  if (!mountedRef.current) { return; }
69
70
  setState('initializing');
@@ -71,18 +72,30 @@ function useMesh(config = {}) {
71
72
 
72
73
  const mesh = getMesh();
73
74
 
75
+ // Remove old listeners if they exist (prevents accumulation on re-init)
76
+ if (stateHandlerRef.current) {
77
+ mesh.off('state-changed', stateHandlerRef.current);
78
+ }
79
+ if (errorHandlerRef.current) {
80
+ mesh.off('error', errorHandlerRef.current);
81
+ }
82
+
74
83
  // Setup state change listener
75
- mesh.on('state-changed', ({ newState }) => {
84
+ // @ts-ignore
85
+ stateHandlerRef.current = ({ newState }) => {
76
86
  if (mountedRef.current) {
77
87
  setState(newState);
78
88
  }
79
- });
89
+ };
80
90
 
81
- mesh.on('error', (err) => {
91
+ errorHandlerRef.current = (/** @type {any} */ err) => {
82
92
  if (mountedRef.current) {
83
93
  setError(err);
84
94
  }
85
- });
95
+ };
96
+
97
+ mesh.on('state-changed', stateHandlerRef.current);
98
+ mesh.on('error', errorHandlerRef.current);
86
99
 
87
100
  // Initialize with storage
88
101
  await mesh.initialize({
@@ -105,7 +118,7 @@ function useMesh(config = {}) {
105
118
  }, [getMesh, config.storage]);
106
119
 
107
120
  // Start with transport
108
- const start = useCallback(async (transport) => {
121
+ const start = useCallback(async (/** @type {any} */ transport) => {
109
122
  const mesh = getMesh();
110
123
  try {
111
124
  await mesh.start(transport);
@@ -9,10 +9,10 @@
9
9
  * React hook for sending and receiving messages in the mesh network.
10
10
  * Manages message state and provides send functions.
11
11
  *
12
- * @param {MeshService} mesh - MeshService instance
13
- * @param {Object} [options] - Options
14
- * @param {number} [options.maxMessages=100] - Maximum messages to keep in state
15
- * @returns {Object} Messages state and send functions
12
+ * @param {any} mesh - MeshService instance
13
+ * @param {any} [options] - Options
14
+ *
15
+ * @returns {any} Messages state and send functions
16
16
  *
17
17
  * @example
18
18
  * function Chat({ mesh, peerId }) {
@@ -38,6 +38,7 @@ function useMessages(mesh, options = {}) {
38
38
  // This hook requires React
39
39
  let React;
40
40
  try {
41
+ // @ts-ignore
41
42
  React = require('react');
42
43
  } catch (e) {
43
44
  throw new Error('useMessages requires React. Install react as a dependency.');
@@ -52,17 +53,17 @@ function useMessages(mesh, options = {}) {
52
53
  const messageIdRef = useRef(new Set());
53
54
 
54
55
  // Add message to state (with dedup)
55
- const addMessage = useCallback((msg) => {
56
+ const addMessage = useCallback((/** @type {any} */ msg) => {
56
57
  if (messageIdRef.current.has(msg.id)) { return; }
57
58
  messageIdRef.current.add(msg.id);
58
59
 
59
- setMessages(prev => {
60
+ setMessages((/** @type {any} */ prev) => {
60
61
  const updated = [msg, ...prev];
61
- // Trim to max messages
62
62
  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);
63
+ for (let i = maxMessages; i < updated.length; i++) {
64
+ messageIdRef.current.delete(updated[i].id);
65
+ }
66
+ updated.length = maxMessages;
66
67
  }
67
68
  return updated;
68
69
  });
@@ -72,7 +73,7 @@ function useMessages(mesh, options = {}) {
72
73
  useEffect(() => {
73
74
  if (!mesh) { return; }
74
75
 
75
- const handleBroadcast = (data) => {
76
+ const handleBroadcast = (/** @type {any} */ data) => {
76
77
  addMessage({
77
78
  id: data.messageId,
78
79
  type: 'broadcast',
@@ -83,7 +84,7 @@ function useMessages(mesh, options = {}) {
83
84
  });
84
85
  };
85
86
 
86
- const handlePrivate = (data) => {
87
+ const handlePrivate = (/** @type {any} */ data) => {
87
88
  addMessage({
88
89
  id: data.messageId,
89
90
  type: 'private',
@@ -94,7 +95,7 @@ function useMessages(mesh, options = {}) {
94
95
  });
95
96
  };
96
97
 
97
- const handleChannel = (data) => {
98
+ const handleChannel = (/** @type {any} */ data) => {
98
99
  addMessage({
99
100
  id: data.messageId,
100
101
  type: 'channel',
@@ -120,7 +121,7 @@ function useMessages(mesh, options = {}) {
120
121
  }, [mesh, addMessage]);
121
122
 
122
123
  // Send broadcast message
123
- const sendBroadcast = useCallback((content) => {
124
+ const sendBroadcast = useCallback((/** @type {any} */ content) => {
124
125
  if (!mesh) { throw new Error('Mesh not initialized'); }
125
126
  setError(null);
126
127
 
@@ -142,7 +143,7 @@ function useMessages(mesh, options = {}) {
142
143
  }, [mesh, addMessage]);
143
144
 
144
145
  // Send private message
145
- const sendPrivate = useCallback(async (peerId, content) => {
146
+ const sendPrivate = useCallback(async (/** @type {any} */ peerId, /** @type {any} */ content) => {
146
147
  if (!mesh) { throw new Error('Mesh not initialized'); }
147
148
  setError(null);
148
149
  setSending(true);
@@ -168,7 +169,7 @@ function useMessages(mesh, options = {}) {
168
169
  }, [mesh, addMessage]);
169
170
 
170
171
  // Send channel message
171
- const sendToChannel = useCallback((channelId, content) => {
172
+ const sendToChannel = useCallback((/** @type {any} */ channelId, /** @type {any} */ content) => {
172
173
  if (!mesh) { throw new Error('Mesh not initialized'); }
173
174
  setError(null);
174
175
 
@@ -9,8 +9,8 @@
9
9
  * React hook for managing and observing peers in the mesh network.
10
10
  * Automatically updates when peers connect, disconnect, or change state.
11
11
  *
12
- * @param {MeshService} mesh - MeshService instance
13
- * @returns {Object} Peers state and utilities
12
+ * @param {any} mesh - MeshService instance
13
+ * @returns {any} Peers state and utilities
14
14
  *
15
15
  * @example
16
16
  * function PeerList({ mesh }) {
@@ -30,23 +30,29 @@ function usePeers(mesh) {
30
30
  // This hook requires React
31
31
  let React;
32
32
  try {
33
+ // @ts-ignore
33
34
  React = require('react');
34
35
  } catch (e) {
35
36
  throw new Error('usePeers requires React. Install react as a dependency.');
36
37
  }
37
38
 
38
- const { useState, useEffect, useCallback, useMemo } = React;
39
+ const { useState, useEffect, useCallback, useMemo, useRef } = React;
39
40
 
40
41
  const [peers, setPeers] = useState([]);
41
- const [lastUpdate, setLastUpdate] = useState(Date.now());
42
+ const lastUpdateRef = useRef(Date.now());
42
43
 
43
44
  // Update peers from mesh
44
45
  const refreshPeers = useCallback(() => {
45
46
  if (mesh) {
46
47
  try {
47
48
  const allPeers = mesh.getPeers();
48
- setPeers(allPeers);
49
- setLastUpdate(Date.now());
49
+ setPeers((/** @type {any} */ prev) => {
50
+ if (prev.length === allPeers.length && prev.every((/** @type {any} */ p, /** @type {any} */ i) => p.id === allPeers[i]?.id && p.connectionState === allPeers[i]?.connectionState)) {
51
+ return prev;
52
+ }
53
+ return allPeers;
54
+ });
55
+ lastUpdateRef.current = Date.now();
50
56
  } catch (e) {
51
57
  // Mesh might not be ready
52
58
  }
@@ -77,20 +83,19 @@ function usePeers(mesh) {
77
83
 
78
84
  // Computed values
79
85
  const connectedPeers = useMemo(() => {
80
- return peers.filter(p => p.connectionState === 'connected' || p.connectionState === 'secured');
86
+ return peers.filter((/** @type {any} */ p) => p.connectionState === 'connected' || p.connectionState === 'secured');
81
87
  }, [peers]);
82
88
 
83
89
  const securedPeers = useMemo(() => {
84
- return peers.filter(p => p.connectionState === 'secured');
90
+ return peers.filter((/** @type {any} */ p) => p.connectionState === 'secured');
85
91
  }, [peers]);
86
92
 
87
- // Get single peer by ID
88
- const getPeer = useCallback((peerId) => {
89
- return peers.find(p => p.id === peerId);
90
- }, [peers]);
93
+ // Get single peer by ID (O(1) lookup via peerMap)
94
+ const peerMap = useMemo(() => new Map(peers.map((/** @type {any} */ p) => [p.id, p])), [peers]);
95
+ const getPeer = useCallback((/** @type {any} */ peerId) => peerMap.get(peerId), [peerMap]);
91
96
 
92
97
  // Check if peer is connected
93
- const isConnected = useCallback((peerId) => {
98
+ const isConnected = useCallback((/** @type {any} */ peerId) => {
94
99
  const peer = getPeer(peerId);
95
100
  return peer && (peer.connectionState === 'connected' || peer.connectionState === 'secured');
96
101
  }, [getPeer]);
@@ -104,7 +109,7 @@ function usePeers(mesh) {
104
109
  getPeer,
105
110
  isConnected,
106
111
  refresh: refreshPeers,
107
- lastUpdate
112
+ lastUpdate: lastUpdateRef.current
108
113
  };
109
114
  }
110
115
 
package/src/index.js CHANGED
@@ -69,7 +69,7 @@ function createMeshNetwork(config) {
69
69
 
70
70
  /**
71
71
  * Create a new MeshService instance
72
- * @param {Object} [config] - Configuration options
72
+ * @param {any} [config] - Configuration options
73
73
  * @returns {MeshService}
74
74
  */
75
75
  function createMeshService(config) {
@@ -80,7 +80,7 @@ function createMeshService(config) {
80
80
  * Create and initialize a MeshService for Node.js usage
81
81
  * @param {Object} [options] - Configuration options
82
82
  * @param {string} [options.displayName='MeshNode'] - Display name for this node
83
- * @param {Object} [options.storage=null] - Storage adapter (null for MemoryStorage)
83
+ * @param {any} [options.storage=null] - Storage adapter (null for MemoryStorage)
84
84
  * @returns {Promise<MeshService>} Initialized MeshService
85
85
  * @example
86
86
  * const mesh = await createNodeMesh({ displayName: 'Alice' });
@@ -13,6 +13,16 @@
13
13
  const FNV_PRIME = 0x01000193;
14
14
  const FNV_OFFSET = 0x811c9dc5;
15
15
 
16
+ // Cached TextEncoder singleton (avoids per-call allocation)
17
+ /** @type {any} */
18
+ let _encoder = null;
19
+ function _getEncoder() {
20
+ if (!_encoder && typeof TextEncoder !== 'undefined') {
21
+ _encoder = new TextEncoder();
22
+ }
23
+ return _encoder;
24
+ }
25
+
16
26
  /**
17
27
  * Bloom filter for probabilistic set membership testing
18
28
  * @class BloomFilter
@@ -56,6 +66,13 @@ class BloomFilter {
56
66
  * @private
57
67
  */
58
68
  this._count = 0;
69
+
70
+ /**
71
+ * Running count of set bits (avoids O(n) scan in getFillRatio)
72
+ * @type {number}
73
+ * @private
74
+ */
75
+ this._setBitCount = 0;
59
76
  }
60
77
 
61
78
  /**
@@ -90,72 +107,50 @@ class BloomFilter {
90
107
  return item;
91
108
  }
92
109
  if (typeof item === 'string') {
93
- const encoder = new TextEncoder();
94
- return encoder.encode(item);
110
+ const encoder = _getEncoder();
111
+ if (encoder) {
112
+ return encoder.encode(item);
113
+ }
114
+ // Fallback for environments without TextEncoder
115
+ const bytes = new Uint8Array(item.length);
116
+ for (let i = 0; i < item.length; i++) {
117
+ bytes[i] = item.charCodeAt(i) & 0xff;
118
+ }
119
+ return bytes;
95
120
  }
96
121
  throw new Error('Item must be string or Uint8Array');
97
122
  }
98
123
 
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
124
  /**
139
125
  * Adds an item to the filter
126
+ * Inlined position computation to avoid intermediate array allocation
140
127
  * @param {string|Uint8Array} item - Item to add
141
128
  */
142
129
  add(item) {
143
- const positions = this._getPositions(item);
144
- for (const pos of positions) {
145
- this._setBit(pos);
130
+ const data = this._toBytes(item);
131
+ for (let i = 0; i < this.hashCount; i++) {
132
+ const pos = this._fnv1a(data, i) % this.size;
133
+ const byteIndex = pos >> 3;
134
+ const mask = 1 << (pos & 7);
135
+ if (!(this._bits[byteIndex] & mask)) {
136
+ this._bits[byteIndex] |= mask;
137
+ this._setBitCount++;
138
+ }
146
139
  }
147
140
  this._count++;
148
141
  }
149
142
 
150
143
  /**
151
144
  * Tests if an item might be in the filter
145
+ * Inlined position computation to avoid intermediate array allocation
152
146
  * @param {string|Uint8Array} item - Item to test
153
147
  * @returns {boolean} True if item might be present, false if definitely absent
154
148
  */
155
149
  mightContain(item) {
156
- const positions = this._getPositions(item);
157
- for (const pos of positions) {
158
- if (!this._getBit(pos)) {
150
+ const data = this._toBytes(item);
151
+ for (let i = 0; i < this.hashCount; i++) {
152
+ const pos = this._fnv1a(data, i) % this.size;
153
+ if (!(this._bits[pos >> 3] & (1 << (pos & 7)))) {
159
154
  return false;
160
155
  }
161
156
  }
@@ -168,22 +163,15 @@ class BloomFilter {
168
163
  clear() {
169
164
  this._bits.fill(0);
170
165
  this._count = 0;
166
+ this._setBitCount = 0;
171
167
  }
172
168
 
173
169
  /**
174
- * Gets the fill ratio of the filter
170
+ * Gets the fill ratio of the filter (O(1) using running count)
175
171
  * @returns {number} Ratio of set bits to total bits (0-1)
176
172
  */
177
173
  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;
174
+ return this._setBitCount / this.size;
187
175
  }
188
176
 
189
177
  /**
@@ -29,13 +29,10 @@ const DEFAULT_CONFIG = {
29
29
  class DedupManager {
30
30
  /**
31
31
  * Creates a new DedupManager
32
- * @param {Object} [options] - Configuration options
33
- * @param {number} [options.bloomFilterSize] - Size of Bloom filter in bits
34
- * @param {number} [options.bloomHashCount] - Number of hash functions
35
- * @param {number} [options.cacheSize] - Maximum LRU cache entries
36
- * @param {number} [options.autoResetThreshold] - Fill ratio for auto reset
32
+ * @param {any} [options] - Configuration options *
37
33
  */
38
34
  constructor(options = {}) {
35
+ /** @type {any} */
39
36
  const config = { ...DEFAULT_CONFIG, ...options };
40
37
 
41
38
  /**
@@ -69,9 +66,16 @@ class DedupManager {
69
66
  */
70
67
  this._autoResetThreshold = config.autoResetThreshold;
71
68
 
69
+ /**
70
+ * Grace period timer ID for bloom filter reset
71
+ * @type {ReturnType<typeof setTimeout>|null}
72
+ * @private
73
+ */
74
+ this._graceTimer = null;
75
+
72
76
  /**
73
77
  * Statistics for monitoring
74
- * @type {Object}
78
+ * @type {any}
75
79
  * @private
76
80
  */
77
81
  this._stats = {
@@ -165,9 +169,15 @@ class DedupManager {
165
169
  // Keep old filter for a grace period by checking both
166
170
  this._oldBloomFilter = oldFilter;
167
171
 
172
+ // Clear any existing grace timer before starting a new one
173
+ if (this._graceTimer) {
174
+ clearTimeout(this._graceTimer);
175
+ }
176
+
168
177
  // Clear old filter after grace period
169
- setTimeout(() => {
178
+ this._graceTimer = setTimeout(() => {
170
179
  this._oldBloomFilter = null;
180
+ this._graceTimer = null;
171
181
  }, 60000); // 1 minute grace
172
182
  }
173
183
 
@@ -175,6 +185,11 @@ class DedupManager {
175
185
  * Resets both the Bloom filter and cache
176
186
  */
177
187
  reset() {
188
+ if (this._graceTimer) {
189
+ clearTimeout(this._graceTimer);
190
+ this._graceTimer = null;
191
+ }
192
+ this._oldBloomFilter = null;
178
193
  this._bloomFilter.clear();
179
194
  this._cache.clear();
180
195
  this._stats.resets++;
@@ -182,7 +197,7 @@ class DedupManager {
182
197
 
183
198
  /**
184
199
  * Gets deduplication statistics
185
- * @returns {Object} Statistics object
200
+ * @returns {any} Statistics object
186
201
  */
187
202
  getStats() {
188
203
  return {
@@ -233,6 +248,19 @@ class DedupManager {
233
248
  getSeenTimestamp(messageId) {
234
249
  return this._cache.getTimestamp(messageId);
235
250
  }
251
+
252
+ /**
253
+ * Destroys the dedup manager and cleans up resources
254
+ */
255
+ destroy() {
256
+ if (this._graceTimer) {
257
+ clearTimeout(this._graceTimer);
258
+ this._graceTimer = null;
259
+ }
260
+ this._oldBloomFilter = null;
261
+ this._bloomFilter.clear();
262
+ this._cache.clear();
263
+ }
236
264
  }
237
265
 
238
266
  module.exports = DedupManager;
@@ -19,7 +19,9 @@ class CacheNode {
19
19
  constructor(key, timestamp) {
20
20
  this.key = key;
21
21
  this.timestamp = timestamp;
22
+ /** @type {CacheNode|null} */
22
23
  this.prev = null;
24
+ /** @type {CacheNode|null} */
23
25
  this.next = null;
24
26
  }
25
27
  }
@@ -207,6 +209,7 @@ class MessageCache {
207
209
  */
208
210
  getAll() {
209
211
  const result = [];
212
+ /** @type {CacheNode|null} */
210
213
  let node = this._head;
211
214
  while (node) {
212
215
  result.push(node.key);
@@ -31,6 +31,7 @@ class PendingFragmentSet {
31
31
  constructor(messageId, total, timeout) {
32
32
  this.messageId = messageId;
33
33
  this.total = total;
34
+ /** @type {Map<number, Uint8Array>} */
34
35
  this.received = new Map();
35
36
  this.createdAt = Date.now();
36
37
  this.expiresAt = this.createdAt + timeout;
@@ -97,7 +98,7 @@ class PendingFragmentSet {
97
98
 
98
99
  /**
99
100
  * Gets progress information
100
- * @returns {Object} Progress { received, total, percent }
101
+ * @returns {any} Progress { received, total, percent }
101
102
  */
102
103
  getProgress() {
103
104
  return {
@@ -137,7 +138,7 @@ class Assembler {
137
138
 
138
139
  /**
139
140
  * Statistics
140
- * @type {Object}
141
+ * @type {any}
141
142
  * @private
142
143
  */
143
144
  this._stats = {
@@ -236,7 +237,7 @@ class Assembler {
236
237
  /**
237
238
  * Gets progress for a pending message
238
239
  * @param {string} messageId - Message ID
239
- * @returns {Object|null} Progress or null if not found
240
+ * @returns {any} Progress or null if not found
240
241
  */
241
242
  getProgress(messageId) {
242
243
  const pendingSet = this._pending.get(messageId);
@@ -254,7 +255,7 @@ class Assembler {
254
255
 
255
256
  /**
256
257
  * Gets assembler statistics
257
- * @returns {Object} Statistics
258
+ * @returns {any} Statistics
258
259
  */
259
260
  getStats() {
260
261
  return {