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.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +183 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +667 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +90 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
- package/src/crypto/providers/QuickCryptoProvider.js +158 -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/AppStateManager.js +9 -1
- package/src/hooks/useMesh.js +47 -13
- package/src/hooks/useMessages.js +6 -4
- package/src/hooks/usePeers.js +13 -9
- package/src/index.js +23 -8
- package/src/mesh/dedup/BloomFilter.js +44 -57
- package/src/mesh/dedup/DedupManager.js +67 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/fragment/Fragmenter.js +1 -1
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +433 -0
- package/src/mesh/monitor/NetworkMonitor.js +376 -320
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/Peer.js +5 -2
- package/src/mesh/peer/PeerManager.js +21 -4
- package/src/mesh/router/MessageRouter.js +38 -19
- package/src/mesh/router/RouteTable.js +24 -8
- package/src/mesh/store/StoreAndForwardManager.js +305 -296
- package/src/mesh/store/index.js +1 -1
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +15 -3
- package/src/protocol/serializer.js +7 -10
- package/src/protocol/validator.js +23 -5
- package/src/service/BatteryOptimizer.js +285 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +163 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +79 -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/TextManager.js +21 -15
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/storage/MessageStore.js +55 -2
- package/src/transport/BLETransport.js +141 -10
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +330 -0
- package/src/transport/WiFiDirectTransport.js +296 -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/EventEmitter.js +6 -9
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +293 -291
- package/src/utils/encoding.js +33 -8
- 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
package/src/errors/MeshError.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
77
|
+
this._boundHandleStateChange
|
|
70
78
|
);
|
|
71
79
|
|
|
72
80
|
this._initialized = true;
|
package/src/hooks/useMesh.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
stateHandlerRef.current = ({ newState }) => {
|
|
86
|
+
if (mountedRef.current) {
|
|
87
|
+
setState(newState);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}, []);
|
package/src/hooks/useMessages.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
package/src/hooks/usePeers.js
CHANGED
|
@@ -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
|
|
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(
|
|
49
|
-
|
|
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
|
|
89
|
-
|
|
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
|
|
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 =
|
|
94
|
-
|
|
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
|
|
144
|
-
for (
|
|
145
|
-
this.
|
|
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
|
|
157
|
-
for (
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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;
|