yakmesh 1.1.0 → 1.3.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/CHANGELOG.md +65 -75
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/discord.md +74 -0
- package/mesh/beacon-broadcast.js +655 -0
- package/mesh/echo-ranging.js +612 -0
- package/mesh/phantom-routing.js +700 -0
- package/mesh/pulse-sync.js +618 -0
- package/mesh/temporal-encoder.js +383 -0
- package/package.json +82 -51
- package/test-novel-systems.mjs +398 -0
- package/test-tme.mjs +383 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAKMESH™ BEACON - Broadcast Emergency Alert Channel Over Network
|
|
3
|
+
*
|
|
4
|
+
* Priority message propagation with guaranteed delivery:
|
|
5
|
+
* - Flood-based protocol with intelligent deduplication
|
|
6
|
+
* - Proof-of-receipt for message delivery confirmation
|
|
7
|
+
* - TTL-based propagation control
|
|
8
|
+
* - Emergency priority levels with preemption
|
|
9
|
+
*
|
|
10
|
+
* Key Innovation: "When the message MUST get through"
|
|
11
|
+
* - Combines TME temporal encoding with PULSE heartbeats
|
|
12
|
+
* - Cryptographic receipts prove delivery chain
|
|
13
|
+
* - Multi-path redundancy ensures survivability
|
|
14
|
+
*
|
|
15
|
+
* @module mesh/beacon-broadcast
|
|
16
|
+
* @license MIT
|
|
17
|
+
* @copyright 2026 YAKMESH Contributors
|
|
18
|
+
* @trademark BEACON™ is a trademark of YAKMESH
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { randomBytes, createHash } from 'crypto';
|
|
22
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
23
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
24
|
+
|
|
25
|
+
const BEACON_CONFIG = {
|
|
26
|
+
// Priority levels
|
|
27
|
+
priorities: {
|
|
28
|
+
ROUTINE: 0, // Normal messages
|
|
29
|
+
PRIORITY: 1, // Important but not urgent
|
|
30
|
+
IMMEDIATE: 2, // Time-sensitive
|
|
31
|
+
FLASH: 3, // Emergency
|
|
32
|
+
CRITICAL: 4, // Life/safety critical
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Propagation settings
|
|
36
|
+
defaultTTL: 10, // Default hop count
|
|
37
|
+
maxTTL: 50, // Maximum hop count
|
|
38
|
+
deduplicationWindowMs: 60000, // 1 minute dedup window
|
|
39
|
+
receiptTimeout: 30000, // 30s to collect receipts
|
|
40
|
+
|
|
41
|
+
// Redundancy
|
|
42
|
+
minRedundantPaths: 3, // Minimum paths for critical messages
|
|
43
|
+
maxRetransmissions: 5, // Max retries per peer
|
|
44
|
+
retransmitDelayMs: 1000, // Delay between retries
|
|
45
|
+
|
|
46
|
+
// Rate limiting
|
|
47
|
+
maxMessagesPerSecond: 100, // Per-node rate limit
|
|
48
|
+
priorityBoost: { // Rate limit multiplier by priority
|
|
49
|
+
ROUTINE: 1,
|
|
50
|
+
PRIORITY: 2,
|
|
51
|
+
IMMEDIATE: 5,
|
|
52
|
+
FLASH: 10,
|
|
53
|
+
CRITICAL: 100,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A beacon message with propagation metadata
|
|
59
|
+
*/
|
|
60
|
+
class BeaconMessage {
|
|
61
|
+
constructor(options) {
|
|
62
|
+
this.id = options.id || bytesToHex(randomBytes(16));
|
|
63
|
+
this.originNodeId = options.originNodeId;
|
|
64
|
+
this.payload = options.payload;
|
|
65
|
+
this.priority = options.priority || BEACON_CONFIG.priorities.ROUTINE;
|
|
66
|
+
this.ttl = options.ttl || BEACON_CONFIG.defaultTTL;
|
|
67
|
+
this.timestamp = options.timestamp || Date.now();
|
|
68
|
+
this.expiresAt = options.expiresAt || (Date.now() + 300000); // 5 min default
|
|
69
|
+
this.hopPath = options.hopPath || [];
|
|
70
|
+
this.signature = options.signature || null;
|
|
71
|
+
this.hash = this._computeHash();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_computeHash() {
|
|
75
|
+
const data = [
|
|
76
|
+
this.id,
|
|
77
|
+
this.originNodeId,
|
|
78
|
+
JSON.stringify(this.payload),
|
|
79
|
+
this.priority.toString(),
|
|
80
|
+
this.timestamp.toString(),
|
|
81
|
+
this.expiresAt.toString(),
|
|
82
|
+
].join(':');
|
|
83
|
+
|
|
84
|
+
return bytesToHex(sha3_256(utf8ToBytes(data)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if message is still valid
|
|
89
|
+
*/
|
|
90
|
+
isValid() {
|
|
91
|
+
return (
|
|
92
|
+
this.ttl > 0 &&
|
|
93
|
+
Date.now() < this.expiresAt &&
|
|
94
|
+
this.hash === this._computeHash()
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a forwarding copy with decremented TTL
|
|
100
|
+
*/
|
|
101
|
+
forward(currentNodeId) {
|
|
102
|
+
if (this.ttl <= 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return new BeaconMessage({
|
|
107
|
+
id: this.id,
|
|
108
|
+
originNodeId: this.originNodeId,
|
|
109
|
+
payload: this.payload,
|
|
110
|
+
priority: this.priority,
|
|
111
|
+
ttl: this.ttl - 1,
|
|
112
|
+
timestamp: this.timestamp,
|
|
113
|
+
expiresAt: this.expiresAt,
|
|
114
|
+
hopPath: [...this.hopPath, currentNodeId],
|
|
115
|
+
signature: this.signature,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get priority name
|
|
121
|
+
*/
|
|
122
|
+
getPriorityName() {
|
|
123
|
+
for (const [name, value] of Object.entries(BEACON_CONFIG.priorities)) {
|
|
124
|
+
if (value === this.priority) return name;
|
|
125
|
+
}
|
|
126
|
+
return 'UNKNOWN';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
serialize() {
|
|
130
|
+
return {
|
|
131
|
+
id: this.id,
|
|
132
|
+
originNodeId: this.originNodeId,
|
|
133
|
+
payload: this.payload,
|
|
134
|
+
priority: this.priority,
|
|
135
|
+
ttl: this.ttl,
|
|
136
|
+
timestamp: this.timestamp,
|
|
137
|
+
expiresAt: this.expiresAt,
|
|
138
|
+
hopPath: this.hopPath,
|
|
139
|
+
signature: this.signature,
|
|
140
|
+
hash: this.hash,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static deserialize(obj) {
|
|
145
|
+
const msg = new BeaconMessage({
|
|
146
|
+
id: obj.id,
|
|
147
|
+
originNodeId: obj.originNodeId,
|
|
148
|
+
payload: obj.payload,
|
|
149
|
+
priority: obj.priority,
|
|
150
|
+
ttl: obj.ttl,
|
|
151
|
+
timestamp: obj.timestamp,
|
|
152
|
+
expiresAt: obj.expiresAt,
|
|
153
|
+
hopPath: obj.hopPath,
|
|
154
|
+
signature: obj.signature,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (msg.hash !== obj.hash) {
|
|
158
|
+
throw new Error('Message hash mismatch');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return msg;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Delivery receipt proving message was received
|
|
167
|
+
*/
|
|
168
|
+
class DeliveryReceipt {
|
|
169
|
+
constructor(options) {
|
|
170
|
+
this.messageId = options.messageId;
|
|
171
|
+
this.receiverNodeId = options.receiverNodeId;
|
|
172
|
+
this.receivedAt = options.receivedAt || Date.now();
|
|
173
|
+
this.hopCount = options.hopCount || 0;
|
|
174
|
+
this.signature = options.signature || null;
|
|
175
|
+
this.hash = this._computeHash();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_computeHash() {
|
|
179
|
+
const data = [
|
|
180
|
+
this.messageId,
|
|
181
|
+
this.receiverNodeId,
|
|
182
|
+
this.receivedAt.toString(),
|
|
183
|
+
this.hopCount.toString(),
|
|
184
|
+
].join(':');
|
|
185
|
+
|
|
186
|
+
return bytesToHex(sha3_256(utf8ToBytes(data)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
serialize() {
|
|
190
|
+
return {
|
|
191
|
+
messageId: this.messageId,
|
|
192
|
+
receiverNodeId: this.receiverNodeId,
|
|
193
|
+
receivedAt: this.receivedAt,
|
|
194
|
+
hopCount: this.hopCount,
|
|
195
|
+
signature: this.signature,
|
|
196
|
+
hash: this.hash,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static deserialize(obj) {
|
|
201
|
+
const receipt = new DeliveryReceipt({
|
|
202
|
+
messageId: obj.messageId,
|
|
203
|
+
receiverNodeId: obj.receiverNodeId,
|
|
204
|
+
receivedAt: obj.receivedAt,
|
|
205
|
+
hopCount: obj.hopCount,
|
|
206
|
+
signature: obj.signature,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (receipt.hash !== obj.hash) {
|
|
210
|
+
throw new Error('Receipt hash mismatch');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return receipt;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Deduplication tracker to prevent message storms
|
|
219
|
+
*/
|
|
220
|
+
class DeduplicationTracker {
|
|
221
|
+
constructor(options = {}) {
|
|
222
|
+
this.windowMs = options.windowMs || BEACON_CONFIG.deduplicationWindowMs;
|
|
223
|
+
this.seen = new Map(); // messageId -> { timestamp, hopPaths }
|
|
224
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 10000);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if message was already seen
|
|
229
|
+
*/
|
|
230
|
+
isDuplicate(messageId) {
|
|
231
|
+
return this.seen.has(messageId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Mark message as seen
|
|
236
|
+
*/
|
|
237
|
+
markSeen(message) {
|
|
238
|
+
const existing = this.seen.get(message.id);
|
|
239
|
+
|
|
240
|
+
if (existing) {
|
|
241
|
+
// Track additional path
|
|
242
|
+
existing.hopPaths.push([...message.hopPath]);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.seen.set(message.id, {
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
hopPaths: [[...message.hopPath]],
|
|
249
|
+
priority: message.priority,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get propagation stats for a message
|
|
257
|
+
*/
|
|
258
|
+
getMessageStats(messageId) {
|
|
259
|
+
const entry = this.seen.get(messageId);
|
|
260
|
+
if (!entry) return null;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
pathCount: entry.hopPaths.length,
|
|
264
|
+
firstSeen: entry.timestamp,
|
|
265
|
+
uniqueNodes: new Set(entry.hopPaths.flat()).size,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
cleanup() {
|
|
270
|
+
const cutoff = Date.now() - this.windowMs;
|
|
271
|
+
for (const [id, entry] of this.seen) {
|
|
272
|
+
if (entry.timestamp < cutoff) {
|
|
273
|
+
this.seen.delete(id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getStats() {
|
|
279
|
+
return {
|
|
280
|
+
trackedMessages: this.seen.size,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
destroy() {
|
|
285
|
+
clearInterval(this.cleanupInterval);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Receipt collector for delivery confirmation
|
|
291
|
+
*/
|
|
292
|
+
class ReceiptCollector {
|
|
293
|
+
constructor() {
|
|
294
|
+
this.pending = new Map(); // messageId -> { receipts, callbacks, timeout }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Start collecting receipts for a message
|
|
299
|
+
*/
|
|
300
|
+
startCollection(messageId, expectedCount, callback) {
|
|
301
|
+
const timeout = setTimeout(() => {
|
|
302
|
+
this._finalize(messageId);
|
|
303
|
+
}, BEACON_CONFIG.receiptTimeout);
|
|
304
|
+
|
|
305
|
+
this.pending.set(messageId, {
|
|
306
|
+
receipts: [],
|
|
307
|
+
expectedCount,
|
|
308
|
+
callback,
|
|
309
|
+
timeout,
|
|
310
|
+
startedAt: Date.now(),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Add a receipt
|
|
316
|
+
*/
|
|
317
|
+
addReceipt(receipt) {
|
|
318
|
+
const pending = this.pending.get(receipt.messageId);
|
|
319
|
+
if (!pending) return false;
|
|
320
|
+
|
|
321
|
+
pending.receipts.push(receipt);
|
|
322
|
+
|
|
323
|
+
// Check if we have enough
|
|
324
|
+
if (pending.receipts.length >= pending.expectedCount) {
|
|
325
|
+
this._finalize(receipt.messageId);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_finalize(messageId) {
|
|
332
|
+
const pending = this.pending.get(messageId);
|
|
333
|
+
if (!pending) return;
|
|
334
|
+
|
|
335
|
+
clearTimeout(pending.timeout);
|
|
336
|
+
|
|
337
|
+
const result = {
|
|
338
|
+
messageId,
|
|
339
|
+
receipts: pending.receipts,
|
|
340
|
+
receivedCount: pending.receipts.length,
|
|
341
|
+
expectedCount: pending.expectedCount,
|
|
342
|
+
success: pending.receipts.length >= pending.expectedCount,
|
|
343
|
+
duration: Date.now() - pending.startedAt,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
pending.callback(result);
|
|
347
|
+
this.pending.delete(messageId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getStats() {
|
|
351
|
+
return {
|
|
352
|
+
pendingMessages: this.pending.size,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Priority queue for message processing
|
|
359
|
+
*/
|
|
360
|
+
class PriorityMessageQueue {
|
|
361
|
+
constructor() {
|
|
362
|
+
this.queues = new Map();
|
|
363
|
+
|
|
364
|
+
// Create queue for each priority
|
|
365
|
+
for (const priority of Object.values(BEACON_CONFIG.priorities)) {
|
|
366
|
+
this.queues.set(priority, []);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
enqueue(message) {
|
|
371
|
+
const queue = this.queues.get(message.priority);
|
|
372
|
+
if (queue) {
|
|
373
|
+
queue.push(message);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Dequeue highest priority message
|
|
379
|
+
*/
|
|
380
|
+
dequeue() {
|
|
381
|
+
// Check from highest to lowest priority
|
|
382
|
+
const priorities = Object.values(BEACON_CONFIG.priorities).sort((a, b) => b - a);
|
|
383
|
+
|
|
384
|
+
for (const priority of priorities) {
|
|
385
|
+
const queue = this.queues.get(priority);
|
|
386
|
+
if (queue && queue.length > 0) {
|
|
387
|
+
return queue.shift();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Peek at highest priority message without removing
|
|
396
|
+
*/
|
|
397
|
+
peek() {
|
|
398
|
+
const priorities = Object.values(BEACON_CONFIG.priorities).sort((a, b) => b - a);
|
|
399
|
+
|
|
400
|
+
for (const priority of priorities) {
|
|
401
|
+
const queue = this.queues.get(priority);
|
|
402
|
+
if (queue && queue.length > 0) {
|
|
403
|
+
return queue[0];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
size() {
|
|
411
|
+
let total = 0;
|
|
412
|
+
for (const queue of this.queues.values()) {
|
|
413
|
+
total += queue.length;
|
|
414
|
+
}
|
|
415
|
+
return total;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getStats() {
|
|
419
|
+
const stats = {};
|
|
420
|
+
for (const [priority, queue] of this.queues) {
|
|
421
|
+
const name = Object.entries(BEACON_CONFIG.priorities)
|
|
422
|
+
.find(([_, v]) => v === priority)?.[0] || 'UNKNOWN';
|
|
423
|
+
stats[name] = queue.length;
|
|
424
|
+
}
|
|
425
|
+
return stats;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Main BEACON broadcast system
|
|
431
|
+
*/
|
|
432
|
+
class BeaconBroadcast {
|
|
433
|
+
constructor(options = {}) {
|
|
434
|
+
this.nodeId = options.nodeId || bytesToHex(randomBytes(16));
|
|
435
|
+
this.dedup = new DeduplicationTracker();
|
|
436
|
+
this.receipts = new ReceiptCollector();
|
|
437
|
+
this.outboundQueue = new PriorityMessageQueue();
|
|
438
|
+
this.peers = new Set();
|
|
439
|
+
|
|
440
|
+
this.stats = {
|
|
441
|
+
messagesOriginated: 0,
|
|
442
|
+
messagesRelayed: 0,
|
|
443
|
+
messagesDuplicate: 0,
|
|
444
|
+
messagesExpired: 0,
|
|
445
|
+
receiptsGenerated: 0,
|
|
446
|
+
receiptsCollected: 0,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Callbacks
|
|
450
|
+
this.onBroadcast = options.onBroadcast || (() => {});
|
|
451
|
+
this.onReceive = options.onReceive || (() => {});
|
|
452
|
+
this.onReceipt = options.onReceipt || (() => {});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Register a peer for broadcasting
|
|
457
|
+
*/
|
|
458
|
+
addPeer(peerId) {
|
|
459
|
+
this.peers.add(peerId);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
removePeer(peerId) {
|
|
463
|
+
this.peers.delete(peerId);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Broadcast a new message
|
|
468
|
+
*/
|
|
469
|
+
broadcast(payload, options = {}) {
|
|
470
|
+
const message = new BeaconMessage({
|
|
471
|
+
originNodeId: this.nodeId,
|
|
472
|
+
payload,
|
|
473
|
+
priority: options.priority || BEACON_CONFIG.priorities.ROUTINE,
|
|
474
|
+
ttl: options.ttl || BEACON_CONFIG.defaultTTL,
|
|
475
|
+
expiresAt: options.expiresAt,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
this.dedup.markSeen(message);
|
|
479
|
+
this.stats.messagesOriginated++;
|
|
480
|
+
|
|
481
|
+
// Queue for each peer
|
|
482
|
+
for (const peerId of this.peers) {
|
|
483
|
+
this.outboundQueue.enqueue(message);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Start receipt collection if requested
|
|
487
|
+
if (options.confirmDelivery) {
|
|
488
|
+
this.receipts.startCollection(
|
|
489
|
+
message.id,
|
|
490
|
+
options.expectedReceipts || this.peers.size,
|
|
491
|
+
(result) => {
|
|
492
|
+
this.stats.receiptsCollected += result.receivedCount;
|
|
493
|
+
options.onDeliveryConfirm?.(result);
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
this.onBroadcast({ message: message.serialize(), peerCount: this.peers.size });
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
messageId: message.id,
|
|
502
|
+
hash: message.hash,
|
|
503
|
+
queuedFor: this.peers.size,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Receive and process an incoming message
|
|
509
|
+
*/
|
|
510
|
+
receive(messageData) {
|
|
511
|
+
try {
|
|
512
|
+
const message = BeaconMessage.deserialize(messageData);
|
|
513
|
+
|
|
514
|
+
// Check validity
|
|
515
|
+
if (!message.isValid()) {
|
|
516
|
+
this.stats.messagesExpired++;
|
|
517
|
+
return { accepted: false, reason: 'Message expired or invalid' };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check for duplicate
|
|
521
|
+
if (this.dedup.isDuplicate(message.id)) {
|
|
522
|
+
this.stats.messagesDuplicate++;
|
|
523
|
+
return { accepted: false, reason: 'Duplicate message' };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Mark as seen
|
|
527
|
+
this.dedup.markSeen(message);
|
|
528
|
+
|
|
529
|
+
// Generate receipt
|
|
530
|
+
const receipt = new DeliveryReceipt({
|
|
531
|
+
messageId: message.id,
|
|
532
|
+
receiverNodeId: this.nodeId,
|
|
533
|
+
hopCount: message.hopPath.length,
|
|
534
|
+
});
|
|
535
|
+
this.stats.receiptsGenerated++;
|
|
536
|
+
|
|
537
|
+
// Notify application
|
|
538
|
+
this.onReceive({
|
|
539
|
+
message: message.serialize(),
|
|
540
|
+
receipt: receipt.serialize(),
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Forward to peers (if TTL allows)
|
|
544
|
+
const forwarded = this._forwardMessage(message);
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
accepted: true,
|
|
548
|
+
messageId: message.id,
|
|
549
|
+
priority: message.getPriorityName(),
|
|
550
|
+
receipt: receipt.serialize(),
|
|
551
|
+
forwarded,
|
|
552
|
+
};
|
|
553
|
+
} catch (err) {
|
|
554
|
+
return { accepted: false, reason: err.message };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Forward message to peers
|
|
560
|
+
*/
|
|
561
|
+
_forwardMessage(message) {
|
|
562
|
+
const forwarded = [];
|
|
563
|
+
const forwardedMsg = message.forward(this.nodeId);
|
|
564
|
+
|
|
565
|
+
if (!forwardedMsg) {
|
|
566
|
+
return forwarded;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
for (const peerId of this.peers) {
|
|
570
|
+
// Don't send back to nodes in the hop path
|
|
571
|
+
if (message.hopPath.includes(peerId)) continue;
|
|
572
|
+
if (peerId === message.originNodeId) continue;
|
|
573
|
+
|
|
574
|
+
forwarded.push(peerId);
|
|
575
|
+
this.outboundQueue.enqueue(forwardedMsg);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (forwarded.length > 0) {
|
|
579
|
+
this.stats.messagesRelayed++;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return forwarded;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Process a delivery receipt
|
|
587
|
+
*/
|
|
588
|
+
processReceipt(receiptData) {
|
|
589
|
+
try {
|
|
590
|
+
const receipt = DeliveryReceipt.deserialize(receiptData);
|
|
591
|
+
const added = this.receipts.addReceipt(receipt);
|
|
592
|
+
|
|
593
|
+
if (added) {
|
|
594
|
+
this.onReceipt({ receipt: receipt.serialize() });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return { accepted: added };
|
|
598
|
+
} catch (err) {
|
|
599
|
+
return { accepted: false, reason: err.message };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get next outbound message
|
|
605
|
+
*/
|
|
606
|
+
getNextOutbound() {
|
|
607
|
+
return this.outboundQueue.dequeue()?.serialize();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Send critical emergency broadcast
|
|
612
|
+
*/
|
|
613
|
+
sendEmergency(payload, options = {}) {
|
|
614
|
+
return this.broadcast(payload, {
|
|
615
|
+
...options,
|
|
616
|
+
priority: BEACON_CONFIG.priorities.CRITICAL,
|
|
617
|
+
ttl: BEACON_CONFIG.maxTTL,
|
|
618
|
+
confirmDelivery: true,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Send flash priority message
|
|
624
|
+
*/
|
|
625
|
+
sendFlash(payload, options = {}) {
|
|
626
|
+
return this.broadcast(payload, {
|
|
627
|
+
...options,
|
|
628
|
+
priority: BEACON_CONFIG.priorities.FLASH,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
getStats() {
|
|
633
|
+
return {
|
|
634
|
+
...this.stats,
|
|
635
|
+
peers: this.peers.size,
|
|
636
|
+
queueStats: this.outboundQueue.getStats(),
|
|
637
|
+
dedupStats: this.dedup.getStats(),
|
|
638
|
+
receiptStats: this.receipts.getStats(),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
destroy() {
|
|
643
|
+
this.dedup.destroy();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export {
|
|
648
|
+
BEACON_CONFIG,
|
|
649
|
+
BeaconMessage,
|
|
650
|
+
DeliveryReceipt,
|
|
651
|
+
DeduplicationTracker,
|
|
652
|
+
ReceiptCollector,
|
|
653
|
+
PriorityMessageQueue,
|
|
654
|
+
BeaconBroadcast,
|
|
655
|
+
};
|