react-native-ble-mesh 1.0.4 → 1.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/package.json +1 -1
- package/src/MeshNetwork.js +787 -0
- package/src/index.d.ts +386 -4
- package/src/index.js +56 -3
- package/src/mesh/index.js +5 -1
- package/src/mesh/monitor/NetworkMonitor.js +543 -0
- package/src/mesh/monitor/index.js +14 -0
- package/src/mesh/store/StoreAndForwardManager.js +476 -0
- package/src/mesh/store/index.js +12 -0
- package/src/service/BatteryOptimizer.js +497 -0
- package/src/service/EmergencyManager.js +370 -0
- package/src/service/index.js +10 -0
- package/src/utils/compression.js +456 -0
- package/src/utils/index.js +9 -1
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview MeshNetwork - Simplified High-Level API
|
|
5
|
+
* @module MeshNetwork
|
|
6
|
+
*
|
|
7
|
+
* Primary entry point for react-native-ble-mesh as specified in the PRD.
|
|
8
|
+
* Provides a developer-friendly API for BitChat-compatible mesh networking.
|
|
9
|
+
*
|
|
10
|
+
* Target: Setup-to-first-message in <15 minutes, <10 lines for basic messaging.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const EventEmitter = require('./utils/EventEmitter');
|
|
14
|
+
const { MeshService } = require('./service');
|
|
15
|
+
const { BLETransport, MockTransport } = require('./transport');
|
|
16
|
+
const { MemoryStorage } = require('./storage');
|
|
17
|
+
const { StoreAndForwardManager } = require('./mesh/store');
|
|
18
|
+
const { NetworkMonitor, HEALTH_STATUS } = require('./mesh/monitor');
|
|
19
|
+
const { BatteryOptimizer, BATTERY_MODE } = require('./service/BatteryOptimizer');
|
|
20
|
+
const { EmergencyManager, PANIC_TRIGGER } = require('./service/EmergencyManager');
|
|
21
|
+
const { MessageCompressor } = require('./utils/compression');
|
|
22
|
+
const { EVENTS } = require('./constants');
|
|
23
|
+
const { ValidationError, MeshError } = require('./errors');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default MeshNetwork configuration
|
|
27
|
+
* @constant {Object}
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
30
|
+
/** Display name for this node */
|
|
31
|
+
nickname: 'Anonymous',
|
|
32
|
+
/** Battery mode: 'high' | 'balanced' | 'low' | 'auto' */
|
|
33
|
+
batteryMode: BATTERY_MODE.BALANCED,
|
|
34
|
+
/** Encryption settings */
|
|
35
|
+
encryption: {
|
|
36
|
+
level: 'standard',
|
|
37
|
+
rotateKeysAfter: 1000,
|
|
38
|
+
},
|
|
39
|
+
/** Routing configuration */
|
|
40
|
+
routing: {
|
|
41
|
+
maxHops: 7,
|
|
42
|
+
bloomFilterSize: 10000,
|
|
43
|
+
},
|
|
44
|
+
/** Compression settings */
|
|
45
|
+
compression: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
threshold: 100,
|
|
48
|
+
},
|
|
49
|
+
/** Store and forward settings */
|
|
50
|
+
storeAndForward: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
retentionHours: 24,
|
|
53
|
+
maxCachedMessages: 1000,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* MeshNetwork - High-level API for BitChat-compatible mesh networking.
|
|
59
|
+
*
|
|
60
|
+
* @class MeshNetwork
|
|
61
|
+
* @extends EventEmitter
|
|
62
|
+
* @example
|
|
63
|
+
* // Basic Setup (Minimal)
|
|
64
|
+
* const mesh = new MeshNetwork({
|
|
65
|
+
* nickname: 'Alice',
|
|
66
|
+
* batteryMode: 'balanced',
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* await mesh.start();
|
|
70
|
+
* await mesh.broadcast('Hello mesh!');
|
|
71
|
+
*
|
|
72
|
+
* mesh.on('messageReceived', (message) => {
|
|
73
|
+
* console.log(`${message.from}: ${message.text}`);
|
|
74
|
+
* });
|
|
75
|
+
*/
|
|
76
|
+
class MeshNetwork extends EventEmitter {
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new MeshNetwork instance.
|
|
79
|
+
* @param {Object} [config={}] - Network configuration
|
|
80
|
+
* @param {string} [config.nickname='Anonymous'] - Display name
|
|
81
|
+
* @param {string} [config.batteryMode='balanced'] - Battery mode
|
|
82
|
+
* @param {Object} [config.encryption] - Encryption settings
|
|
83
|
+
* @param {Object} [config.routing] - Routing settings
|
|
84
|
+
* @param {Object} [config.compression] - Compression settings
|
|
85
|
+
* @param {Object} [config.storeAndForward] - Store and forward settings
|
|
86
|
+
*/
|
|
87
|
+
constructor(config = {}) {
|
|
88
|
+
super();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Configuration
|
|
92
|
+
* @type {Object}
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
this._config = this._mergeConfig(DEFAULT_CONFIG, config);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Underlying MeshService
|
|
99
|
+
* @type {MeshService}
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
this._service = new MeshService({
|
|
103
|
+
displayName: this._config.nickname,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Transport layer
|
|
108
|
+
* @type {Transport|null}
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
this._transport = null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Store and forward manager
|
|
115
|
+
* @type {StoreAndForwardManager|null}
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
this._storeForward = this._config.storeAndForward.enabled
|
|
119
|
+
? new StoreAndForwardManager({
|
|
120
|
+
retentionMs: this._config.storeAndForward.retentionHours * 60 * 60 * 1000,
|
|
121
|
+
maxTotalMessages: this._config.storeAndForward.maxCachedMessages,
|
|
122
|
+
})
|
|
123
|
+
: null;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Network monitor
|
|
127
|
+
* @type {NetworkMonitor}
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
this._monitor = new NetworkMonitor();
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Battery optimizer
|
|
134
|
+
* @type {BatteryOptimizer}
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
this._batteryOptimizer = new BatteryOptimizer({
|
|
138
|
+
initialMode: this._config.batteryMode,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Emergency manager
|
|
143
|
+
* @type {EmergencyManager}
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
this._emergencyManager = new EmergencyManager();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Message compressor
|
|
150
|
+
* @type {MessageCompressor}
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
this._compressor = new MessageCompressor({
|
|
154
|
+
threshold: this._config.compression.threshold,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Channel manager reference
|
|
159
|
+
* @type {Object|null}
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
this._channels = null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Network state
|
|
166
|
+
* @type {string}
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
this._state = 'stopped';
|
|
170
|
+
|
|
171
|
+
// Setup event forwarding
|
|
172
|
+
this._setupEventForwarding();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Lifecycle Methods
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Starts the mesh network.
|
|
181
|
+
* @param {Object} [transport] - Optional custom transport
|
|
182
|
+
* @returns {Promise<void>}
|
|
183
|
+
*/
|
|
184
|
+
async start(transport) {
|
|
185
|
+
if (this._state === 'running') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create transport if not provided
|
|
190
|
+
this._transport = transport || new BLETransport();
|
|
191
|
+
|
|
192
|
+
// Initialize the service
|
|
193
|
+
await this._service.initialize({
|
|
194
|
+
storage: new MemoryStorage(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Connect battery optimizer to transport
|
|
198
|
+
this._batteryOptimizer.setTransport(this._transport);
|
|
199
|
+
await this._batteryOptimizer.setMode(this._config.batteryMode);
|
|
200
|
+
|
|
201
|
+
// Register data clearers for panic mode
|
|
202
|
+
this._registerPanicClearers();
|
|
203
|
+
|
|
204
|
+
// Start the service
|
|
205
|
+
await this._service.start(this._transport);
|
|
206
|
+
|
|
207
|
+
// Setup store and forward
|
|
208
|
+
if (this._storeForward) {
|
|
209
|
+
this._setupStoreAndForward();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this._state = 'running';
|
|
213
|
+
this.emit('started');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Stops the mesh network.
|
|
218
|
+
* @returns {Promise<void>}
|
|
219
|
+
*/
|
|
220
|
+
async stop() {
|
|
221
|
+
if (this._state !== 'running') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await this._service.stop();
|
|
226
|
+
this._state = 'stopped';
|
|
227
|
+
this.emit('stopped');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Destroys the mesh network and cleans up resources.
|
|
232
|
+
* @returns {Promise<void>}
|
|
233
|
+
*/
|
|
234
|
+
async destroy() {
|
|
235
|
+
await this.stop();
|
|
236
|
+
await this._service.destroy();
|
|
237
|
+
|
|
238
|
+
if (this._storeForward) {
|
|
239
|
+
this._storeForward.destroy();
|
|
240
|
+
}
|
|
241
|
+
this._monitor.destroy();
|
|
242
|
+
this._batteryOptimizer.destroy();
|
|
243
|
+
this._emergencyManager.destroy();
|
|
244
|
+
|
|
245
|
+
this.removeAllListeners();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Messaging Methods
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Broadcasts a message to all peers.
|
|
254
|
+
* @param {string} text - Message text
|
|
255
|
+
* @returns {Promise<string>} Message ID
|
|
256
|
+
* @throws {Error} If text is invalid
|
|
257
|
+
*/
|
|
258
|
+
async broadcast(text) {
|
|
259
|
+
this._validateRunning();
|
|
260
|
+
this._validateMessageText(text);
|
|
261
|
+
|
|
262
|
+
const messageId = await this._service.sendBroadcast(text);
|
|
263
|
+
this._monitor.trackMessageSent('broadcast', messageId);
|
|
264
|
+
|
|
265
|
+
return messageId;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Sends a direct encrypted message to a specific peer.
|
|
270
|
+
* @param {string} peerId - Target peer ID or nickname
|
|
271
|
+
* @param {string} text - Message text
|
|
272
|
+
* @returns {Promise<string>} Message ID
|
|
273
|
+
* @throws {Error} If peerId or text is invalid
|
|
274
|
+
*/
|
|
275
|
+
async sendDirect(peerId, text) {
|
|
276
|
+
this._validateRunning();
|
|
277
|
+
this._validatePeerId(peerId);
|
|
278
|
+
this._validateMessageText(text);
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const messageId = await this._service.sendPrivateMessage(peerId, text);
|
|
282
|
+
this._monitor.trackMessageSent(peerId, messageId);
|
|
283
|
+
return messageId;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// If peer is offline and store-forward is enabled, cache the message
|
|
286
|
+
if (this._storeForward && this._isPeerOffline(peerId)) {
|
|
287
|
+
const payload = this._encodeMessage(text);
|
|
288
|
+
await this._storeForward.cacheForOfflinePeer(peerId, payload);
|
|
289
|
+
this.emit('messageCached', { peerId, text });
|
|
290
|
+
return 'cached';
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Channel Methods
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Joins a channel (IRC-style topic-based group chat).
|
|
302
|
+
* @param {string} channelName - Channel name (e.g., '#general')
|
|
303
|
+
* @param {string} [password] - Optional password
|
|
304
|
+
* @returns {Promise<void>}
|
|
305
|
+
*/
|
|
306
|
+
async joinChannel(channelName, password) {
|
|
307
|
+
this._validateRunning();
|
|
308
|
+
|
|
309
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
310
|
+
await this._service.joinChannel(normalized, password);
|
|
311
|
+
|
|
312
|
+
this.emit('channelJoined', { channel: normalized });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Leaves a channel.
|
|
317
|
+
* @param {string} channelName - Channel name
|
|
318
|
+
* @returns {Promise<void>}
|
|
319
|
+
*/
|
|
320
|
+
async leaveChannel(channelName) {
|
|
321
|
+
this._validateRunning();
|
|
322
|
+
|
|
323
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
324
|
+
await this._service.leaveChannel(normalized);
|
|
325
|
+
|
|
326
|
+
this.emit('channelLeft', { channel: normalized });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Sends a message to a channel.
|
|
331
|
+
* @param {string} channelName - Channel name
|
|
332
|
+
* @param {string} text - Message text
|
|
333
|
+
* @returns {Promise<string>} Message ID
|
|
334
|
+
*/
|
|
335
|
+
async sendToChannel(channelName, text) {
|
|
336
|
+
this._validateRunning();
|
|
337
|
+
|
|
338
|
+
const normalized = this._normalizeChannelName(channelName);
|
|
339
|
+
return await this._service.sendChannelMessage(normalized, text);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Gets list of joined channels.
|
|
344
|
+
* @returns {Object[]} Channels
|
|
345
|
+
*/
|
|
346
|
+
getChannels() {
|
|
347
|
+
return this._service.getChannels();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================================================
|
|
351
|
+
// Peer Methods
|
|
352
|
+
// ============================================================================
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Gets all known peers.
|
|
356
|
+
* @returns {Object[]} Array of peers
|
|
357
|
+
*/
|
|
358
|
+
getPeers() {
|
|
359
|
+
return this._service.getPeers();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Gets connected peers.
|
|
364
|
+
* @returns {Object[]} Connected peers
|
|
365
|
+
*/
|
|
366
|
+
getConnectedPeers() {
|
|
367
|
+
return this._service.getConnectedPeers();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Gets peers with secure sessions.
|
|
372
|
+
* @returns {Object[]} Secured peers
|
|
373
|
+
*/
|
|
374
|
+
getSecuredPeers() {
|
|
375
|
+
return this._service.getSecuredPeers();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Blocks a peer.
|
|
380
|
+
* @param {string} peerId - Peer ID
|
|
381
|
+
*/
|
|
382
|
+
blockPeer(peerId) {
|
|
383
|
+
this._service.blockPeer(peerId);
|
|
384
|
+
this.emit('peerBlocked', { peerId });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Unblocks a peer.
|
|
389
|
+
* @param {string} peerId - Peer ID
|
|
390
|
+
*/
|
|
391
|
+
unblockPeer(peerId) {
|
|
392
|
+
this._service.unblockPeer(peerId);
|
|
393
|
+
this.emit('peerUnblocked', { peerId });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ============================================================================
|
|
397
|
+
// Network Health Methods
|
|
398
|
+
// ============================================================================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Gets network health metrics.
|
|
402
|
+
* @returns {Object} Health report
|
|
403
|
+
*/
|
|
404
|
+
getNetworkHealth() {
|
|
405
|
+
return this._monitor.generateHealthReport();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Gets detailed health for a specific peer.
|
|
410
|
+
* @param {string} peerId - Peer ID
|
|
411
|
+
* @returns {Object|null} Node health
|
|
412
|
+
*/
|
|
413
|
+
getPeerHealth(peerId) {
|
|
414
|
+
return this._monitor.getNodeHealth(peerId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Battery Management
|
|
419
|
+
// ============================================================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Sets the battery mode.
|
|
423
|
+
* @param {string} mode - 'high' | 'balanced' | 'low' | 'auto'
|
|
424
|
+
* @returns {Promise<void>}
|
|
425
|
+
*/
|
|
426
|
+
async setBatteryMode(mode) {
|
|
427
|
+
await this._batteryOptimizer.setMode(mode);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Gets the current battery mode.
|
|
432
|
+
* @returns {string} Current mode
|
|
433
|
+
*/
|
|
434
|
+
getBatteryMode() {
|
|
435
|
+
return this._batteryOptimizer.getMode();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Updates battery level (for auto mode).
|
|
440
|
+
* @param {number} level - Battery level (0-100)
|
|
441
|
+
* @param {boolean} [charging=false] - Whether charging
|
|
442
|
+
*/
|
|
443
|
+
updateBatteryLevel(level, charging = false) {
|
|
444
|
+
this._batteryOptimizer.updateBatteryLevel(level, charging);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Security Methods
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Enables panic mode for emergency data wipe.
|
|
453
|
+
* @param {Object} [options={}] - Panic mode options
|
|
454
|
+
* @param {string} [options.trigger='triple_tap'] - Trigger type
|
|
455
|
+
* @param {Function} [options.onWipe] - Callback after wipe
|
|
456
|
+
*/
|
|
457
|
+
enablePanicMode(options = {}) {
|
|
458
|
+
this._emergencyManager.enablePanicMode(options);
|
|
459
|
+
this.emit('panicModeEnabled');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Disables panic mode.
|
|
464
|
+
*/
|
|
465
|
+
disablePanicMode() {
|
|
466
|
+
this._emergencyManager.disablePanicMode();
|
|
467
|
+
this.emit('panicModeDisabled');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Registers a tap for panic mode detection.
|
|
472
|
+
*/
|
|
473
|
+
registerPanicTap() {
|
|
474
|
+
this._emergencyManager.registerTap();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Manually triggers data wipe.
|
|
479
|
+
* @returns {Promise<Object>} Wipe result
|
|
480
|
+
*/
|
|
481
|
+
async wipeAllData() {
|
|
482
|
+
return await this._emergencyManager.wipeAllData();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Status Methods
|
|
487
|
+
// ============================================================================
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Gets the network status.
|
|
491
|
+
* @returns {Object} Status
|
|
492
|
+
*/
|
|
493
|
+
getStatus() {
|
|
494
|
+
return {
|
|
495
|
+
state: this._state,
|
|
496
|
+
identity: this._service.getIdentity(),
|
|
497
|
+
peers: this.getPeers().length,
|
|
498
|
+
connectedPeers: this.getConnectedPeers().length,
|
|
499
|
+
channels: this.getChannels().length,
|
|
500
|
+
health: this.getNetworkHealth(),
|
|
501
|
+
batteryMode: this.getBatteryMode(),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Gets the identity information.
|
|
507
|
+
* @returns {Object} Identity
|
|
508
|
+
*/
|
|
509
|
+
getIdentity() {
|
|
510
|
+
return this._service.getIdentity();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Sets the display name/nickname.
|
|
515
|
+
* @param {string} nickname - New nickname
|
|
516
|
+
*/
|
|
517
|
+
setNickname(nickname) {
|
|
518
|
+
this._config.nickname = nickname;
|
|
519
|
+
this._service.setDisplayName(nickname);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// Private Methods
|
|
524
|
+
// ============================================================================
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Merges configuration with defaults.
|
|
528
|
+
* @param {Object} defaults - Default config
|
|
529
|
+
* @param {Object} custom - Custom config
|
|
530
|
+
* @returns {Object} Merged config
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
_mergeConfig(defaults, custom) {
|
|
534
|
+
return {
|
|
535
|
+
...defaults,
|
|
536
|
+
...custom,
|
|
537
|
+
encryption: { ...defaults.encryption, ...custom.encryption },
|
|
538
|
+
routing: { ...defaults.routing, ...custom.routing },
|
|
539
|
+
compression: { ...defaults.compression, ...custom.compression },
|
|
540
|
+
storeAndForward: { ...defaults.storeAndForward, ...custom.storeAndForward },
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Sets up event forwarding from underlying services.
|
|
546
|
+
* @private
|
|
547
|
+
*/
|
|
548
|
+
_setupEventForwarding() {
|
|
549
|
+
// Forward service events with PRD-style naming
|
|
550
|
+
this._service.on('peer-discovered', (peer) => {
|
|
551
|
+
this._monitor.trackPeerDiscovered(peer.id);
|
|
552
|
+
this.emit('peerDiscovered', peer);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
this._service.on('peer-connected', (peer) => {
|
|
556
|
+
this.emit('peerConnected', peer);
|
|
557
|
+
// Deliver cached messages
|
|
558
|
+
if (this._storeForward && this._storeForward.hasCachedMessages(peer.id)) {
|
|
559
|
+
this._deliverCachedMessages(peer.id);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
this._service.on('peer-disconnected', (peer) => {
|
|
564
|
+
this._monitor.trackPeerDisconnected(peer.id);
|
|
565
|
+
this.emit('peerDisconnected', peer);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
this._service.on('message', (message) => {
|
|
569
|
+
this._monitor.trackMessageReceived(message.senderId);
|
|
570
|
+
this.emit('messageReceived', {
|
|
571
|
+
from: message.senderId,
|
|
572
|
+
text: message.content,
|
|
573
|
+
timestamp: message.timestamp,
|
|
574
|
+
type: message.type,
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
this._service.on('private-message', (message) => {
|
|
579
|
+
this.emit('directMessage', {
|
|
580
|
+
from: message.senderId,
|
|
581
|
+
text: message.content,
|
|
582
|
+
timestamp: message.timestamp,
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
this._service.on('channel-message', (message) => {
|
|
587
|
+
this.emit('channelMessage', {
|
|
588
|
+
channel: message.channelId,
|
|
589
|
+
from: message.senderId,
|
|
590
|
+
text: message.content,
|
|
591
|
+
timestamp: message.timestamp,
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
this._service.on('message-delivered', (info) => {
|
|
596
|
+
this._monitor.trackMessageDelivered(info.messageId);
|
|
597
|
+
this.emit('messageDelivered', info);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
this._service.on('error', (error) => {
|
|
601
|
+
this.emit('error', error);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Forward monitor events
|
|
605
|
+
this._monitor.on('health-changed', (info) => {
|
|
606
|
+
this.emit('networkHealthChanged', info);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Forward emergency events
|
|
610
|
+
this._emergencyManager.on('panic-wipe-completed', (result) => {
|
|
611
|
+
this.emit('dataWiped', result);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Sets up store and forward integration.
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
_setupStoreAndForward() {
|
|
620
|
+
// Nothing extra needed - handled in peerConnected event
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Delivers cached messages to a peer.
|
|
625
|
+
* @param {string} peerId - Peer ID
|
|
626
|
+
* @private
|
|
627
|
+
*/
|
|
628
|
+
async _deliverCachedMessages(peerId) {
|
|
629
|
+
if (!this._storeForward) return;
|
|
630
|
+
|
|
631
|
+
const sendFn = async (payload) => {
|
|
632
|
+
await this._service._sendRaw(peerId, payload);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const result = await this._storeForward.deliverCachedMessages(peerId, sendFn);
|
|
636
|
+
|
|
637
|
+
if (result.delivered > 0) {
|
|
638
|
+
this.emit('cachedMessagesDelivered', {
|
|
639
|
+
peerId,
|
|
640
|
+
delivered: result.delivered,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Registers data clearers for panic mode.
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
_registerPanicClearers() {
|
|
650
|
+
// Clear service data
|
|
651
|
+
this._emergencyManager.registerClearer(async () => {
|
|
652
|
+
await this._service.destroy();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Clear store and forward cache
|
|
656
|
+
if (this._storeForward) {
|
|
657
|
+
this._emergencyManager.registerClearer(async () => {
|
|
658
|
+
this._storeForward.clear();
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Clear monitor data
|
|
663
|
+
this._emergencyManager.registerClearer(() => {
|
|
664
|
+
this._monitor.reset();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Validates that network is running.
|
|
670
|
+
* @throws {MeshError} If not running
|
|
671
|
+
* @private
|
|
672
|
+
*/
|
|
673
|
+
_validateRunning() {
|
|
674
|
+
if (this._state !== 'running') {
|
|
675
|
+
throw new MeshError(
|
|
676
|
+
'MeshNetwork is not running. Call start() first.',
|
|
677
|
+
'E900',
|
|
678
|
+
{ state: this._state }
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Checks if a peer is offline.
|
|
685
|
+
* @param {string} peerId - Peer ID
|
|
686
|
+
* @returns {boolean} True if offline
|
|
687
|
+
* @private
|
|
688
|
+
*/
|
|
689
|
+
_isPeerOffline(peerId) {
|
|
690
|
+
const peer = this._service.getPeer(peerId);
|
|
691
|
+
return !peer || !peer.isConnected;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Encodes a message for storage.
|
|
696
|
+
* @param {string} text - Message text
|
|
697
|
+
* @returns {Uint8Array} Encoded payload
|
|
698
|
+
* @private
|
|
699
|
+
*/
|
|
700
|
+
_encodeMessage(text) {
|
|
701
|
+
return new TextEncoder().encode(text);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Normalizes channel name (adds # if missing).
|
|
706
|
+
* @param {string} name - Channel name
|
|
707
|
+
* @returns {string} Normalized name
|
|
708
|
+
* @private
|
|
709
|
+
*/
|
|
710
|
+
_normalizeChannelName(name) {
|
|
711
|
+
return name.startsWith('#') ? name : `#${name}`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Maximum allowed message size in bytes (1MB).
|
|
716
|
+
* @constant {number}
|
|
717
|
+
* @private
|
|
718
|
+
*/
|
|
719
|
+
static get MAX_MESSAGE_SIZE() {
|
|
720
|
+
return 1024 * 1024;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Validates message text input.
|
|
725
|
+
* @param {string} text - Message text to validate
|
|
726
|
+
* @throws {ValidationError} If text is invalid
|
|
727
|
+
* @private
|
|
728
|
+
*/
|
|
729
|
+
_validateMessageText(text) {
|
|
730
|
+
if (text === null || text === undefined) {
|
|
731
|
+
throw ValidationError.missingArgument('text');
|
|
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
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Validates peer ID input.
|
|
756
|
+
* @param {string} peerId - Peer ID to validate
|
|
757
|
+
* @throws {ValidationError} If peerId is invalid
|
|
758
|
+
* @private
|
|
759
|
+
*/
|
|
760
|
+
_validatePeerId(peerId) {
|
|
761
|
+
if (peerId === null || peerId === undefined) {
|
|
762
|
+
throw ValidationError.missingArgument('peerId');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (typeof peerId !== 'string') {
|
|
766
|
+
throw ValidationError.invalidType('peerId', peerId, 'string');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (peerId.trim().length === 0) {
|
|
770
|
+
throw ValidationError.invalidArgument('peerId', peerId, {
|
|
771
|
+
reason: 'Peer ID cannot be empty or whitespace only',
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Export convenience constants
|
|
778
|
+
MeshNetwork.BatteryMode = BATTERY_MODE;
|
|
779
|
+
MeshNetwork.PanicTrigger = PANIC_TRIGGER;
|
|
780
|
+
MeshNetwork.HealthStatus = HEALTH_STATUS;
|
|
781
|
+
|
|
782
|
+
module.exports = {
|
|
783
|
+
MeshNetwork,
|
|
784
|
+
BATTERY_MODE,
|
|
785
|
+
PANIC_TRIGGER,
|
|
786
|
+
HEALTH_STATUS,
|
|
787
|
+
};
|