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.
- package/README.md +2 -2
- package/docs/OPTIMIZATION.md +165 -52
- package/package.json +1 -1
- package/src/MeshNetwork.js +63 -53
- package/src/constants/audio.js +4 -4
- package/src/constants/ble.js +1 -1
- package/src/constants/crypto.js +1 -1
- package/src/constants/errors.js +2 -2
- package/src/constants/events.js +1 -1
- package/src/constants/protocol.js +2 -2
- package/src/crypto/AutoCrypto.js +16 -3
- package/src/crypto/CryptoProvider.js +17 -17
- package/src/crypto/providers/ExpoCryptoProvider.js +15 -9
- package/src/crypto/providers/QuickCryptoProvider.js +41 -12
- package/src/crypto/providers/TweetNaClProvider.js +10 -8
- package/src/errors/AudioError.js +2 -1
- package/src/errors/ConnectionError.js +2 -2
- package/src/errors/CryptoError.js +1 -1
- package/src/errors/HandshakeError.js +2 -2
- package/src/errors/MeshError.js +4 -4
- package/src/errors/MessageError.js +2 -2
- package/src/errors/ValidationError.js +3 -3
- package/src/expo/withBLEMesh.js +10 -10
- package/src/hooks/AppStateManager.js +11 -2
- package/src/hooks/useMesh.js +23 -10
- package/src/hooks/useMessages.js +17 -16
- package/src/hooks/usePeers.js +19 -14
- package/src/index.js +2 -2
- package/src/mesh/dedup/BloomFilter.js +45 -57
- package/src/mesh/dedup/DedupManager.js +36 -8
- package/src/mesh/dedup/MessageCache.js +3 -0
- package/src/mesh/fragment/Assembler.js +5 -4
- package/src/mesh/fragment/Fragmenter.js +3 -3
- package/src/mesh/monitor/ConnectionQuality.js +59 -25
- package/src/mesh/monitor/NetworkMonitor.js +80 -28
- package/src/mesh/peer/Peer.js +9 -11
- package/src/mesh/peer/PeerDiscovery.js +18 -19
- package/src/mesh/peer/PeerManager.js +29 -17
- package/src/mesh/router/MessageRouter.js +28 -20
- package/src/mesh/router/PathFinder.js +10 -13
- package/src/mesh/router/RouteTable.js +25 -14
- package/src/mesh/store/StoreAndForwardManager.js +32 -24
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +18 -14
- package/src/protocol/serializer.js +9 -12
- package/src/protocol/validator.js +29 -10
- package/src/service/BatteryOptimizer.js +22 -18
- package/src/service/EmergencyManager.js +18 -25
- package/src/service/HandshakeManager.js +112 -18
- package/src/service/MeshService.js +106 -22
- package/src/service/SessionManager.js +50 -13
- package/src/service/audio/AudioManager.js +80 -38
- package/src/service/audio/buffer/FrameBuffer.js +7 -8
- package/src/service/audio/buffer/JitterBuffer.js +1 -1
- package/src/service/audio/codec/LC3Codec.js +18 -19
- package/src/service/audio/codec/LC3Decoder.js +10 -10
- package/src/service/audio/codec/LC3Encoder.js +11 -9
- package/src/service/audio/session/AudioSession.js +14 -17
- package/src/service/audio/session/VoiceMessage.js +15 -22
- package/src/service/audio/transport/AudioFragmenter.js +17 -9
- package/src/service/audio/transport/AudioFramer.js +8 -12
- package/src/service/file/FileAssembler.js +4 -2
- package/src/service/file/FileChunker.js +1 -1
- package/src/service/file/FileManager.js +26 -20
- package/src/service/file/FileMessage.js +7 -12
- package/src/service/text/TextManager.js +75 -42
- package/src/service/text/broadcast/BroadcastManager.js +14 -17
- package/src/service/text/channel/Channel.js +10 -14
- package/src/service/text/channel/ChannelManager.js +10 -10
- package/src/service/text/message/TextMessage.js +12 -19
- package/src/service/text/message/TextSerializer.js +2 -2
- package/src/storage/AsyncStorageAdapter.js +17 -14
- package/src/storage/MemoryStorage.js +11 -8
- package/src/storage/MessageStore.js +77 -32
- package/src/storage/Storage.js +9 -9
- package/src/transport/BLETransport.js +27 -16
- package/src/transport/MockTransport.js +7 -2
- package/src/transport/MultiTransport.js +43 -11
- package/src/transport/Transport.js +9 -9
- package/src/transport/WiFiDirectTransport.js +26 -20
- package/src/transport/adapters/BLEAdapter.js +19 -19
- package/src/transport/adapters/NodeBLEAdapter.js +24 -23
- package/src/transport/adapters/RNBLEAdapter.js +14 -11
- package/src/utils/EventEmitter.js +15 -16
- package/src/utils/LRUCache.js +10 -4
- package/src/utils/RateLimiter.js +1 -1
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +10 -8
- package/src/utils/encoding.js +39 -8
- package/src/utils/retry.js +11 -13
- package/src/utils/time.js +9 -4
- package/src/utils/validation.js +1 -1
package/src/expo/withBLEMesh.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Default configuration
|
|
25
|
-
* @constant {
|
|
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 {
|
|
44
|
-
* @param {
|
|
45
|
-
* @returns {
|
|
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 {
|
|
68
|
-
* @param {
|
|
69
|
-
* @returns {
|
|
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 {
|
|
90
|
-
* @param {
|
|
91
|
-
* @returns {
|
|
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 {
|
|
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.
|
|
78
|
+
this._boundHandleStateChange
|
|
70
79
|
);
|
|
71
80
|
|
|
72
81
|
this._initialized = true;
|
package/src/hooks/useMesh.js
CHANGED
|
@@ -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 {
|
|
13
|
-
* @
|
|
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
|
-
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
stateHandlerRef.current = ({ newState }) => {
|
|
76
86
|
if (mountedRef.current) {
|
|
77
87
|
setState(newState);
|
|
78
88
|
}
|
|
79
|
-
}
|
|
89
|
+
};
|
|
80
90
|
|
|
81
|
-
|
|
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);
|
package/src/hooks/useMessages.js
CHANGED
|
@@ -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 {
|
|
13
|
-
* @param {
|
|
14
|
-
|
|
15
|
-
* @returns {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
package/src/hooks/usePeers.js
CHANGED
|
@@ -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 {
|
|
13
|
-
* @returns {
|
|
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
|
|
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(
|
|
49
|
-
|
|
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
|
|
89
|
-
|
|
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 {
|
|
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 {
|
|
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 =
|
|
94
|
-
|
|
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
|
|
144
|
-
for (
|
|
145
|
-
this.
|
|
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
|
|
157
|
-
for (
|
|
158
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
258
|
+
* @returns {any} Statistics
|
|
258
259
|
*/
|
|
259
260
|
getStats() {
|
|
260
261
|
return {
|