react-native-ble-mesh 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +70 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +659 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +79 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +125 -0
- package/src/crypto/providers/QuickCryptoProvider.js +134 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/useMesh.js +30 -9
- package/src/hooks/useMessages.js +2 -0
- package/src/index.js +23 -8
- package/src/mesh/dedup/DedupManager.js +36 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +408 -0
- package/src/mesh/monitor/NetworkMonitor.js +327 -316
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/PeerManager.js +6 -1
- package/src/mesh/router/MessageRouter.js +26 -15
- package/src/mesh/router/RouteTable.js +7 -1
- package/src/mesh/store/StoreAndForwardManager.js +295 -297
- package/src/mesh/store/index.js +1 -1
- package/src/service/BatteryOptimizer.js +282 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +167 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +77 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/transport/BLETransport.js +131 -9
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +305 -0
- package/src/transport/WiFiDirectTransport.js +295 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/compression.js +291 -291
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
package/src/MeshNetwork.js
CHANGED
|
@@ -3,60 +3,61 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview MeshNetwork - Simplified High-Level API
|
|
5
5
|
* @module MeshNetwork
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Primary entry point for react-native-ble-mesh as specified in the PRD.
|
|
8
8
|
* Provides a developer-friendly API for BitChat-compatible mesh networking.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* Target: Setup-to-first-message in <15 minutes, <10 lines for basic messaging.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const EventEmitter = require('./utils/EventEmitter');
|
|
14
14
|
const { MeshService } = require('./service');
|
|
15
|
-
const { BLETransport
|
|
15
|
+
const { BLETransport } = require('./transport');
|
|
16
16
|
const { MemoryStorage } = require('./storage');
|
|
17
17
|
const { StoreAndForwardManager } = require('./mesh/store');
|
|
18
|
-
const { NetworkMonitor, HEALTH_STATUS } = require('./mesh/monitor');
|
|
18
|
+
const { NetworkMonitor, HEALTH_STATUS, ConnectionQuality } = require('./mesh/monitor');
|
|
19
19
|
const { BatteryOptimizer, BATTERY_MODE } = require('./service/BatteryOptimizer');
|
|
20
20
|
const { EmergencyManager, PANIC_TRIGGER } = require('./service/EmergencyManager');
|
|
21
21
|
const { MessageCompressor } = require('./utils/compression');
|
|
22
|
-
const { EVENTS } = require('./constants');
|
|
23
22
|
const { ValidationError, MeshError } = require('./errors');
|
|
23
|
+
const { FileManager } = require('./service/file');
|
|
24
|
+
const { SERVICE_STATE, EVENTS } = require('./constants');
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Default MeshNetwork configuration
|
|
27
28
|
* @constant {Object}
|
|
28
29
|
*/
|
|
29
30
|
const DEFAULT_CONFIG = Object.freeze({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
31
|
+
/** Display name for this node */
|
|
32
|
+
nickname: 'Anonymous',
|
|
33
|
+
/** Battery mode: 'high' | 'balanced' | 'low' | 'auto' */
|
|
34
|
+
batteryMode: BATTERY_MODE.BALANCED,
|
|
35
|
+
/** Encryption settings */
|
|
36
|
+
encryption: {
|
|
37
|
+
level: 'standard',
|
|
38
|
+
rotateKeysAfter: 1000
|
|
39
|
+
},
|
|
40
|
+
/** Routing configuration */
|
|
41
|
+
routing: {
|
|
42
|
+
maxHops: 7,
|
|
43
|
+
bloomFilterSize: 10000
|
|
44
|
+
},
|
|
45
|
+
/** Compression settings */
|
|
46
|
+
compression: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
threshold: 100
|
|
49
|
+
},
|
|
50
|
+
/** Store and forward settings */
|
|
51
|
+
storeAndForward: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
retentionHours: 24,
|
|
54
|
+
maxCachedMessages: 1000
|
|
55
|
+
}
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* MeshNetwork - High-level API for BitChat-compatible mesh networking.
|
|
59
|
-
*
|
|
60
|
+
*
|
|
60
61
|
* @class MeshNetwork
|
|
61
62
|
* @extends EventEmitter
|
|
62
63
|
* @example
|
|
@@ -65,16 +66,16 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
65
66
|
* nickname: 'Alice',
|
|
66
67
|
* batteryMode: 'balanced',
|
|
67
68
|
* });
|
|
68
|
-
*
|
|
69
|
+
*
|
|
69
70
|
* await mesh.start();
|
|
70
71
|
* await mesh.broadcast('Hello mesh!');
|
|
71
|
-
*
|
|
72
|
+
*
|
|
72
73
|
* mesh.on('messageReceived', (message) => {
|
|
73
74
|
* console.log(`${message.from}: ${message.text}`);
|
|
74
75
|
* });
|
|
75
76
|
*/
|
|
76
77
|
class MeshNetwork extends EventEmitter {
|
|
77
|
-
|
|
78
|
+
/**
|
|
78
79
|
* Creates a new MeshNetwork instance.
|
|
79
80
|
* @param {Object} [config={}] - Network configuration
|
|
80
81
|
* @param {string} [config.nickname='Anonymous'] - Display name
|
|
@@ -84,694 +85,887 @@ class MeshNetwork extends EventEmitter {
|
|
|
84
85
|
* @param {Object} [config.compression] - Compression settings
|
|
85
86
|
* @param {Object} [config.storeAndForward] - Store and forward settings
|
|
86
87
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
constructor(config = {}) {
|
|
89
|
+
super();
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
/**
|
|
91
92
|
* Configuration
|
|
92
93
|
* @type {Object}
|
|
93
94
|
* @private
|
|
94
95
|
*/
|
|
95
|
-
|
|
96
|
+
this._config = this._mergeConfig(DEFAULT_CONFIG, config);
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
/**
|
|
98
99
|
* Underlying MeshService
|
|
99
100
|
* @type {MeshService}
|
|
100
101
|
* @private
|
|
101
102
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
this._service = new MeshService({
|
|
104
|
+
displayName: this._config.nickname
|
|
105
|
+
});
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
/**
|
|
107
108
|
* Transport layer
|
|
108
109
|
* @type {Transport|null}
|
|
109
110
|
* @private
|
|
110
111
|
*/
|
|
111
|
-
|
|
112
|
+
this._transport = null;
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
/**
|
|
114
115
|
* Store and forward manager
|
|
115
116
|
* @type {StoreAndForwardManager|null}
|
|
116
117
|
* @private
|
|
117
118
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
this._storeForward = this._config.storeAndForward.enabled
|
|
120
|
+
? new StoreAndForwardManager({
|
|
121
|
+
retentionMs: this._config.storeAndForward.retentionHours * 60 * 60 * 1000,
|
|
122
|
+
maxTotalMessages: this._config.storeAndForward.maxCachedMessages
|
|
123
|
+
})
|
|
124
|
+
: null;
|
|
125
|
+
|
|
126
|
+
/**
|
|
126
127
|
* Network monitor
|
|
127
128
|
* @type {NetworkMonitor}
|
|
128
129
|
* @private
|
|
129
130
|
*/
|
|
130
|
-
|
|
131
|
+
this._monitor = new NetworkMonitor();
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
/**
|
|
133
134
|
* Battery optimizer
|
|
134
135
|
* @type {BatteryOptimizer}
|
|
135
136
|
* @private
|
|
136
137
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
this._batteryOptimizer = new BatteryOptimizer({
|
|
139
|
+
initialMode: this._config.batteryMode
|
|
140
|
+
});
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
/**
|
|
142
143
|
* Emergency manager
|
|
143
144
|
* @type {EmergencyManager}
|
|
144
145
|
* @private
|
|
145
146
|
*/
|
|
146
|
-
|
|
147
|
+
this._emergencyManager = new EmergencyManager();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* File transfer manager
|
|
151
|
+
* @type {FileManager}
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
this._fileManager = new FileManager({
|
|
155
|
+
chunkSize: this._config.fileTransfer?.chunkSize,
|
|
156
|
+
maxFileSize: this._config.fileTransfer?.maxFileSize,
|
|
157
|
+
transferTimeoutMs: this._config.fileTransfer?.timeoutMs
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Connection quality tracker
|
|
162
|
+
* @type {ConnectionQuality}
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
this._connectionQuality = new ConnectionQuality(this._config.qualityConfig || {});
|
|
147
166
|
|
|
148
|
-
|
|
167
|
+
/**
|
|
149
168
|
* Message compressor
|
|
150
169
|
* @type {MessageCompressor}
|
|
151
170
|
* @private
|
|
152
171
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
this._compressor = new MessageCompressor({
|
|
173
|
+
threshold: this._config.compression.threshold
|
|
174
|
+
});
|
|
156
175
|
|
|
157
|
-
|
|
176
|
+
/**
|
|
158
177
|
* Channel manager reference
|
|
159
178
|
* @type {Object|null}
|
|
160
179
|
* @private
|
|
161
180
|
*/
|
|
162
|
-
|
|
181
|
+
this._channels = null;
|
|
163
182
|
|
|
164
|
-
|
|
183
|
+
/**
|
|
165
184
|
* Network state
|
|
166
185
|
* @type {string}
|
|
167
186
|
* @private
|
|
168
187
|
*/
|
|
169
|
-
|
|
188
|
+
this._state = 'stopped';
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
// Setup event forwarding
|
|
191
|
+
this._setupEventForwarding();
|
|
192
|
+
}
|
|
174
193
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Lifecycle Methods
|
|
196
|
+
// ============================================================================
|
|
178
197
|
|
|
179
|
-
|
|
198
|
+
/**
|
|
180
199
|
* Starts the mesh network.
|
|
181
200
|
* @param {Object} [transport] - Optional custom transport
|
|
182
201
|
* @returns {Promise<void>}
|
|
183
202
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
203
|
+
async start(transport) {
|
|
204
|
+
if (this._state === 'running') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create transport if not provided
|
|
209
|
+
if (!transport) {
|
|
210
|
+
try {
|
|
211
|
+
const { RNBLEAdapter } = require('./transport/adapters');
|
|
212
|
+
const adapter = new RNBLEAdapter();
|
|
213
|
+
this._transport = new BLETransport(adapter);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
throw new MeshError(
|
|
216
|
+
'No transport provided and BLE adapter not available. ' +
|
|
217
|
+
'Either pass a transport to start(), install react-native-ble-plx for BLE, ' +
|
|
218
|
+
'or use MockTransport for testing.',
|
|
219
|
+
'E900',
|
|
220
|
+
{ cause: e.message }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
this._transport = transport;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Initialize the service (only on first start)
|
|
228
|
+
if (this._service._state === SERVICE_STATE.UNINITIALIZED) {
|
|
229
|
+
await this._service.initialize({
|
|
230
|
+
storage: new MemoryStorage()
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Connect battery optimizer to transport
|
|
235
|
+
this._batteryOptimizer.setTransport(this._transport);
|
|
236
|
+
await this._batteryOptimizer.setMode(this._config.batteryMode);
|
|
237
|
+
|
|
238
|
+
// Register data clearers for panic mode
|
|
239
|
+
this._registerPanicClearers();
|
|
240
|
+
|
|
241
|
+
// Start the service
|
|
242
|
+
await this._service.start(this._transport);
|
|
243
|
+
|
|
244
|
+
// Setup store and forward
|
|
245
|
+
if (this._storeForward) {
|
|
246
|
+
this._setupStoreAndForward();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Setup file transfer events
|
|
250
|
+
this._fileManager.on('sendProgress', (info) => this.emit('fileSendProgress', info));
|
|
251
|
+
this._fileManager.on('receiveProgress', (info) => this.emit('fileReceiveProgress', info));
|
|
252
|
+
this._fileManager.on('fileReceived', (info) => this.emit('fileReceived', info));
|
|
253
|
+
this._fileManager.on('transferFailed', (info) => this.emit('fileTransferFailed', info));
|
|
254
|
+
this._fileManager.on('transferCancelled', (info) => this.emit('fileTransferCancelled', info));
|
|
255
|
+
|
|
256
|
+
// Start connection quality monitoring
|
|
257
|
+
this._connectionQuality.start();
|
|
258
|
+
this._connectionQuality.on('qualityChanged', (quality) => {
|
|
259
|
+
this.emit('connectionQualityChanged', quality);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
this._state = 'running';
|
|
263
|
+
this.emit('started');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
217
267
|
* Stops the mesh network.
|
|
218
268
|
* @returns {Promise<void>}
|
|
219
269
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
await this._service.stop();
|
|
226
|
-
this._state = 'stopped';
|
|
227
|
-
this.emit('stopped');
|
|
270
|
+
async stop() {
|
|
271
|
+
if (this._state !== 'running') {
|
|
272
|
+
return;
|
|
228
273
|
}
|
|
229
274
|
|
|
230
|
-
|
|
275
|
+
// Clean up listeners added in start()
|
|
276
|
+
this._fileManager.removeAllListeners('sendProgress');
|
|
277
|
+
this._fileManager.removeAllListeners('receiveProgress');
|
|
278
|
+
this._fileManager.removeAllListeners('fileReceived');
|
|
279
|
+
this._fileManager.removeAllListeners('transferFailed');
|
|
280
|
+
this._fileManager.removeAllListeners('transferCancelled');
|
|
281
|
+
this._connectionQuality.removeAllListeners('qualityChanged');
|
|
282
|
+
this._connectionQuality.stop();
|
|
283
|
+
|
|
284
|
+
await this._service.stop();
|
|
285
|
+
this._state = 'stopped';
|
|
286
|
+
this.emit('stopped');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
231
290
|
* Destroys the mesh network and cleans up resources.
|
|
232
291
|
* @returns {Promise<void>}
|
|
233
292
|
*/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
293
|
+
async destroy() {
|
|
294
|
+
await this.stop();
|
|
295
|
+
await this._service.destroy();
|
|
237
296
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
this._monitor.destroy();
|
|
242
|
-
this._batteryOptimizer.destroy();
|
|
243
|
-
this._emergencyManager.destroy();
|
|
297
|
+
this._connectionQuality.destroy();
|
|
298
|
+
this._fileManager.destroy();
|
|
244
299
|
|
|
245
|
-
|
|
300
|
+
if (this._storeForward) {
|
|
301
|
+
this._storeForward.destroy();
|
|
246
302
|
}
|
|
303
|
+
this._monitor.destroy();
|
|
304
|
+
this._batteryOptimizer.destroy();
|
|
305
|
+
this._emergencyManager.destroy();
|
|
247
306
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// ============================================================================
|
|
307
|
+
this.removeAllListeners();
|
|
308
|
+
}
|
|
251
309
|
|
|
252
|
-
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Messaging Methods
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
253
315
|
* Broadcasts a message to all peers.
|
|
254
316
|
* @param {string} text - Message text
|
|
255
317
|
* @returns {Promise<string>} Message ID
|
|
256
318
|
* @throws {Error} If text is invalid
|
|
257
319
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
320
|
+
async broadcast(text) {
|
|
321
|
+
this._validateRunning();
|
|
322
|
+
this._validateMessageText(text);
|
|
261
323
|
|
|
262
|
-
|
|
263
|
-
|
|
324
|
+
const messageId = await this._service.sendBroadcast(text);
|
|
325
|
+
this._monitor.trackMessageSent('broadcast', messageId);
|
|
264
326
|
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
return messageId;
|
|
328
|
+
}
|
|
267
329
|
|
|
268
|
-
|
|
330
|
+
/**
|
|
269
331
|
* Sends a direct encrypted message to a specific peer.
|
|
270
332
|
* @param {string} peerId - Target peer ID or nickname
|
|
271
333
|
* @param {string} text - Message text
|
|
272
334
|
* @returns {Promise<string>} Message ID
|
|
273
335
|
* @throws {Error} If peerId or text is invalid
|
|
274
336
|
*/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
async sendDirect(peerId, text) {
|
|
338
|
+
this._validateRunning();
|
|
339
|
+
this._validatePeerId(peerId);
|
|
340
|
+
this._validateMessageText(text);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const messageId = await this._service.sendPrivateMessage(peerId, text);
|
|
344
|
+
this._monitor.trackMessageSent(peerId, messageId);
|
|
345
|
+
return messageId;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
// If peer is offline and store-forward is enabled, cache the message
|
|
348
|
+
if (this._storeForward && this._isPeerOffline(peerId)) {
|
|
349
|
+
const payload = this._encodeMessage(text);
|
|
350
|
+
await this._storeForward.cacheForOfflinePeer(peerId, payload, {
|
|
351
|
+
needsEncryption: true
|
|
352
|
+
});
|
|
353
|
+
this.emit('messageCached', { peerId, text });
|
|
354
|
+
return 'cached';
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
294
357
|
}
|
|
358
|
+
}
|
|
295
359
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Channel Methods
|
|
362
|
+
// ============================================================================
|
|
299
363
|
|
|
300
|
-
|
|
364
|
+
/**
|
|
301
365
|
* Joins a channel (IRC-style topic-based group chat).
|
|
302
366
|
* @param {string} channelName - Channel name (e.g., '#general')
|
|
303
367
|
* @param {string} [password] - Optional password
|
|
304
368
|
* @returns {Promise<void>}
|
|
305
369
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
370
|
+
async joinChannel(channelName, password) {
|
|
371
|
+
this._validateRunning();
|
|
308
372
|
|
|
309
|
-
|
|
310
|
-
|
|
373
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
374
|
+
await this._service.joinChannel(normalized, password);
|
|
311
375
|
|
|
312
|
-
|
|
313
|
-
|
|
376
|
+
this.emit('channelJoined', { channel: normalized });
|
|
377
|
+
}
|
|
314
378
|
|
|
315
|
-
|
|
379
|
+
/**
|
|
316
380
|
* Leaves a channel.
|
|
317
381
|
* @param {string} channelName - Channel name
|
|
318
382
|
* @returns {Promise<void>}
|
|
319
383
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
384
|
+
async leaveChannel(channelName) {
|
|
385
|
+
this._validateRunning();
|
|
322
386
|
|
|
323
|
-
|
|
324
|
-
|
|
387
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
388
|
+
await this._service.leaveChannel(normalized);
|
|
325
389
|
|
|
326
|
-
|
|
327
|
-
|
|
390
|
+
this.emit('channelLeft', { channel: normalized });
|
|
391
|
+
}
|
|
328
392
|
|
|
329
|
-
|
|
393
|
+
/**
|
|
330
394
|
* Sends a message to a channel.
|
|
331
395
|
* @param {string} channelName - Channel name
|
|
332
396
|
* @param {string} text - Message text
|
|
333
397
|
* @returns {Promise<string>} Message ID
|
|
334
398
|
*/
|
|
335
|
-
|
|
336
|
-
|
|
399
|
+
async sendToChannel(channelName, text) {
|
|
400
|
+
this._validateRunning();
|
|
337
401
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
403
|
+
return await this._service.sendChannelMessage(normalized, text);
|
|
404
|
+
}
|
|
341
405
|
|
|
342
|
-
|
|
406
|
+
/**
|
|
343
407
|
* Gets list of joined channels.
|
|
344
408
|
* @returns {Object[]} Channels
|
|
345
409
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
410
|
+
getChannels() {
|
|
411
|
+
return this._service.getChannels();
|
|
412
|
+
}
|
|
349
413
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
414
|
+
// ============================================================================
|
|
415
|
+
// Peer Methods
|
|
416
|
+
// ============================================================================
|
|
353
417
|
|
|
354
|
-
|
|
418
|
+
/**
|
|
355
419
|
* Gets all known peers.
|
|
356
420
|
* @returns {Object[]} Array of peers
|
|
357
421
|
*/
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
422
|
+
getPeers() {
|
|
423
|
+
return this._service.getPeers();
|
|
424
|
+
}
|
|
361
425
|
|
|
362
|
-
|
|
426
|
+
/**
|
|
363
427
|
* Gets connected peers.
|
|
364
428
|
* @returns {Object[]} Connected peers
|
|
365
429
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
430
|
+
getConnectedPeers() {
|
|
431
|
+
return this._service.getConnectedPeers();
|
|
432
|
+
}
|
|
369
433
|
|
|
370
|
-
|
|
434
|
+
/**
|
|
371
435
|
* Gets peers with secure sessions.
|
|
372
436
|
* @returns {Object[]} Secured peers
|
|
373
437
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
438
|
+
getSecuredPeers() {
|
|
439
|
+
return this._service.getSecuredPeers();
|
|
440
|
+
}
|
|
377
441
|
|
|
378
|
-
|
|
442
|
+
/**
|
|
379
443
|
* Blocks a peer.
|
|
380
444
|
* @param {string} peerId - Peer ID
|
|
381
445
|
*/
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
446
|
+
blockPeer(peerId) {
|
|
447
|
+
this._service.blockPeer(peerId);
|
|
448
|
+
this.emit('peerBlocked', { peerId });
|
|
449
|
+
}
|
|
386
450
|
|
|
387
|
-
|
|
451
|
+
/**
|
|
388
452
|
* Unblocks a peer.
|
|
389
453
|
* @param {string} peerId - Peer ID
|
|
390
454
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
455
|
+
unblockPeer(peerId) {
|
|
456
|
+
this._service.unblockPeer(peerId);
|
|
457
|
+
this.emit('peerUnblocked', { peerId });
|
|
458
|
+
}
|
|
395
459
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// Network Health Methods
|
|
462
|
+
// ============================================================================
|
|
399
463
|
|
|
400
|
-
|
|
464
|
+
/**
|
|
401
465
|
* Gets network health metrics.
|
|
402
466
|
* @returns {Object} Health report
|
|
403
467
|
*/
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
468
|
+
getNetworkHealth() {
|
|
469
|
+
return this._monitor.generateHealthReport();
|
|
470
|
+
}
|
|
407
471
|
|
|
408
|
-
|
|
472
|
+
/**
|
|
409
473
|
* Gets detailed health for a specific peer.
|
|
410
474
|
* @param {string} peerId - Peer ID
|
|
411
475
|
* @returns {Object|null} Node health
|
|
412
476
|
*/
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
477
|
+
getPeerHealth(peerId) {
|
|
478
|
+
return this._monitor.getNodeHealth(peerId);
|
|
479
|
+
}
|
|
416
480
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
481
|
+
// ============================================================================
|
|
482
|
+
// Battery Management
|
|
483
|
+
// ============================================================================
|
|
420
484
|
|
|
421
|
-
|
|
485
|
+
/**
|
|
422
486
|
* Sets the battery mode.
|
|
423
487
|
* @param {string} mode - 'high' | 'balanced' | 'low' | 'auto'
|
|
424
488
|
* @returns {Promise<void>}
|
|
425
489
|
*/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
490
|
+
async setBatteryMode(mode) {
|
|
491
|
+
await this._batteryOptimizer.setMode(mode);
|
|
492
|
+
}
|
|
429
493
|
|
|
430
|
-
|
|
494
|
+
/**
|
|
431
495
|
* Gets the current battery mode.
|
|
432
496
|
* @returns {string} Current mode
|
|
433
497
|
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
498
|
+
getBatteryMode() {
|
|
499
|
+
return this._batteryOptimizer.getMode();
|
|
500
|
+
}
|
|
437
501
|
|
|
438
|
-
|
|
502
|
+
/**
|
|
439
503
|
* Updates battery level (for auto mode).
|
|
440
504
|
* @param {number} level - Battery level (0-100)
|
|
441
505
|
* @param {boolean} [charging=false] - Whether charging
|
|
442
506
|
*/
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
507
|
+
updateBatteryLevel(level, charging = false) {
|
|
508
|
+
this._batteryOptimizer.updateBatteryLevel(level, charging);
|
|
509
|
+
}
|
|
446
510
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
511
|
+
// ============================================================================
|
|
512
|
+
// Security Methods
|
|
513
|
+
// ============================================================================
|
|
450
514
|
|
|
451
|
-
|
|
515
|
+
/**
|
|
452
516
|
* Enables panic mode for emergency data wipe.
|
|
453
517
|
* @param {Object} [options={}] - Panic mode options
|
|
454
518
|
* @param {string} [options.trigger='triple_tap'] - Trigger type
|
|
455
519
|
* @param {Function} [options.onWipe] - Callback after wipe
|
|
456
520
|
*/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
521
|
+
enablePanicMode(options = {}) {
|
|
522
|
+
this._emergencyManager.enablePanicMode(options);
|
|
523
|
+
this.emit('panicModeEnabled');
|
|
524
|
+
}
|
|
461
525
|
|
|
462
|
-
|
|
526
|
+
/**
|
|
463
527
|
* Disables panic mode.
|
|
464
528
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
529
|
+
disablePanicMode() {
|
|
530
|
+
this._emergencyManager.disablePanicMode();
|
|
531
|
+
this.emit('panicModeDisabled');
|
|
532
|
+
}
|
|
469
533
|
|
|
470
|
-
|
|
534
|
+
/**
|
|
471
535
|
* Registers a tap for panic mode detection.
|
|
472
536
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
537
|
+
registerPanicTap() {
|
|
538
|
+
this._emergencyManager.registerTap();
|
|
539
|
+
}
|
|
476
540
|
|
|
477
|
-
|
|
541
|
+
/**
|
|
478
542
|
* Manually triggers data wipe.
|
|
479
543
|
* @returns {Promise<Object>} Wipe result
|
|
480
544
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
545
|
+
async wipeAllData() {
|
|
546
|
+
return await this._emergencyManager.wipeAllData();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// File Sharing Methods
|
|
551
|
+
// ============================================================================
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Sends a file to a specific peer.
|
|
555
|
+
* @param {string} peerId - Target peer ID
|
|
556
|
+
* @param {Object} fileInfo - File information
|
|
557
|
+
* @param {Uint8Array} fileInfo.data - File data
|
|
558
|
+
* @param {string} fileInfo.name - File name
|
|
559
|
+
* @param {string} [fileInfo.mimeType] - MIME type
|
|
560
|
+
* @returns {Object} Transfer info with id and event emitter
|
|
561
|
+
*/
|
|
562
|
+
async sendFile(peerId, fileInfo) {
|
|
563
|
+
this._validateRunning();
|
|
564
|
+
this._validatePeerId(peerId);
|
|
565
|
+
|
|
566
|
+
if (!fileInfo || !fileInfo.data || !fileInfo.name) {
|
|
567
|
+
throw new ValidationError('File must have data and name', 'E800');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const transfer = this._fileManager.prepareSend(peerId, fileInfo);
|
|
571
|
+
|
|
572
|
+
// Send offer (JSON is OK for metadata, but use binary type marker)
|
|
573
|
+
const offerJson = JSON.stringify(transfer.offer);
|
|
574
|
+
const offerBytes = new TextEncoder().encode(offerJson);
|
|
575
|
+
const offerPayload = new Uint8Array(1 + offerBytes.length);
|
|
576
|
+
offerPayload[0] = 0x01; // Binary type marker for OFFER
|
|
577
|
+
offerPayload.set(offerBytes, 1);
|
|
578
|
+
await this._service._sendRaw(peerId, offerPayload);
|
|
579
|
+
|
|
580
|
+
// Send chunks sequentially using binary protocol
|
|
581
|
+
for (let i = 0; i < transfer.chunks.length; i++) {
|
|
582
|
+
// Check if still running (handles app backgrounding)
|
|
583
|
+
if (this._state !== 'running') {
|
|
584
|
+
this._fileManager.cancelTransfer(transfer.id);
|
|
585
|
+
throw new MeshError('File transfer cancelled: network stopped', 'E900');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const chunk = transfer.chunks[i];
|
|
589
|
+
|
|
590
|
+
// Binary format: [type(1)] [transferIdLen(1)] [transferId(N)] [index(2)] [data(M)]
|
|
591
|
+
const transferIdBytes = new TextEncoder().encode(transfer.id);
|
|
592
|
+
const header = new Uint8Array(1 + 1 + transferIdBytes.length + 2);
|
|
593
|
+
let offset = 0;
|
|
594
|
+
header[offset++] = 0x02; // Binary type marker for CHUNK
|
|
595
|
+
header[offset++] = transferIdBytes.length;
|
|
596
|
+
header.set(transferIdBytes, offset);
|
|
597
|
+
offset += transferIdBytes.length;
|
|
598
|
+
header[offset++] = (chunk.index >> 8) & 0xFF;
|
|
599
|
+
header[offset++] = chunk.index & 0xFF;
|
|
600
|
+
|
|
601
|
+
// Combine header + chunk data
|
|
602
|
+
const payload = new Uint8Array(header.length + chunk.data.length);
|
|
603
|
+
payload.set(header, 0);
|
|
604
|
+
payload.set(chunk.data, header.length);
|
|
605
|
+
|
|
606
|
+
// Add per-chunk timeout (10 seconds)
|
|
607
|
+
const sendPromise = this._service._sendRaw(peerId, payload);
|
|
608
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
609
|
+
setTimeout(() => reject(new Error('Chunk send timeout')), 10000)
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await Promise.race([sendPromise, timeoutPromise]);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
this._fileManager.cancelTransfer(transfer.id);
|
|
616
|
+
this.emit('fileTransferFailed', {
|
|
617
|
+
id: transfer.id, name: fileInfo.name, error: err.message
|
|
618
|
+
});
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
this._fileManager.markChunkSent(transfer.id, i);
|
|
483
623
|
}
|
|
484
624
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// ============================================================================
|
|
625
|
+
return { id: transfer.id, name: fileInfo.name };
|
|
626
|
+
}
|
|
488
627
|
|
|
489
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Gets active file transfers
|
|
630
|
+
* @returns {Object} { outgoing: [], incoming: [] }
|
|
631
|
+
*/
|
|
632
|
+
getActiveTransfers() {
|
|
633
|
+
return this._fileManager.getActiveTransfers();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Cancels a file transfer
|
|
638
|
+
* @param {string} transferId - Transfer ID
|
|
639
|
+
*/
|
|
640
|
+
cancelTransfer(transferId) {
|
|
641
|
+
this._fileManager.cancelTransfer(transferId);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ============================================================================
|
|
645
|
+
// Connection Quality Methods
|
|
646
|
+
// ============================================================================
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Gets connection quality for a specific peer.
|
|
650
|
+
* @param {string} peerId - Peer ID
|
|
651
|
+
* @returns {Object|null} Quality report
|
|
652
|
+
*/
|
|
653
|
+
getConnectionQuality(peerId) {
|
|
654
|
+
return this._connectionQuality.getQuality(peerId);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Gets connection quality for all peers.
|
|
659
|
+
* @returns {Object[]} Array of quality reports
|
|
660
|
+
*/
|
|
661
|
+
getAllConnectionQuality() {
|
|
662
|
+
return this._connectionQuality.getAllQuality();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Status Methods
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
490
670
|
* Gets the network status.
|
|
491
671
|
* @returns {Object} Status
|
|
492
672
|
*/
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
673
|
+
getStatus() {
|
|
674
|
+
const identity = this._state === 'running' ? this._service.getIdentity() : null;
|
|
675
|
+
return {
|
|
676
|
+
state: this._state,
|
|
677
|
+
identity,
|
|
678
|
+
peers: this._state === 'running' ? this.getPeers().length : 0,
|
|
679
|
+
connectedPeers: this._state === 'running' ? this.getConnectedPeers().length : 0,
|
|
680
|
+
channels: this._state === 'running' ? this.getChannels().length : 0,
|
|
681
|
+
health: this._state === 'running' ? this.getNetworkHealth() : null,
|
|
682
|
+
batteryMode: this.getBatteryMode()
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
506
687
|
* Gets the identity information.
|
|
507
688
|
* @returns {Object} Identity
|
|
508
689
|
*/
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
690
|
+
getIdentity() {
|
|
691
|
+
return this._service.getIdentity();
|
|
692
|
+
}
|
|
512
693
|
|
|
513
|
-
|
|
694
|
+
/**
|
|
514
695
|
* Sets the display name/nickname.
|
|
515
696
|
* @param {string} nickname - New nickname
|
|
516
697
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
698
|
+
setNickname(nickname) {
|
|
699
|
+
this._config.nickname = nickname;
|
|
700
|
+
this._service.setDisplayName(nickname);
|
|
701
|
+
}
|
|
521
702
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
703
|
+
// ============================================================================
|
|
704
|
+
// Private Methods
|
|
705
|
+
// ============================================================================
|
|
525
706
|
|
|
526
|
-
|
|
707
|
+
/**
|
|
527
708
|
* Merges configuration with defaults.
|
|
528
709
|
* @param {Object} defaults - Default config
|
|
529
710
|
* @param {Object} custom - Custom config
|
|
530
711
|
* @returns {Object} Merged config
|
|
531
712
|
* @private
|
|
532
713
|
*/
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
714
|
+
_mergeConfig(defaults, custom) {
|
|
715
|
+
return {
|
|
716
|
+
...defaults,
|
|
717
|
+
...custom,
|
|
718
|
+
encryption: { ...defaults.encryption, ...custom.encryption },
|
|
719
|
+
routing: { ...defaults.routing, ...custom.routing },
|
|
720
|
+
compression: { ...defaults.compression, ...custom.compression },
|
|
721
|
+
storeAndForward: { ...defaults.storeAndForward, ...custom.storeAndForward },
|
|
722
|
+
fileTransfer: { ...defaults.fileTransfer, ...custom.fileTransfer },
|
|
723
|
+
qualityConfig: { ...defaults.qualityConfig, ...custom.qualityConfig }
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
545
728
|
* Sets up event forwarding from underlying services.
|
|
546
729
|
* @private
|
|
547
730
|
*/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
731
|
+
_setupEventForwarding() {
|
|
732
|
+
// Forward service events with PRD-style naming
|
|
733
|
+
this._service.on(EVENTS.PEER_DISCOVERED, (peer) => {
|
|
734
|
+
this._monitor.trackPeerDiscovered(peer.id);
|
|
735
|
+
this.emit('peerDiscovered', peer);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
this._service.on(EVENTS.PEER_CONNECTED, (peer) => {
|
|
739
|
+
if (peer.rssi) {
|
|
740
|
+
this._connectionQuality.recordRssi(peer.id, peer.rssi);
|
|
741
|
+
}
|
|
742
|
+
this.emit('peerConnected', peer);
|
|
743
|
+
// Deliver cached messages
|
|
744
|
+
if (this._storeForward && this._storeForward.hasCachedMessages(peer.id)) {
|
|
745
|
+
this._deliverCachedMessages(peer.id);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
this._service.on(EVENTS.PEER_DISCONNECTED, (peer) => {
|
|
750
|
+
this._monitor.trackPeerDisconnected(peer.id);
|
|
751
|
+
this._connectionQuality.removePeer(peer.id);
|
|
752
|
+
this.emit('peerDisconnected', peer);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
this._service.on('message', (message) => {
|
|
756
|
+
this._monitor.trackMessageReceived(message.senderId);
|
|
757
|
+
this.emit('messageReceived', {
|
|
758
|
+
from: message.senderId,
|
|
759
|
+
text: message.content,
|
|
760
|
+
timestamp: message.timestamp,
|
|
761
|
+
type: message.type
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
this._service.on('private-message', (message) => {
|
|
766
|
+
this.emit('directMessage', {
|
|
767
|
+
from: message.senderId,
|
|
768
|
+
text: message.content,
|
|
769
|
+
timestamp: message.timestamp
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
this._service.on('channel-message', (message) => {
|
|
774
|
+
this.emit('channelMessage', {
|
|
775
|
+
channel: message.channelId,
|
|
776
|
+
from: message.senderId,
|
|
777
|
+
text: message.content,
|
|
778
|
+
timestamp: message.timestamp
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
this._service.on('message-delivered', (info) => {
|
|
783
|
+
this._monitor.trackMessageDelivered(info.messageId);
|
|
784
|
+
this.emit('messageDelivered', info);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
this._service.on('error', (error) => {
|
|
788
|
+
this.emit('error', error);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Forward monitor events
|
|
792
|
+
this._monitor.on('health-changed', (info) => {
|
|
793
|
+
this.emit('networkHealthChanged', info);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Forward emergency events
|
|
797
|
+
this._emergencyManager.on('panic-wipe-completed', (result) => {
|
|
798
|
+
this.emit('dataWiped', result);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
616
803
|
* Sets up store and forward integration.
|
|
617
804
|
* @private
|
|
618
805
|
*/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
806
|
+
_setupStoreAndForward() {
|
|
807
|
+
// Nothing extra needed - handled in peerConnected event
|
|
808
|
+
}
|
|
622
809
|
|
|
623
|
-
|
|
810
|
+
/**
|
|
624
811
|
* Delivers cached messages to a peer.
|
|
625
812
|
* @param {string} peerId - Peer ID
|
|
626
813
|
* @private
|
|
627
814
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
815
|
+
async _deliverCachedMessages(peerId) {
|
|
816
|
+
if (!this._storeForward) { return; }
|
|
630
817
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
818
|
+
const sendFn = async (payload) => {
|
|
819
|
+
// Re-encrypt and send via the proper encrypted channel
|
|
820
|
+
try {
|
|
821
|
+
const text = new TextDecoder().decode(payload);
|
|
822
|
+
await this._service.sendPrivateMessage(peerId, text);
|
|
823
|
+
} catch (e) {
|
|
824
|
+
// Fallback to raw send if encryption fails
|
|
825
|
+
await this._service._sendRaw(peerId, payload);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
634
828
|
|
|
635
|
-
|
|
829
|
+
const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
|
|
636
830
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
831
|
+
if (result.delivered > 0) {
|
|
832
|
+
this.emit('cachedMessagesDelivered', {
|
|
833
|
+
peerId,
|
|
834
|
+
delivered: result.delivered
|
|
835
|
+
});
|
|
643
836
|
}
|
|
837
|
+
}
|
|
644
838
|
|
|
645
|
-
|
|
839
|
+
/**
|
|
646
840
|
* Registers data clearers for panic mode.
|
|
647
841
|
* @private
|
|
648
842
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// Clear store and forward cache
|
|
656
|
-
if (this._storeForward) {
|
|
657
|
-
this._emergencyManager.registerClearer(async () => {
|
|
658
|
-
this._storeForward.clear();
|
|
659
|
-
});
|
|
660
|
-
}
|
|
843
|
+
_registerPanicClearers() {
|
|
844
|
+
// Clear service data
|
|
845
|
+
this._emergencyManager.registerClearer(async () => {
|
|
846
|
+
await this._service.destroy();
|
|
847
|
+
});
|
|
661
848
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
849
|
+
// Clear store and forward cache
|
|
850
|
+
if (this._storeForward) {
|
|
851
|
+
this._emergencyManager.registerClearer(async () => {
|
|
852
|
+
this._storeForward.clear();
|
|
853
|
+
});
|
|
666
854
|
}
|
|
667
855
|
|
|
668
|
-
|
|
856
|
+
// Clear monitor data
|
|
857
|
+
this._emergencyManager.registerClearer(() => {
|
|
858
|
+
this._monitor.reset();
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
669
863
|
* Validates that network is running.
|
|
670
864
|
* @throws {MeshError} If not running
|
|
671
865
|
* @private
|
|
672
866
|
*/
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
867
|
+
_validateRunning() {
|
|
868
|
+
if (this._state !== 'running') {
|
|
869
|
+
throw new MeshError(
|
|
870
|
+
'MeshNetwork is not running. Call start() first.',
|
|
871
|
+
'E900',
|
|
872
|
+
{ state: this._state }
|
|
873
|
+
);
|
|
681
874
|
}
|
|
875
|
+
}
|
|
682
876
|
|
|
683
|
-
|
|
877
|
+
/**
|
|
684
878
|
* Checks if a peer is offline.
|
|
685
879
|
* @param {string} peerId - Peer ID
|
|
686
880
|
* @returns {boolean} True if offline
|
|
687
881
|
* @private
|
|
688
882
|
*/
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
883
|
+
_isPeerOffline(peerId) {
|
|
884
|
+
const peer = this._service.getPeer(peerId);
|
|
885
|
+
return !peer || !peer.isConnected;
|
|
886
|
+
}
|
|
693
887
|
|
|
694
|
-
|
|
888
|
+
/**
|
|
695
889
|
* Encodes a message for storage.
|
|
696
890
|
* @param {string} text - Message text
|
|
697
891
|
* @returns {Uint8Array} Encoded payload
|
|
698
892
|
* @private
|
|
699
893
|
*/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
894
|
+
_encodeMessage(text) {
|
|
895
|
+
return new TextEncoder().encode(text);
|
|
896
|
+
}
|
|
703
897
|
|
|
704
|
-
|
|
898
|
+
/**
|
|
705
899
|
* Normalizes channel name (adds # if missing).
|
|
706
900
|
* @param {string} name - Channel name
|
|
707
901
|
* @returns {string} Normalized name
|
|
708
902
|
* @private
|
|
709
903
|
*/
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
904
|
+
_normalizeChannelName(name) {
|
|
905
|
+
return name.startsWith('#') ? name : `#${name}`;
|
|
906
|
+
}
|
|
713
907
|
|
|
714
|
-
|
|
908
|
+
/**
|
|
715
909
|
* Maximum allowed message size in bytes (1MB).
|
|
716
910
|
* @constant {number}
|
|
717
911
|
* @private
|
|
718
912
|
*/
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
913
|
+
static get MAX_MESSAGE_SIZE() {
|
|
914
|
+
return 1024 * 1024;
|
|
915
|
+
}
|
|
722
916
|
|
|
723
|
-
|
|
917
|
+
/**
|
|
724
918
|
* Validates message text input.
|
|
725
919
|
* @param {string} text - Message text to validate
|
|
726
920
|
* @throws {ValidationError} If text is invalid
|
|
727
921
|
* @private
|
|
728
922
|
*/
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (typeof text !== 'string') {
|
|
735
|
-
throw ValidationError.invalidType('text', text, 'string');
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (text.length === 0) {
|
|
739
|
-
throw ValidationError.invalidArgument('text', text, {
|
|
740
|
-
reason: 'Message text cannot be empty',
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Check size limit (UTF-8 encoded)
|
|
745
|
-
const byteLength = new TextEncoder().encode(text).length;
|
|
746
|
-
if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
|
|
747
|
-
throw ValidationError.outOfRange('text', byteLength, {
|
|
748
|
-
min: 1,
|
|
749
|
-
max: MeshNetwork.MAX_MESSAGE_SIZE,
|
|
750
|
-
});
|
|
751
|
-
}
|
|
923
|
+
_validateMessageText(text) {
|
|
924
|
+
if (text === null || text === undefined) {
|
|
925
|
+
throw ValidationError.missingArgument('text');
|
|
752
926
|
}
|
|
753
927
|
|
|
754
|
-
|
|
928
|
+
if (typeof text !== 'string') {
|
|
929
|
+
throw ValidationError.invalidType('text', text, 'string');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (text.length === 0) {
|
|
933
|
+
throw ValidationError.invalidArgument('text', text, {
|
|
934
|
+
reason: 'Message text cannot be empty'
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Check size limit (UTF-8 encoded)
|
|
939
|
+
const byteLength = new TextEncoder().encode(text).length;
|
|
940
|
+
if (byteLength > MeshNetwork.MAX_MESSAGE_SIZE) {
|
|
941
|
+
throw ValidationError.outOfRange('text', byteLength, {
|
|
942
|
+
min: 1,
|
|
943
|
+
max: MeshNetwork.MAX_MESSAGE_SIZE
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
755
949
|
* Validates peer ID input.
|
|
756
950
|
* @param {string} peerId - Peer ID to validate
|
|
757
951
|
* @throws {ValidationError} If peerId is invalid
|
|
758
952
|
* @private
|
|
759
953
|
*/
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
954
|
+
_validatePeerId(peerId) {
|
|
955
|
+
if (peerId === null || peerId === undefined) {
|
|
956
|
+
throw ValidationError.missingArgument('peerId');
|
|
957
|
+
}
|
|
764
958
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
959
|
+
if (typeof peerId !== 'string') {
|
|
960
|
+
throw ValidationError.invalidType('peerId', peerId, 'string');
|
|
961
|
+
}
|
|
768
962
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
963
|
+
if (peerId.trim().length === 0) {
|
|
964
|
+
throw ValidationError.invalidArgument('peerId', peerId, {
|
|
965
|
+
reason: 'Peer ID cannot be empty or whitespace only'
|
|
966
|
+
});
|
|
774
967
|
}
|
|
968
|
+
}
|
|
775
969
|
}
|
|
776
970
|
|
|
777
971
|
// Export convenience constants
|
|
@@ -780,8 +974,8 @@ MeshNetwork.PanicTrigger = PANIC_TRIGGER;
|
|
|
780
974
|
MeshNetwork.HealthStatus = HEALTH_STATUS;
|
|
781
975
|
|
|
782
976
|
module.exports = {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
977
|
+
MeshNetwork,
|
|
978
|
+
BATTERY_MODE,
|
|
979
|
+
PANIC_TRIGGER,
|
|
980
|
+
HEALTH_STATUS
|
|
787
981
|
};
|