react-native-ble-mesh 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/OPTIMIZATION.md +165 -52
- package/package.json +1 -1
- package/src/MeshNetwork.js +16 -8
- package/src/crypto/AutoCrypto.js +14 -3
- package/src/crypto/providers/ExpoCryptoProvider.js +3 -2
- package/src/crypto/providers/QuickCryptoProvider.js +30 -6
- package/src/crypto/providers/TweetNaClProvider.js +1 -1
- package/src/hooks/AppStateManager.js +9 -1
- package/src/hooks/useMesh.js +17 -4
- package/src/hooks/useMessages.js +4 -4
- package/src/hooks/usePeers.js +13 -9
- package/src/mesh/dedup/BloomFilter.js +44 -57
- package/src/mesh/dedup/DedupManager.js +32 -1
- package/src/mesh/fragment/Fragmenter.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +42 -17
- package/src/mesh/monitor/NetworkMonitor.js +58 -13
- package/src/mesh/peer/Peer.js +5 -2
- package/src/mesh/peer/PeerManager.js +15 -3
- package/src/mesh/router/MessageRouter.js +14 -6
- package/src/mesh/router/RouteTable.js +17 -7
- package/src/mesh/store/StoreAndForwardManager.js +13 -2
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +15 -3
- package/src/protocol/serializer.js +7 -10
- package/src/protocol/validator.js +23 -5
- package/src/service/BatteryOptimizer.js +4 -1
- package/src/service/HandshakeManager.js +12 -16
- package/src/service/SessionManager.js +12 -10
- package/src/service/text/TextManager.js +21 -15
- package/src/storage/MessageStore.js +55 -2
- package/src/transport/BLETransport.js +11 -2
- package/src/transport/MultiTransport.js +35 -10
- package/src/transport/WiFiDirectTransport.js +4 -3
- package/src/utils/EventEmitter.js +6 -9
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +5 -3
- package/src/utils/encoding.js +33 -8
package/src/protocol/header.js
CHANGED
|
@@ -16,6 +16,12 @@ const { randomBytes } = require('../utils/bytes');
|
|
|
16
16
|
*/
|
|
17
17
|
const HEADER_SIZE = 48;
|
|
18
18
|
|
|
19
|
+
// Pre-computed hex lookup table (avoids Array.from().map().join() per call)
|
|
20
|
+
const HEX_TABLE = new Array(256);
|
|
21
|
+
for (let i = 0; i < 256; i++) {
|
|
22
|
+
HEX_TABLE[i] = (i < 16 ? '0' : '') + i.toString(16);
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* Message header class representing the 48-byte header structure.
|
|
21
27
|
* @class MessageHeader
|
|
@@ -43,7 +49,6 @@ class MessageHeader {
|
|
|
43
49
|
this.flags = options.flags ?? MESSAGE_FLAGS.NONE;
|
|
44
50
|
this.hopCount = options.hopCount ?? 0;
|
|
45
51
|
this.maxHops = options.maxHops ?? MESH_CONFIG.MAX_HOPS;
|
|
46
|
-
this.reserved = new Uint8Array(3);
|
|
47
52
|
this.messageId = options.messageId;
|
|
48
53
|
this.timestamp = options.timestamp;
|
|
49
54
|
this.expiresAt = options.expiresAt;
|
|
@@ -143,9 +148,8 @@ class MessageHeader {
|
|
|
143
148
|
view.setUint16(40, header.payloadLength, false);
|
|
144
149
|
buffer[42] = header.fragmentIndex;
|
|
145
150
|
buffer[43] = header.fragmentTotal;
|
|
146
|
-
// Calculate checksum over header without checksum field
|
|
147
|
-
const
|
|
148
|
-
const checksum = crc32(checksumData);
|
|
151
|
+
// Calculate checksum over header without checksum field (subarray = zero-copy view)
|
|
152
|
+
const checksum = crc32(buffer.subarray(0, 44));
|
|
149
153
|
view.setUint32(44, checksum, false);
|
|
150
154
|
header.checksum = checksum;
|
|
151
155
|
|
|
@@ -204,9 +208,11 @@ function writeUint64BE(view, offset, value) {
|
|
|
204
208
|
* @returns {string} Hex string
|
|
205
209
|
*/
|
|
206
210
|
function bytesToHex(bytes) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
let hex = '';
|
|
212
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
213
|
+
hex += HEX_TABLE[bytes[i]];
|
|
214
|
+
}
|
|
215
|
+
return hex;
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
module.exports = {
|
package/src/protocol/message.js
CHANGED
|
@@ -9,6 +9,18 @@ const { MessageHeader, HEADER_SIZE, generateUuid } = require('./header');
|
|
|
9
9
|
const { MESSAGE_FLAGS, MESH_CONFIG } = require('../constants');
|
|
10
10
|
const { MessageError } = require('../errors');
|
|
11
11
|
|
|
12
|
+
// Cached TextEncoder/TextDecoder singletons (avoids per-call allocation)
|
|
13
|
+
let _encoder = null;
|
|
14
|
+
let _decoder = null;
|
|
15
|
+
function _getEncoder() {
|
|
16
|
+
if (!_encoder) { _encoder = new TextEncoder(); }
|
|
17
|
+
return _encoder;
|
|
18
|
+
}
|
|
19
|
+
function _getDecoder() {
|
|
20
|
+
if (!_decoder) { _decoder = new TextDecoder(); }
|
|
21
|
+
return _decoder;
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Message class representing a complete mesh network message.
|
|
14
26
|
* @class Message
|
|
@@ -44,7 +56,7 @@ class Message {
|
|
|
44
56
|
|
|
45
57
|
// Convert string payload to bytes
|
|
46
58
|
if (typeof payload === 'string') {
|
|
47
|
-
payload =
|
|
59
|
+
payload = _getEncoder().encode(payload);
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
if (!(payload instanceof Uint8Array)) {
|
|
@@ -99,7 +111,7 @@ class Message {
|
|
|
99
111
|
});
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
const payload = data.
|
|
114
|
+
const payload = data.subarray(HEADER_SIZE, HEADER_SIZE + header.payloadLength);
|
|
103
115
|
|
|
104
116
|
return new Message(header, payload);
|
|
105
117
|
}
|
|
@@ -171,7 +183,7 @@ class Message {
|
|
|
171
183
|
* @returns {string} Decoded payload content
|
|
172
184
|
*/
|
|
173
185
|
getContent() {
|
|
174
|
-
return
|
|
186
|
+
return _getDecoder().decode(this.payload);
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
/**
|
|
@@ -71,9 +71,8 @@ function serializeHeader(header) {
|
|
|
71
71
|
// Byte 43: fragmentTotal
|
|
72
72
|
buffer[43] = header.fragmentTotal ?? 1;
|
|
73
73
|
|
|
74
|
-
// Bytes 44-47: checksum (calculated over bytes 0-43)
|
|
75
|
-
const
|
|
76
|
-
const checksum = crc32(checksumData);
|
|
74
|
+
// Bytes 44-47: checksum (calculated over bytes 0-43, subarray = zero-copy view)
|
|
75
|
+
const checksum = crc32(buffer.subarray(0, 44));
|
|
77
76
|
view.setUint32(44, checksum, false);
|
|
78
77
|
|
|
79
78
|
return buffer;
|
|
@@ -110,7 +109,8 @@ function serialize(message) {
|
|
|
110
109
|
|
|
111
110
|
// Convert string payload to bytes
|
|
112
111
|
if (typeof payload === 'string') {
|
|
113
|
-
|
|
112
|
+
if (!serialize._encoder) { serialize._encoder = new TextEncoder(); }
|
|
113
|
+
payload = serialize._encoder.encode(payload);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// Default to empty payload
|
|
@@ -122,13 +122,10 @@ function serialize(message) {
|
|
|
122
122
|
throw MessageError.invalidFormat(null, { reason: 'Payload must be Uint8Array or string' });
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Update payloadLength
|
|
126
|
-
|
|
127
|
-
...header,
|
|
128
|
-
payloadLength: payload.length
|
|
129
|
-
};
|
|
125
|
+
// Update payloadLength directly (avoids object spread allocation)
|
|
126
|
+
header.payloadLength = payload.length;
|
|
130
127
|
|
|
131
|
-
const headerBytes = serializeHeader(
|
|
128
|
+
const headerBytes = serializeHeader(header);
|
|
132
129
|
const result = new Uint8Array(headerBytes.length + payload.length);
|
|
133
130
|
|
|
134
131
|
result.set(headerBytes, 0);
|
|
@@ -15,6 +15,12 @@ const { PROTOCOL_VERSION, MESSAGE_TYPE, MESH_CONFIG } = require('../constants');
|
|
|
15
15
|
*/
|
|
16
16
|
const VALID_MESSAGE_TYPES = new Set(Object.values(MESSAGE_TYPE));
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Cached frozen result for valid validations to avoid repeated allocations.
|
|
20
|
+
* @type {{ valid: boolean, errors: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Validates a message header.
|
|
20
26
|
*
|
|
@@ -102,8 +108,12 @@ function validateHeader(header) {
|
|
|
102
108
|
errors.push(`Fragment index (${header.fragmentIndex}) >= total (${header.fragmentTotal})`);
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
if (errors.length === 0) {
|
|
112
|
+
return VALID_RESULT;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
return {
|
|
106
|
-
valid:
|
|
116
|
+
valid: false,
|
|
107
117
|
errors
|
|
108
118
|
};
|
|
109
119
|
}
|
|
@@ -152,8 +162,12 @@ function validateMessage(message) {
|
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
if (errors.length === 0) {
|
|
166
|
+
return VALID_RESULT;
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
return {
|
|
156
|
-
valid:
|
|
170
|
+
valid: false,
|
|
157
171
|
errors
|
|
158
172
|
};
|
|
159
173
|
}
|
|
@@ -171,7 +185,7 @@ function validateChecksum(headerBytes) {
|
|
|
171
185
|
|
|
172
186
|
const view = new DataView(headerBytes.buffer, headerBytes.byteOffset, HEADER_SIZE);
|
|
173
187
|
const storedChecksum = view.getUint32(44, false);
|
|
174
|
-
const checksumData = headerBytes.
|
|
188
|
+
const checksumData = headerBytes.subarray(0, 44);
|
|
175
189
|
const calculatedChecksum = crc32(checksumData);
|
|
176
190
|
|
|
177
191
|
return {
|
|
@@ -242,7 +256,7 @@ function validateRawMessage(data) {
|
|
|
242
256
|
}
|
|
243
257
|
|
|
244
258
|
// Validate checksum
|
|
245
|
-
const checksumResult = validateChecksum(data.
|
|
259
|
+
const checksumResult = validateChecksum(data.subarray(0, HEADER_SIZE));
|
|
246
260
|
if (!checksumResult.valid) {
|
|
247
261
|
errors.push(
|
|
248
262
|
`Checksum mismatch: expected 0x${checksumResult.expected.toString(16)}, ` +
|
|
@@ -259,8 +273,12 @@ function validateRawMessage(data) {
|
|
|
259
273
|
errors.push(`Incomplete message: expected ${expectedTotal} bytes, got ${data.length}`);
|
|
260
274
|
}
|
|
261
275
|
|
|
276
|
+
if (errors.length === 0) {
|
|
277
|
+
return VALID_RESULT;
|
|
278
|
+
}
|
|
279
|
+
|
|
262
280
|
return {
|
|
263
|
-
valid:
|
|
281
|
+
valid: false,
|
|
264
282
|
errors
|
|
265
283
|
};
|
|
266
284
|
}
|
|
@@ -23,6 +23,9 @@ const BATTERY_MODE = Object.freeze({
|
|
|
23
23
|
AUTO: 'auto'
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
/** Pre-computed Set of valid battery modes for O(1) lookup */
|
|
27
|
+
const BATTERY_MODE_SET = new Set(Object.values(BATTERY_MODE));
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* Battery profile configuration
|
|
28
31
|
* @typedef {Object} BatteryProfile
|
|
@@ -224,7 +227,7 @@ class BatteryOptimizer extends EventEmitter {
|
|
|
224
227
|
* @returns {Promise<void>}
|
|
225
228
|
*/
|
|
226
229
|
async setMode(mode) {
|
|
227
|
-
if (!
|
|
230
|
+
if (!BATTERY_MODE_SET.has(mode)) {
|
|
228
231
|
throw new Error(`Invalid battery mode: ${mode}`);
|
|
229
232
|
}
|
|
230
233
|
|
|
@@ -177,32 +177,28 @@ class HandshakeManager extends EventEmitter {
|
|
|
177
177
|
let sendNonce = 0;
|
|
178
178
|
let recvNonce = 0;
|
|
179
179
|
|
|
180
|
+
// Pre-allocate nonce buffers per direction to avoid per-call allocation
|
|
181
|
+
const sendNonceBuf = new Uint8Array(24);
|
|
182
|
+
const sendNonceView = new DataView(sendNonceBuf.buffer);
|
|
183
|
+
const recvNonceBuf = new Uint8Array(24);
|
|
184
|
+
const recvNonceView = new DataView(recvNonceBuf.buffer);
|
|
185
|
+
|
|
180
186
|
return {
|
|
181
187
|
encrypt: (plaintext) => {
|
|
182
188
|
if (provider && typeof provider.encrypt === 'function') {
|
|
183
|
-
// Use proper AEAD encryption with nonce
|
|
184
|
-
const nonce = new Uint8Array(24); // tweetnacl uses 24-byte nonces
|
|
185
|
-
const view = new DataView(nonce.buffer);
|
|
186
189
|
// Store counter in last 8 bytes of nonce
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
return provider.encrypt(sendKey, nonce, plaintext);
|
|
190
|
+
sendNonceView.setUint32(16, 0, true);
|
|
191
|
+
sendNonceView.setUint32(20, sendNonce++, true);
|
|
192
|
+
return provider.encrypt(sendKey, sendNonceBuf, plaintext);
|
|
192
193
|
}
|
|
193
194
|
throw new Error('Crypto provider required for encryption');
|
|
194
195
|
},
|
|
195
196
|
|
|
196
197
|
decrypt: (ciphertext) => {
|
|
197
198
|
if (provider && typeof provider.decrypt === 'function') {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (nonce.byteLength >= 8) {
|
|
202
|
-
view.setUint32(nonce.byteLength - 8, 0, true);
|
|
203
|
-
view.setUint32(nonce.byteLength - 4, recvNonce++, true);
|
|
204
|
-
}
|
|
205
|
-
return provider.decrypt(recvKey, nonce, ciphertext);
|
|
199
|
+
recvNonceView.setUint32(16, 0, true);
|
|
200
|
+
recvNonceView.setUint32(20, recvNonce++, true);
|
|
201
|
+
return provider.decrypt(recvKey, recvNonceBuf, ciphertext);
|
|
206
202
|
}
|
|
207
203
|
throw new Error('Crypto provider required for encryption');
|
|
208
204
|
},
|
|
@@ -120,24 +120,26 @@ class SessionManager {
|
|
|
120
120
|
// No crypto provider available
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Pre-allocate nonce buffers per direction to avoid per-call allocation
|
|
124
|
+
const sendNonceBuf = new Uint8Array(24);
|
|
125
|
+
const sendNonceView = new DataView(sendNonceBuf.buffer);
|
|
126
|
+
const recvNonceBuf = new Uint8Array(24);
|
|
127
|
+
const recvNonceView = new DataView(recvNonceBuf.buffer);
|
|
128
|
+
|
|
123
129
|
const session = {
|
|
124
130
|
encrypt: (plaintext) => {
|
|
125
131
|
if (provider && typeof provider.encrypt === 'function') {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
view.setUint32(nonce.byteLength - 4, sendNonce++, true);
|
|
130
|
-
return provider.encrypt(sendKey, nonce, plaintext);
|
|
132
|
+
sendNonceView.setUint32(16, 0, true);
|
|
133
|
+
sendNonceView.setUint32(20, sendNonce++, true);
|
|
134
|
+
return provider.encrypt(sendKey, sendNonceBuf, plaintext);
|
|
131
135
|
}
|
|
132
136
|
return plaintext;
|
|
133
137
|
},
|
|
134
138
|
decrypt: (ciphertext) => {
|
|
135
139
|
if (provider && typeof provider.decrypt === 'function') {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
view.setUint32(nonce.byteLength - 4, recvNonce++, true);
|
|
140
|
-
return provider.decrypt(recvKey, nonce, ciphertext);
|
|
140
|
+
recvNonceView.setUint32(16, 0, true);
|
|
141
|
+
recvNonceView.setUint32(20, recvNonce++, true);
|
|
142
|
+
return provider.decrypt(recvKey, recvNonceBuf, ciphertext);
|
|
141
143
|
}
|
|
142
144
|
return ciphertext;
|
|
143
145
|
},
|
|
@@ -12,6 +12,13 @@ const TextMessage = require('./message/TextMessage');
|
|
|
12
12
|
const { ChannelManager } = require('./channel');
|
|
13
13
|
const { BroadcastManager } = require('./broadcast');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Cached TextEncoder/TextDecoder instances to avoid per-call allocation
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
const cachedEncoder = new TextEncoder();
|
|
20
|
+
const cachedDecoder = new TextDecoder();
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Text manager states
|
|
17
24
|
* @constant {Object}
|
|
@@ -420,8 +427,8 @@ class TextManager extends EventEmitter {
|
|
|
420
427
|
_handleChannelMessagePayload(peerId, payload) {
|
|
421
428
|
// First byte is channel ID length
|
|
422
429
|
const channelIdLength = payload[0];
|
|
423
|
-
const channelId =
|
|
424
|
-
const messagePayload = payload.
|
|
430
|
+
const channelId = cachedDecoder.decode(payload.subarray(1, 1 + channelIdLength));
|
|
431
|
+
const messagePayload = payload.subarray(1 + channelIdLength);
|
|
425
432
|
|
|
426
433
|
this.handleChannelMessage(peerId, channelId, messagePayload);
|
|
427
434
|
}
|
|
@@ -435,7 +442,7 @@ class TextManager extends EventEmitter {
|
|
|
435
442
|
while (offset < payload.length) {
|
|
436
443
|
const length = payload[offset];
|
|
437
444
|
offset += 1;
|
|
438
|
-
const messageId =
|
|
445
|
+
const messageId = cachedDecoder.decode(payload.subarray(offset, offset + length));
|
|
439
446
|
messageIds.push(messageId);
|
|
440
447
|
offset += length;
|
|
441
448
|
}
|
|
@@ -456,21 +463,20 @@ class TextManager extends EventEmitter {
|
|
|
456
463
|
const messageIds = Array.from(this._pendingReadReceipts);
|
|
457
464
|
this._pendingReadReceipts.clear();
|
|
458
465
|
|
|
459
|
-
//
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return part;
|
|
466
|
-
});
|
|
466
|
+
// Pre-calculate total size and allocate once
|
|
467
|
+
const encodedIds = messageIds.map(id => cachedEncoder.encode(id));
|
|
468
|
+
let totalLength = 0;
|
|
469
|
+
for (let i = 0; i < encodedIds.length; i++) {
|
|
470
|
+
totalLength += 1 + encodedIds[i].length;
|
|
471
|
+
}
|
|
467
472
|
|
|
468
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
469
473
|
const payload = new Uint8Array(totalLength);
|
|
470
474
|
let offset = 0;
|
|
471
|
-
for (
|
|
472
|
-
payload.
|
|
473
|
-
offset +=
|
|
475
|
+
for (let i = 0; i < encodedIds.length; i++) {
|
|
476
|
+
payload[offset] = encodedIds[i].length;
|
|
477
|
+
offset += 1;
|
|
478
|
+
payload.set(encodedIds[i], offset);
|
|
479
|
+
offset += encodedIds[i].length;
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
this.emit('read-receipts-sent', { messageIds, count: messageIds.length });
|
|
@@ -8,6 +8,52 @@
|
|
|
8
8
|
const MemoryStorage = require('./MemoryStorage');
|
|
9
9
|
const { MESH_CONFIG } = require('../constants');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Base64 encoding/decoding for compact Uint8Array serialization
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
15
|
+
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
16
|
+
|
|
17
|
+
function uint8ArrayToBase64(bytes) {
|
|
18
|
+
let result = '';
|
|
19
|
+
const len = bytes.length;
|
|
20
|
+
for (let i = 0; i < len; i += 3) {
|
|
21
|
+
const b0 = bytes[i];
|
|
22
|
+
const b1 = i + 1 < len ? bytes[i + 1] : 0;
|
|
23
|
+
const b2 = i + 2 < len ? bytes[i + 2] : 0;
|
|
24
|
+
result += BASE64_CHARS[b0 >> 2];
|
|
25
|
+
result += BASE64_CHARS[((b0 & 3) << 4) | (b1 >> 4)];
|
|
26
|
+
result += (i + 1 < len) ? BASE64_CHARS[((b1 & 15) << 2) | (b2 >> 6)] : '=';
|
|
27
|
+
result += (i + 2 < len) ? BASE64_CHARS[b2 & 63] : '=';
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BASE64_LOOKUP = new Uint8Array(128);
|
|
33
|
+
for (let i = 0; i < BASE64_CHARS.length; i++) {
|
|
34
|
+
BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function base64ToUint8Array(str) {
|
|
38
|
+
const len = str.length;
|
|
39
|
+
let padding = 0;
|
|
40
|
+
if (str[len - 1] === '=') { padding++; }
|
|
41
|
+
if (str[len - 2] === '=') { padding++; }
|
|
42
|
+
const byteLen = (len * 3 / 4) - padding;
|
|
43
|
+
const bytes = new Uint8Array(byteLen);
|
|
44
|
+
let j = 0;
|
|
45
|
+
for (let i = 0; i < len; i += 4) {
|
|
46
|
+
const a = BASE64_LOOKUP[str.charCodeAt(i)];
|
|
47
|
+
const b = BASE64_LOOKUP[str.charCodeAt(i + 1)];
|
|
48
|
+
const c = BASE64_LOOKUP[str.charCodeAt(i + 2)];
|
|
49
|
+
const d = BASE64_LOOKUP[str.charCodeAt(i + 3)];
|
|
50
|
+
bytes[j++] = (a << 2) | (b >> 4);
|
|
51
|
+
if (j < byteLen) { bytes[j++] = ((b & 15) << 4) | (c >> 2); }
|
|
52
|
+
if (j < byteLen) { bytes[j++] = ((c & 3) << 6) | d; }
|
|
53
|
+
}
|
|
54
|
+
return bytes;
|
|
55
|
+
}
|
|
56
|
+
|
|
11
57
|
/**
|
|
12
58
|
* Message store for persisting and retrieving mesh network messages.
|
|
13
59
|
* Provides message caching, deduplication support, and cleanup functionality.
|
|
@@ -69,8 +115,9 @@ class MessageStore {
|
|
|
69
115
|
const storedMessage = {
|
|
70
116
|
...message,
|
|
71
117
|
payload: message.payload
|
|
72
|
-
?
|
|
118
|
+
? uint8ArrayToBase64(message.payload)
|
|
73
119
|
: undefined,
|
|
120
|
+
payloadEncoding: message.payload ? 'base64' : undefined,
|
|
74
121
|
storedAt: Date.now()
|
|
75
122
|
};
|
|
76
123
|
|
|
@@ -96,7 +143,13 @@ class MessageStore {
|
|
|
96
143
|
|
|
97
144
|
// Convert payload back to Uint8Array
|
|
98
145
|
if (message.payload) {
|
|
99
|
-
message.
|
|
146
|
+
if (message.payloadEncoding === 'base64') {
|
|
147
|
+
message.payload = base64ToUint8Array(message.payload);
|
|
148
|
+
} else if (Array.isArray(message.payload)) {
|
|
149
|
+
// Backwards compatibility with old Array.from() format
|
|
150
|
+
message.payload = new Uint8Array(message.payload);
|
|
151
|
+
}
|
|
152
|
+
delete message.payloadEncoding;
|
|
100
153
|
}
|
|
101
154
|
|
|
102
155
|
return message;
|
|
@@ -293,6 +293,15 @@ class BLETransport extends Transport {
|
|
|
293
293
|
await this._adapter.disconnect(peerId);
|
|
294
294
|
} finally {
|
|
295
295
|
this._peers.delete(peerId);
|
|
296
|
+
|
|
297
|
+
// Clean up write queue and writing state
|
|
298
|
+
const queue = this._writeQueue.get(peerId);
|
|
299
|
+
if (queue) {
|
|
300
|
+
queue.forEach(({ reject }) => reject(new Error('Peer disconnected')));
|
|
301
|
+
this._writeQueue.delete(peerId);
|
|
302
|
+
}
|
|
303
|
+
this._writing.delete(peerId);
|
|
304
|
+
|
|
296
305
|
this.emit('peerDisconnected', { peerId, reason: 'user_request' });
|
|
297
306
|
}
|
|
298
307
|
}
|
|
@@ -325,7 +334,7 @@ class BLETransport extends Transport {
|
|
|
325
334
|
|
|
326
335
|
// Chunk data for BLE MTU compliance
|
|
327
336
|
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
|
328
|
-
const chunk = data.
|
|
337
|
+
const chunk = data.subarray(offset, Math.min(offset + chunkSize, data.length));
|
|
329
338
|
await this._queuedWrite(peerId, chunk);
|
|
330
339
|
}
|
|
331
340
|
}
|
|
@@ -414,7 +423,7 @@ class BLETransport extends Transport {
|
|
|
414
423
|
* @private
|
|
415
424
|
*/
|
|
416
425
|
_handleData(peerId, data) {
|
|
417
|
-
this.emit('message', { peerId, data: new Uint8Array(data) });
|
|
426
|
+
this.emit('message', { peerId, data: data instanceof Uint8Array ? data : new Uint8Array(data) });
|
|
418
427
|
}
|
|
419
428
|
|
|
420
429
|
/**
|
|
@@ -54,6 +54,9 @@ class MultiTransport extends Transport {
|
|
|
54
54
|
|
|
55
55
|
/** @type {Map<string, string>} peerId → preferred transport name */
|
|
56
56
|
this._peerTransportMap = new Map();
|
|
57
|
+
|
|
58
|
+
/** @type {Array<{transport: Transport, event: string, handler: Function}>} */
|
|
59
|
+
this._wiredHandlers = [];
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
@@ -122,6 +125,13 @@ class MultiTransport extends Transport {
|
|
|
122
125
|
if (this._wifiTransport) { stopPromises.push(this._wifiTransport.stop().catch(() => {})); }
|
|
123
126
|
|
|
124
127
|
await Promise.allSettled(stopPromises);
|
|
128
|
+
|
|
129
|
+
// Remove wired event handlers to prevent listener leaks
|
|
130
|
+
for (const { transport, event, handler } of this._wiredHandlers) {
|
|
131
|
+
transport.off(event, handler);
|
|
132
|
+
}
|
|
133
|
+
this._wiredHandlers = [];
|
|
134
|
+
|
|
125
135
|
this._peers.clear();
|
|
126
136
|
this._peerTransportMap.clear();
|
|
127
137
|
this._setState(Transport.STATE.STOPPED);
|
|
@@ -272,13 +282,13 @@ class MultiTransport extends Transport {
|
|
|
272
282
|
* @private
|
|
273
283
|
*/
|
|
274
284
|
_wireTransport(transport, name) {
|
|
275
|
-
|
|
285
|
+
const onPeerConnected = (info) => {
|
|
276
286
|
this._peerTransportMap.set(info.peerId, name);
|
|
277
287
|
this._peers.set(info.peerId, { ...info, transport: name });
|
|
278
288
|
this.emit('peerConnected', { ...info, transport: name });
|
|
279
|
-
}
|
|
289
|
+
};
|
|
280
290
|
|
|
281
|
-
|
|
291
|
+
const onPeerDisconnected = (info) => {
|
|
282
292
|
// Only remove if no other transport has this peer
|
|
283
293
|
const otherTransport = this._getFallbackTransport(transport);
|
|
284
294
|
if (!otherTransport || !otherTransport.isConnected(info.peerId)) {
|
|
@@ -286,19 +296,34 @@ class MultiTransport extends Transport {
|
|
|
286
296
|
this._peerTransportMap.delete(info.peerId);
|
|
287
297
|
this.emit('peerDisconnected', { ...info, transport: name });
|
|
288
298
|
}
|
|
289
|
-
}
|
|
299
|
+
};
|
|
290
300
|
|
|
291
|
-
|
|
301
|
+
const onMessage = (msg) => {
|
|
292
302
|
this.emit('message', { ...msg, transport: name });
|
|
293
|
-
}
|
|
303
|
+
};
|
|
294
304
|
|
|
295
|
-
|
|
305
|
+
const onDeviceDiscovered = (info) => {
|
|
296
306
|
this.emit('deviceDiscovered', { ...info, transport: name });
|
|
297
|
-
}
|
|
307
|
+
};
|
|
298
308
|
|
|
299
|
-
|
|
309
|
+
const onError = (err) => {
|
|
300
310
|
this.emit('transportError', { transport: name, error: err });
|
|
301
|
-
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
transport.on('peerConnected', onPeerConnected);
|
|
314
|
+
transport.on('peerDisconnected', onPeerDisconnected);
|
|
315
|
+
transport.on('message', onMessage);
|
|
316
|
+
transport.on('deviceDiscovered', onDeviceDiscovered);
|
|
317
|
+
transport.on('error', onError);
|
|
318
|
+
|
|
319
|
+
// Store references for cleanup
|
|
320
|
+
this._wiredHandlers.push(
|
|
321
|
+
{ transport, event: 'peerConnected', handler: onPeerConnected },
|
|
322
|
+
{ transport, event: 'peerDisconnected', handler: onPeerDisconnected },
|
|
323
|
+
{ transport, event: 'message', handler: onMessage },
|
|
324
|
+
{ transport, event: 'deviceDiscovered', handler: onDeviceDiscovered },
|
|
325
|
+
{ transport, event: 'error', handler: onError }
|
|
326
|
+
);
|
|
302
327
|
}
|
|
303
328
|
}
|
|
304
329
|
|
|
@@ -284,10 +284,11 @@ class WiFiDirectTransport extends Transport {
|
|
|
284
284
|
|
|
285
285
|
/** @private */
|
|
286
286
|
_uint8ArrayToBase64(bytes) {
|
|
287
|
-
|
|
288
|
-
for (let i = 0; i < bytes.length; i
|
|
289
|
-
|
|
287
|
+
const chunks = [];
|
|
288
|
+
for (let i = 0; i < bytes.length; i += 8192) {
|
|
289
|
+
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, Math.min(i + 8192, bytes.length))));
|
|
290
290
|
}
|
|
291
|
+
const binary = chunks.join('');
|
|
291
292
|
return typeof btoa !== 'undefined' ? btoa(binary) : Buffer.from(bytes).toString('base64');
|
|
292
293
|
}
|
|
293
294
|
}
|
|
@@ -116,10 +116,11 @@ class EventEmitter {
|
|
|
116
116
|
return false;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const listeners = this._events.get(event)
|
|
120
|
-
const
|
|
119
|
+
const listeners = this._events.get(event);
|
|
120
|
+
const hasOnce = listeners.some(e => e.once);
|
|
121
|
+
const iterList = hasOnce ? listeners.slice() : listeners;
|
|
121
122
|
|
|
122
|
-
for (const entry of
|
|
123
|
+
for (const entry of iterList) {
|
|
123
124
|
try {
|
|
124
125
|
entry.listener.apply(this, args);
|
|
125
126
|
} catch (error) {
|
|
@@ -130,15 +131,11 @@ class EventEmitter {
|
|
|
130
131
|
console.error('Error in error handler:', error);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
if (entry.once) {
|
|
135
|
-
toRemove.push(entry);
|
|
136
|
-
}
|
|
137
134
|
}
|
|
138
135
|
|
|
139
136
|
// Remove one-time listeners
|
|
140
|
-
if (
|
|
141
|
-
const remaining =
|
|
137
|
+
if (hasOnce) {
|
|
138
|
+
const remaining = listeners.filter(e => !e.once);
|
|
142
139
|
if (remaining.length === 0) {
|
|
143
140
|
this._events.delete(event);
|
|
144
141
|
} else {
|
package/src/utils/bytes.js
CHANGED
|
@@ -11,9 +11,16 @@
|
|
|
11
11
|
* @returns {Uint8Array} Concatenated array
|
|
12
12
|
*/
|
|
13
13
|
function concat(...arrays) {
|
|
14
|
-
//
|
|
15
|
-
const validArrays =
|
|
16
|
-
|
|
14
|
+
// Single-pass: filter out undefined/null and calculate total length
|
|
15
|
+
const validArrays = [];
|
|
16
|
+
let totalLength = 0;
|
|
17
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
18
|
+
const arr = arrays[i];
|
|
19
|
+
if (arr !== null && arr !== undefined) {
|
|
20
|
+
validArrays.push(arr);
|
|
21
|
+
totalLength += arr.length;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
|
|
18
25
|
const result = new Uint8Array(totalLength);
|
|
19
26
|
let offset = 0;
|
|
@@ -119,10 +126,7 @@ function xor(a, b) {
|
|
|
119
126
|
* @returns {Uint8Array} The filled array (same reference)
|
|
120
127
|
*/
|
|
121
128
|
function fill(array, value) {
|
|
122
|
-
|
|
123
|
-
for (let i = 0; i < array.length; i++) {
|
|
124
|
-
array[i] = byte;
|
|
125
|
-
}
|
|
129
|
+
array.fill(value & 0xff);
|
|
126
130
|
return array;
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -132,9 +136,7 @@ function fill(array, value) {
|
|
|
132
136
|
* @returns {Uint8Array} Copy of the array
|
|
133
137
|
*/
|
|
134
138
|
function copy(array) {
|
|
135
|
-
|
|
136
|
-
result.set(array);
|
|
137
|
-
return result;
|
|
139
|
+
return array.slice();
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/**
|