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.
@@ -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
+ };