@waku/sds 0.0.8-ff9c430.0 → 0.0.8

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bundle/index.js +614 -25
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/message_channel/events.d.ts +27 -3
  7. package/dist/message_channel/events.js +6 -0
  8. package/dist/message_channel/events.js.map +1 -1
  9. package/dist/message_channel/index.d.ts +1 -1
  10. package/dist/message_channel/message.d.ts +17 -13
  11. package/dist/message_channel/message.js +19 -11
  12. package/dist/message_channel/message.js.map +1 -1
  13. package/dist/message_channel/message_channel.d.ts +31 -3
  14. package/dist/message_channel/message_channel.js +99 -10
  15. package/dist/message_channel/message_channel.js.map +1 -1
  16. package/dist/message_channel/repair/buffers.d.ts +106 -0
  17. package/dist/message_channel/repair/buffers.js +206 -0
  18. package/dist/message_channel/repair/buffers.js.map +1 -0
  19. package/dist/message_channel/repair/repair.d.ts +92 -0
  20. package/dist/message_channel/repair/repair.js +209 -0
  21. package/dist/message_channel/repair/repair.js.map +1 -0
  22. package/dist/message_channel/repair/utils.d.ts +40 -0
  23. package/dist/message_channel/repair/utils.js +61 -0
  24. package/dist/message_channel/repair/utils.js.map +1 -0
  25. package/package.json +93 -1
  26. package/src/index.ts +7 -1
  27. package/src/message_channel/events.ts +28 -3
  28. package/src/message_channel/index.ts +1 -1
  29. package/src/message_channel/message.ts +24 -13
  30. package/src/message_channel/message_channel.ts +151 -17
  31. package/src/message_channel/repair/buffers.ts +277 -0
  32. package/src/message_channel/repair/repair.ts +331 -0
  33. package/src/message_channel/repair/utils.ts +80 -0
package/bundle/index.js CHANGED
@@ -1045,6 +1045,12 @@ var MessageChannelEvent;
1045
1045
  MessageChannelEvent["InSyncReceived"] = "sds:in:sync-received";
1046
1046
  MessageChannelEvent["InMessageLost"] = "sds:in:message-irretrievably-lost";
1047
1047
  MessageChannelEvent["ErrorTask"] = "sds:error-task";
1048
+ // SDS-R Repair Events
1049
+ MessageChannelEvent["RepairRequestQueued"] = "sds:repair:request-queued";
1050
+ MessageChannelEvent["RepairRequestSent"] = "sds:repair:request-sent";
1051
+ MessageChannelEvent["RepairRequestReceived"] = "sds:repair:request-received";
1052
+ MessageChannelEvent["RepairResponseQueued"] = "sds:repair:response-queued";
1053
+ MessageChannelEvent["RepairResponseSent"] = "sds:repair:response-sent";
1048
1054
  })(MessageChannelEvent || (MessageChannelEvent = {}));
1049
1055
 
1050
1056
  /**
@@ -24056,6 +24062,10 @@ var HistoryEntry;
24056
24062
  w.uint32(18);
24057
24063
  w.bytes(obj.retrievalHint);
24058
24064
  }
24065
+ if (obj.senderId != null) {
24066
+ w.uint32(26);
24067
+ w.string(obj.senderId);
24068
+ }
24059
24069
  if (opts.lengthDelimited !== false) {
24060
24070
  w.ldelim();
24061
24071
  }
@@ -24075,6 +24085,10 @@ var HistoryEntry;
24075
24085
  obj.retrievalHint = reader.bytes();
24076
24086
  break;
24077
24087
  }
24088
+ case 3: {
24089
+ obj.senderId = reader.string();
24090
+ break;
24091
+ }
24078
24092
  default: {
24079
24093
  reader.skipType(tag & 7);
24080
24094
  break;
@@ -24128,6 +24142,12 @@ var SdsMessage;
24128
24142
  w.uint32(98);
24129
24143
  w.bytes(obj.bloomFilter);
24130
24144
  }
24145
+ if (obj.repairRequest != null) {
24146
+ for (const value of obj.repairRequest) {
24147
+ w.uint32(106);
24148
+ HistoryEntry.codec().encode(value, w);
24149
+ }
24150
+ }
24131
24151
  if (obj.content != null) {
24132
24152
  w.uint32(162);
24133
24153
  w.bytes(obj.content);
@@ -24140,7 +24160,8 @@ var SdsMessage;
24140
24160
  senderId: '',
24141
24161
  messageId: '',
24142
24162
  channelId: '',
24143
- causalHistory: []
24163
+ causalHistory: [],
24164
+ repairRequest: []
24144
24165
  };
24145
24166
  const end = length == null ? reader.len : reader.pos + length;
24146
24167
  while (reader.pos < end) {
@@ -24175,6 +24196,15 @@ var SdsMessage;
24175
24196
  obj.bloomFilter = reader.bytes();
24176
24197
  break;
24177
24198
  }
24199
+ case 13: {
24200
+ if (opts.limits?.repairRequest != null && obj.repairRequest.length === opts.limits.repairRequest) {
24201
+ throw new MaxLengthError('Decode error - map field "repairRequest" had too many elements');
24202
+ }
24203
+ obj.repairRequest.push(HistoryEntry.codec().decode(reader, reader.uint32(), {
24204
+ limits: opts.limits?.repairRequest$
24205
+ }));
24206
+ break;
24207
+ }
24178
24208
  case 20: {
24179
24209
  obj.content = reader.bytes();
24180
24210
  break;
@@ -24198,7 +24228,7 @@ var SdsMessage;
24198
24228
  };
24199
24229
  })(SdsMessage || (SdsMessage = {}));
24200
24230
 
24201
- const log$1 = new Logger("sds:message");
24231
+ const log$3 = new Logger("sds:message");
24202
24232
  class Message {
24203
24233
  messageId;
24204
24234
  channelId;
@@ -24207,8 +24237,9 @@ class Message {
24207
24237
  lamportTimestamp;
24208
24238
  bloomFilter;
24209
24239
  content;
24240
+ repairRequest;
24210
24241
  retrievalHint;
24211
- constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content,
24242
+ constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest = [],
24212
24243
  /**
24213
24244
  * Not encoded, set after it is sent, used to include in follow-up messages
24214
24245
  */
@@ -24220,6 +24251,7 @@ class Message {
24220
24251
  this.lamportTimestamp = lamportTimestamp;
24221
24252
  this.bloomFilter = bloomFilter;
24222
24253
  this.content = content;
24254
+ this.repairRequest = repairRequest;
24223
24255
  this.retrievalHint = retrievalHint;
24224
24256
  }
24225
24257
  encode() {
@@ -24227,20 +24259,20 @@ class Message {
24227
24259
  }
24228
24260
  static decode(data) {
24229
24261
  try {
24230
- const { messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content } = SdsMessage.decode(data);
24262
+ const { messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest } = SdsMessage.decode(data);
24231
24263
  if (testContentMessage({ lamportTimestamp, content })) {
24232
- return new ContentMessage(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content);
24264
+ return new ContentMessage(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest);
24233
24265
  }
24234
24266
  if (testEphemeralMessage({ lamportTimestamp, content })) {
24235
- return new EphemeralMessage(messageId, channelId, senderId, causalHistory, undefined, bloomFilter, content);
24267
+ return new EphemeralMessage(messageId, channelId, senderId, causalHistory, undefined, bloomFilter, content, repairRequest);
24236
24268
  }
24237
24269
  if (testSyncMessage({ lamportTimestamp, content })) {
24238
- return new SyncMessage(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, undefined);
24270
+ return new SyncMessage(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, undefined, repairRequest);
24239
24271
  }
24240
- log$1.error("message received was of unknown type", lamportTimestamp, content);
24272
+ log$3.error("message received was of unknown type", lamportTimestamp, content);
24241
24273
  }
24242
24274
  catch (err) {
24243
- log$1.error("failed to decode sds message", err);
24275
+ log$3.error("failed to decode sds message", err);
24244
24276
  }
24245
24277
  return undefined;
24246
24278
  }
@@ -24253,13 +24285,14 @@ class SyncMessage extends Message {
24253
24285
  lamportTimestamp;
24254
24286
  bloomFilter;
24255
24287
  content;
24288
+ repairRequest;
24256
24289
  retrievalHint;
24257
- constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content,
24290
+ constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest = [],
24258
24291
  /**
24259
24292
  * Not encoded, set after it is sent, used to include in follow-up messages
24260
24293
  */
24261
24294
  retrievalHint) {
24262
- super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, retrievalHint);
24295
+ super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest, retrievalHint);
24263
24296
  this.messageId = messageId;
24264
24297
  this.channelId = channelId;
24265
24298
  this.senderId = senderId;
@@ -24267,6 +24300,7 @@ class SyncMessage extends Message {
24267
24300
  this.lamportTimestamp = lamportTimestamp;
24268
24301
  this.bloomFilter = bloomFilter;
24269
24302
  this.content = content;
24303
+ this.repairRequest = repairRequest;
24270
24304
  this.retrievalHint = retrievalHint;
24271
24305
  }
24272
24306
  }
@@ -24286,8 +24320,9 @@ class EphemeralMessage extends Message {
24286
24320
  lamportTimestamp;
24287
24321
  bloomFilter;
24288
24322
  content;
24323
+ repairRequest;
24289
24324
  retrievalHint;
24290
- constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content,
24325
+ constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest = [],
24291
24326
  /**
24292
24327
  * Not encoded, set after it is sent, used to include in follow-up messages
24293
24328
  */
@@ -24295,7 +24330,7 @@ class EphemeralMessage extends Message {
24295
24330
  if (!content || !content.length) {
24296
24331
  throw Error("Ephemeral Message must have content");
24297
24332
  }
24298
- super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, retrievalHint);
24333
+ super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest, retrievalHint);
24299
24334
  this.messageId = messageId;
24300
24335
  this.channelId = channelId;
24301
24336
  this.senderId = senderId;
@@ -24303,6 +24338,7 @@ class EphemeralMessage extends Message {
24303
24338
  this.lamportTimestamp = lamportTimestamp;
24304
24339
  this.bloomFilter = bloomFilter;
24305
24340
  this.content = content;
24341
+ this.repairRequest = repairRequest;
24306
24342
  this.retrievalHint = retrievalHint;
24307
24343
  }
24308
24344
  }
@@ -24323,8 +24359,9 @@ class ContentMessage extends Message {
24323
24359
  lamportTimestamp;
24324
24360
  bloomFilter;
24325
24361
  content;
24362
+ repairRequest;
24326
24363
  retrievalHint;
24327
- constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content,
24364
+ constructor(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest = [],
24328
24365
  /**
24329
24366
  * Not encoded, set after it is sent, used to include in follow-up messages
24330
24367
  */
@@ -24332,7 +24369,7 @@ class ContentMessage extends Message {
24332
24369
  if (!content.length) {
24333
24370
  throw Error("Content Message must have content");
24334
24371
  }
24335
- super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, retrievalHint);
24372
+ super(messageId, channelId, senderId, causalHistory, lamportTimestamp, bloomFilter, content, repairRequest, retrievalHint);
24336
24373
  this.messageId = messageId;
24337
24374
  this.channelId = channelId;
24338
24375
  this.senderId = senderId;
@@ -24340,6 +24377,7 @@ class ContentMessage extends Message {
24340
24377
  this.lamportTimestamp = lamportTimestamp;
24341
24378
  this.bloomFilter = bloomFilter;
24342
24379
  this.content = content;
24380
+ this.repairRequest = repairRequest;
24343
24381
  this.retrievalHint = retrievalHint;
24344
24382
  }
24345
24383
  // `valueOf` is used by comparison operands such as `<`
@@ -24407,10 +24445,477 @@ class MemLocalHistory {
24407
24445
  }
24408
24446
  }
24409
24447
 
24448
+ const log$2 = new Logger("sds:repair:buffers");
24449
+ /**
24450
+ * Buffer for outgoing repair requests (messages we need)
24451
+ * Maintains a sorted array by T_req for efficient retrieval of eligible entries
24452
+ */
24453
+ class OutgoingRepairBuffer {
24454
+ // Sorted array by T_req (ascending - earliest first)
24455
+ items = [];
24456
+ maxSize;
24457
+ constructor(maxSize = 1000) {
24458
+ this.maxSize = maxSize;
24459
+ }
24460
+ /**
24461
+ * Add a missing message to the outgoing repair request buffer
24462
+ * If message already exists, it is not updated (keeps original T_req)
24463
+ * @returns true if the entry was added, false if it already existed
24464
+ */
24465
+ add(entry, tReq) {
24466
+ const messageId = entry.messageId;
24467
+ // Check if already exists - do NOT update T_req per spec
24468
+ if (this.has(messageId)) {
24469
+ log$2.info(`Message ${messageId} already in outgoing buffer, keeping original T_req`);
24470
+ return false;
24471
+ }
24472
+ // Check buffer size limit
24473
+ if (this.items.length >= this.maxSize) {
24474
+ // Evict furthest T_req entry (last in sorted array) to preserve repairs that need to be sent the soonest
24475
+ const evicted = this.items.pop();
24476
+ log$2.warn(`Buffer full, evicted furthest entry ${evicted.entry.messageId} with T_req ${evicted.tReq}`);
24477
+ }
24478
+ // Add new entry and re-sort
24479
+ const newEntry = { entry, tReq, requested: false };
24480
+ const combined = [...this.items, newEntry];
24481
+ // Sort by T_req (ascending)
24482
+ combined.sort((a, b) => a.tReq - b.tReq);
24483
+ this.items = combined;
24484
+ log$2.info(`Added ${messageId} to outgoing buffer with T_req: ${tReq}`);
24485
+ return true;
24486
+ }
24487
+ /**
24488
+ * Remove a message from the buffer (e.g., when received)
24489
+ */
24490
+ remove(messageId) {
24491
+ this.items = this.items.filter((item) => item.entry.messageId !== messageId);
24492
+ }
24493
+ /**
24494
+ * Get eligible repair requests (where T_req <= currentTime)
24495
+ * Returns up to maxRequests entries from the front of the sorted array
24496
+ * Marks returned entries as requested but keeps them in buffer until received
24497
+ */
24498
+ getEligible(currentTime = Date.now(), maxRequests = 3) {
24499
+ const eligible = [];
24500
+ // Iterate from front of sorted array (earliest T_req first)
24501
+ for (const item of this.items) {
24502
+ // Since array is sorted, once we hit an item with tReq > currentTime,
24503
+ // all remaining items also have tReq > currentTime
24504
+ if (item.tReq > currentTime) {
24505
+ break;
24506
+ }
24507
+ // Only return items that haven't been requested yet
24508
+ if (!item.requested && eligible.length < maxRequests) {
24509
+ eligible.push(item.entry);
24510
+ // Mark as requested so we don't request it again
24511
+ item.requested = true;
24512
+ log$2.info(`Repair request for ${item.entry.messageId} is eligible and marked as requested`);
24513
+ }
24514
+ // If we've found enough eligible items, exit early
24515
+ if (eligible.length >= maxRequests) {
24516
+ break;
24517
+ }
24518
+ }
24519
+ return eligible;
24520
+ }
24521
+ /**
24522
+ * Check if a message is in the buffer
24523
+ */
24524
+ has(messageId) {
24525
+ return this.items.some((item) => item.entry.messageId === messageId);
24526
+ }
24527
+ /**
24528
+ * Get the current buffer size
24529
+ */
24530
+ get size() {
24531
+ return this.items.length;
24532
+ }
24533
+ /**
24534
+ * Clear all entries
24535
+ */
24536
+ clear() {
24537
+ this.items = [];
24538
+ }
24539
+ /**
24540
+ * Get all entries (for testing/debugging)
24541
+ */
24542
+ getAll() {
24543
+ return this.items.map((item) => item.entry);
24544
+ }
24545
+ /**
24546
+ * Get items array directly (for testing)
24547
+ */
24548
+ getItems() {
24549
+ return [...this.items];
24550
+ }
24551
+ }
24552
+ /**
24553
+ * Buffer for incoming repair requests (repairs we need to send)
24554
+ * Maintains a sorted array by T_resp for efficient retrieval of ready entries
24555
+ */
24556
+ class IncomingRepairBuffer {
24557
+ // Sorted array by T_resp (ascending - earliest first)
24558
+ items = [];
24559
+ maxSize;
24560
+ constructor(maxSize = 1000) {
24561
+ this.maxSize = maxSize;
24562
+ }
24563
+ /**
24564
+ * Add a repair request that we can fulfill
24565
+ * If message already exists, it is ignored (not updated)
24566
+ * @returns true if the entry was added, false if it already existed
24567
+ */
24568
+ add(entry, tResp) {
24569
+ const messageId = entry.messageId;
24570
+ // Check if already exists - ignore per spec
24571
+ if (this.has(messageId)) {
24572
+ log$2.info(`Message ${messageId} already in incoming buffer, ignoring`);
24573
+ return false;
24574
+ }
24575
+ // Check buffer size limit
24576
+ if (this.items.length >= this.maxSize) {
24577
+ // Evict furthest T_resp entry (last in sorted array)
24578
+ const evicted = this.items.pop();
24579
+ log$2.warn(`Buffer full, evicted furthest entry ${evicted.entry.messageId} with T_resp ${evicted.tResp}`);
24580
+ }
24581
+ // Add new entry and re-sort
24582
+ const newEntry = { entry, tResp };
24583
+ const combined = [...this.items, newEntry];
24584
+ // Sort by T_resp (ascending)
24585
+ combined.sort((a, b) => a.tResp - b.tResp);
24586
+ this.items = combined;
24587
+ log$2.info(`Added ${messageId} to incoming buffer with T_resp: ${tResp}`);
24588
+ return true;
24589
+ }
24590
+ /**
24591
+ * Remove a message from the buffer
24592
+ */
24593
+ remove(messageId) {
24594
+ this.items = this.items.filter((item) => item.entry.messageId !== messageId);
24595
+ }
24596
+ /**
24597
+ * Get repairs ready to be sent (where T_resp <= currentTime)
24598
+ * Removes and returns ready entries
24599
+ */
24600
+ getReady(currentTime) {
24601
+ // Find cutoff point - first item with tResp > currentTime
24602
+ // Since array is sorted, all items before this are ready
24603
+ let cutoff = 0;
24604
+ for (let i = 0; i < this.items.length; i++) {
24605
+ if (this.items[i].tResp > currentTime) {
24606
+ cutoff = i;
24607
+ break;
24608
+ }
24609
+ // If we reach the end, all items are ready
24610
+ cutoff = i + 1;
24611
+ }
24612
+ // Extract ready items and log them
24613
+ const ready = this.items.slice(0, cutoff).map((item) => {
24614
+ log$2.info(`Repair for ${item.entry.messageId} is ready to be sent`);
24615
+ return item.entry;
24616
+ });
24617
+ // Keep only items after cutoff
24618
+ this.items = this.items.slice(cutoff);
24619
+ return ready;
24620
+ }
24621
+ /**
24622
+ * Check if a message is in the buffer
24623
+ */
24624
+ has(messageId) {
24625
+ return this.items.some((item) => item.entry.messageId === messageId);
24626
+ }
24627
+ /**
24628
+ * Get the current buffer size
24629
+ */
24630
+ get size() {
24631
+ return this.items.length;
24632
+ }
24633
+ /**
24634
+ * Clear all entries
24635
+ */
24636
+ clear() {
24637
+ this.items = [];
24638
+ }
24639
+ /**
24640
+ * Get all entries (for testing/debugging)
24641
+ */
24642
+ getAll() {
24643
+ return this.items.map((item) => item.entry);
24644
+ }
24645
+ /**
24646
+ * Get items array directly (for testing)
24647
+ */
24648
+ getItems() {
24649
+ return [...this.items];
24650
+ }
24651
+ }
24652
+
24653
+ /**
24654
+ * Compute SHA256 hash and convert to integer for modulo operations
24655
+ * Uses first 8 bytes of hash for the integer conversion
24656
+ */
24657
+ function hashToInteger(input) {
24658
+ const hashBytes = sha256(new TextEncoder().encode(input));
24659
+ // Use first 8 bytes for a 64-bit integer
24660
+ const view = new DataView(hashBytes.buffer, 0, 8);
24661
+ return view.getBigUint64(0, false); // big-endian
24662
+ }
24663
+ /**
24664
+ * Compute combined hash for (participantId, messageId) and convert to integer
24665
+ * This is used for T_req calculations and response group membership
24666
+ */
24667
+ function combinedHash(participantId, messageId) {
24668
+ const combined = `${participantId}${messageId}`;
24669
+ return hashToInteger(combined);
24670
+ }
24671
+ /**
24672
+ * Convert ParticipantId to numeric representation for XOR operations
24673
+ * TODO: Not per spec, further review needed
24674
+ * The spec assumes participant IDs support XOR natively, but we're using
24675
+ * SHA256 hash to ensure consistent numeric representation for string IDs
24676
+ */
24677
+ function participantIdToNumeric(participantId) {
24678
+ return hashToInteger(participantId);
24679
+ }
24680
+ /**
24681
+ * Calculate XOR distance between two participant IDs
24682
+ * Used for T_resp calculations where distance affects response timing
24683
+ */
24684
+ function calculateXorDistance(participantId1, participantId2) {
24685
+ const numeric1 = participantIdToNumeric(participantId1);
24686
+ const numeric2 = participantIdToNumeric(participantId2);
24687
+ return numeric1 ^ numeric2;
24688
+ }
24689
+ /**
24690
+ * Helper to convert bigint to number for timing calculations
24691
+ * Ensures the result fits in JavaScript's number range
24692
+ */
24693
+ function bigintToNumber(value) {
24694
+ // For timing calculations, we modulo by MAX_SAFE_INTEGER to ensure it fits
24695
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
24696
+ return Number(value % maxSafe);
24697
+ }
24698
+ /**
24699
+ * Calculate hash for a single string (used for message_id in T_resp)
24700
+ */
24701
+ function hashString(input) {
24702
+ return hashToInteger(input);
24703
+ }
24704
+
24705
+ const log$1 = new Logger("sds:repair:manager");
24706
+ /**
24707
+ * Per SDS-R spec: One response group per 128 participants
24708
+ */
24709
+ const PARTICIPANTS_PER_RESPONSE_GROUP = 128;
24710
+ /**
24711
+ * Default configuration values based on spec recommendations
24712
+ */
24713
+ const DEFAULT_REPAIR_CONFIG = {
24714
+ tMin: 30000, // 30 seconds
24715
+ tMax: 120000, // 120 seconds
24716
+ numResponseGroups: 1, // Recommendation is 1 group per PARTICIPANTS_PER_RESPONSE_GROUP participants
24717
+ bufferSize: 1000
24718
+ };
24719
+ /**
24720
+ * Manager for SDS-R repair protocol
24721
+ * Handles repair request/response timing and coordination
24722
+ */
24723
+ class RepairManager {
24724
+ participantId;
24725
+ config;
24726
+ outgoingBuffer;
24727
+ incomingBuffer;
24728
+ eventEmitter;
24729
+ constructor(participantId, config = {}, eventEmitter) {
24730
+ this.participantId = participantId;
24731
+ this.config = { ...DEFAULT_REPAIR_CONFIG, ...config };
24732
+ this.eventEmitter = eventEmitter;
24733
+ this.outgoingBuffer = new OutgoingRepairBuffer(this.config.bufferSize);
24734
+ this.incomingBuffer = new IncomingRepairBuffer(this.config.bufferSize);
24735
+ log$1.info(`RepairManager initialized for participant ${participantId}`);
24736
+ }
24737
+ /**
24738
+ * Calculate T_req - when to request repair for a missing message
24739
+ * Per spec: T_req = current_time + hash(participant_id, message_id) % (T_max - T_min) + T_min
24740
+ */
24741
+ calculateTReq(messageId, currentTime = Date.now()) {
24742
+ const hash = combinedHash(this.participantId, messageId);
24743
+ const range = BigInt(this.config.tMax - this.config.tMin);
24744
+ const offset = bigintToNumber(hash % range) + this.config.tMin;
24745
+ return currentTime + offset;
24746
+ }
24747
+ /**
24748
+ * Calculate T_resp - when to respond with a repair
24749
+ * Per spec: T_resp = current_time + (distance * hash(message_id)) % T_max
24750
+ * where distance = participant_id XOR sender_id
24751
+ */
24752
+ calculateTResp(senderId, messageId, currentTime = Date.now()) {
24753
+ const distance = calculateXorDistance(this.participantId, senderId);
24754
+ const messageHash = hashString(messageId);
24755
+ const product = distance * messageHash;
24756
+ const offset = bigintToNumber(product % BigInt(this.config.tMax));
24757
+ return currentTime + offset;
24758
+ }
24759
+ /**
24760
+ * Determine if this participant is in the response group for a message
24761
+ * Per spec: (hash(participant_id, message_id) % num_response_groups) ==
24762
+ * (hash(sender_id, message_id) % num_response_groups)
24763
+ */
24764
+ isInResponseGroup(senderId, messageId) {
24765
+ if (!senderId) {
24766
+ // Cannot determine response group without sender_id
24767
+ return false;
24768
+ }
24769
+ const numGroups = BigInt(this.config.numResponseGroups);
24770
+ if (numGroups <= BigInt(1)) {
24771
+ // Single group, everyone is in it
24772
+ return true;
24773
+ }
24774
+ const participantGroup = combinedHash(this.participantId, messageId) % numGroups;
24775
+ const senderGroup = combinedHash(senderId, messageId) % numGroups;
24776
+ return participantGroup === senderGroup;
24777
+ }
24778
+ /**
24779
+ * Handle missing dependencies by adding them to outgoing repair buffer
24780
+ * Called when causal dependencies are detected as missing
24781
+ */
24782
+ markDependenciesMissing(missingEntries, currentTime = Date.now()) {
24783
+ for (const entry of missingEntries) {
24784
+ // Calculate when to request this repair
24785
+ const tReq = this.calculateTReq(entry.messageId, currentTime);
24786
+ // Add to outgoing buffer - only log and emit event if actually added
24787
+ const wasAdded = this.outgoingBuffer.add(entry, tReq);
24788
+ if (wasAdded) {
24789
+ log$1.info(`Added missing dependency ${entry.messageId} to repair buffer with T_req=${tReq}`);
24790
+ // Emit event
24791
+ this.eventEmitter?.("RepairRequestQueued", {
24792
+ messageId: entry.messageId,
24793
+ tReq
24794
+ });
24795
+ }
24796
+ }
24797
+ }
24798
+ /**
24799
+ * Handle receipt of a message - remove from repair buffers
24800
+ * Called when a message is successfully received
24801
+ */
24802
+ markMessageReceived(messageId) {
24803
+ // Remove from both buffers as we no longer need to request or respond
24804
+ const wasInOutgoing = this.outgoingBuffer.has(messageId);
24805
+ const wasInIncoming = this.incomingBuffer.has(messageId);
24806
+ if (wasInOutgoing) {
24807
+ this.outgoingBuffer.remove(messageId);
24808
+ log$1.info(`Removed ${messageId} from outgoing repair buffer after receipt`);
24809
+ }
24810
+ if (wasInIncoming) {
24811
+ this.incomingBuffer.remove(messageId);
24812
+ log$1.info(`Removed ${messageId} from incoming repair buffer after receipt`);
24813
+ }
24814
+ }
24815
+ /**
24816
+ * Get repair requests that are eligible to be sent
24817
+ * Returns up to maxRequests entries where T_req <= currentTime
24818
+ */
24819
+ getRepairRequests(maxRequests = 3, currentTime = Date.now()) {
24820
+ return this.outgoingBuffer.getEligible(currentTime, maxRequests);
24821
+ }
24822
+ /**
24823
+ * Process incoming repair requests from other participants
24824
+ * Adds to incoming buffer if we can fulfill and are in response group
24825
+ */
24826
+ processIncomingRepairRequests(requests, localHistory, currentTime = Date.now()) {
24827
+ for (const request of requests) {
24828
+ // Remove from our own outgoing buffer (someone else is requesting it)
24829
+ this.outgoingBuffer.remove(request.messageId);
24830
+ // Check if we have this message
24831
+ const message = localHistory.find((m) => m.messageId === request.messageId);
24832
+ if (!message) {
24833
+ log$1.info(`Cannot fulfill repair for ${request.messageId} - not in local history`);
24834
+ continue;
24835
+ }
24836
+ // Check if we're in the response group
24837
+ if (!request.senderId) {
24838
+ log$1.warn(`Cannot determine response group for ${request.messageId} - missing sender_id`);
24839
+ continue;
24840
+ }
24841
+ if (!this.isInResponseGroup(request.senderId, request.messageId)) {
24842
+ log$1.info(`Not in response group for ${request.messageId}`);
24843
+ continue;
24844
+ }
24845
+ // Calculate when to respond
24846
+ const tResp = this.calculateTResp(request.senderId, request.messageId, currentTime);
24847
+ // Add to incoming buffer - only log and emit event if actually added
24848
+ const wasAdded = this.incomingBuffer.add(request, tResp);
24849
+ if (wasAdded) {
24850
+ log$1.info(`Will respond to repair request for ${request.messageId} at T_resp=${tResp}`);
24851
+ // Emit event
24852
+ this.eventEmitter?.("RepairResponseQueued", {
24853
+ messageId: request.messageId,
24854
+ tResp
24855
+ });
24856
+ }
24857
+ }
24858
+ }
24859
+ /**
24860
+ * Sweep outgoing buffer for repairs that should be requested
24861
+ * Returns entries where T_req <= currentTime
24862
+ */
24863
+ sweepOutgoingBuffer(maxRequests = 3, currentTime = Date.now()) {
24864
+ return this.getRepairRequests(maxRequests, currentTime);
24865
+ }
24866
+ /**
24867
+ * Sweep incoming buffer for repairs ready to be sent
24868
+ * Returns messages that should be rebroadcast
24869
+ */
24870
+ sweepIncomingBuffer(localHistory, currentTime = Date.now()) {
24871
+ const ready = this.incomingBuffer.getReady(currentTime);
24872
+ const messages = [];
24873
+ for (const entry of ready) {
24874
+ const message = localHistory.find((m) => m.messageId === entry.messageId);
24875
+ if (message) {
24876
+ messages.push(message);
24877
+ log$1.info(`Sending repair for ${entry.messageId}`);
24878
+ }
24879
+ else {
24880
+ log$1.warn(`Message ${entry.messageId} no longer in local history`);
24881
+ }
24882
+ }
24883
+ return messages;
24884
+ }
24885
+ /**
24886
+ * Clear all buffers
24887
+ */
24888
+ clear() {
24889
+ this.outgoingBuffer.clear();
24890
+ this.incomingBuffer.clear();
24891
+ }
24892
+ /**
24893
+ * Update number of response groups (e.g., when participants change)
24894
+ */
24895
+ updateResponseGroups(numParticipants) {
24896
+ if (numParticipants < 0 ||
24897
+ !Number.isFinite(numParticipants) ||
24898
+ !Number.isInteger(numParticipants)) {
24899
+ throw new Error(`Invalid numParticipants: ${numParticipants}. Must be a positive integer.`);
24900
+ }
24901
+ if (numParticipants > Number.MAX_SAFE_INTEGER) {
24902
+ log$1.warn(`numParticipants ${numParticipants} exceeds MAX_SAFE_INTEGER, using MAX_SAFE_INTEGER`);
24903
+ numParticipants = Number.MAX_SAFE_INTEGER;
24904
+ }
24905
+ // Per spec: num_response_groups = max(1, num_participants / PARTICIPANTS_PER_RESPONSE_GROUP)
24906
+ this.config.numResponseGroups = Math.max(1, Math.floor(numParticipants / PARTICIPANTS_PER_RESPONSE_GROUP));
24907
+ log$1.info(`Updated response groups to ${this.config.numResponseGroups} for ${numParticipants} participants`);
24908
+ }
24909
+ }
24910
+
24410
24911
  const DEFAULT_BLOOM_FILTER_OPTIONS = {
24411
24912
  capacity: 10000,
24412
24913
  errorRate: 0.001
24413
24914
  };
24915
+ /**
24916
+ * Maximum number of repair requests to include in a single message
24917
+ */
24918
+ const MAX_REPAIR_REQUESTS_PER_MESSAGE = 3;
24414
24919
  const DEFAULT_CAUSAL_HISTORY_SIZE = 200;
24415
24920
  const DEFAULT_POSSIBLE_ACKS_THRESHOLD = 2;
24416
24921
  const log = new Logger("sds:message-channel");
@@ -24427,6 +24932,7 @@ class MessageChannel extends TypedEventEmitter {
24427
24932
  causalHistorySize;
24428
24933
  possibleAcksThreshold;
24429
24934
  timeoutForLostMessagesMs;
24935
+ repairManager;
24430
24936
  tasks = [];
24431
24937
  handlers = {
24432
24938
  [Command.Send]: async (params) => {
@@ -24457,6 +24963,12 @@ class MessageChannel extends TypedEventEmitter {
24457
24963
  options.possibleAcksThreshold ?? DEFAULT_POSSIBLE_ACKS_THRESHOLD;
24458
24964
  this.timeReceived = new Map();
24459
24965
  this.timeoutForLostMessagesMs = options.timeoutForLostMessagesMs;
24966
+ // Only construct RepairManager if repair is enabled (default: true)
24967
+ if (options.enableRepair ?? true) {
24968
+ this.repairManager = new RepairManager(senderId, options.repairConfig, (event, detail) => {
24969
+ this.safeSendEvent(event, { detail });
24970
+ });
24971
+ }
24460
24972
  }
24461
24973
  static getMessageId(payload) {
24462
24974
  return bytesToHex(sha256(payload));
@@ -24589,7 +25101,7 @@ class MessageChannel extends TypedEventEmitter {
24589
25101
  sweepIncomingBuffer() {
24590
25102
  const { buffer, missing } = this.incomingBuffer.reduce(({ buffer, missing }, message) => {
24591
25103
  log.info(this.senderId, "sweeping incoming buffer", message.messageId, message.causalHistory.map((ch) => ch.messageId));
24592
- const missingDependencies = message.causalHistory.filter((messageHistoryEntry) => !this.localHistory.some(({ messageId }) => messageId === messageHistoryEntry.messageId));
25104
+ const missingDependencies = message.causalHistory.filter((messageHistoryEntry) => !this.isMessageAvailable(messageHistoryEntry.messageId));
24593
25105
  if (missingDependencies.length === 0) {
24594
25106
  if (isContentMessage(message) && this.deliverMessage(message)) {
24595
25107
  this.safeSendEvent(MessageChannelEvent.InMessageDelivered, {
@@ -24645,6 +25157,34 @@ class MessageChannel extends TypedEventEmitter {
24645
25157
  possiblyAcknowledged: new Array()
24646
25158
  });
24647
25159
  }
25160
+ /**
25161
+ * Sweep repair incoming buffer and rebroadcast messages ready for repair.
25162
+ * Per SDS-R spec: periodically check for repair responses that are due.
25163
+ *
25164
+ * @param callback - callback to rebroadcast the message
25165
+ * @returns Promise that resolves when all ready repairs have been sent
25166
+ */
25167
+ async sweepRepairIncomingBuffer(callback) {
25168
+ const repairsToSend = this.repairManager?.sweepIncomingBuffer(this.localHistory) ?? [];
25169
+ if (callback) {
25170
+ for (const message of repairsToSend) {
25171
+ try {
25172
+ await callback(message);
25173
+ log.info(this.senderId, "repair message rebroadcast", message.messageId);
25174
+ // Emit RepairResponseSent event
25175
+ this.safeSendEvent(MessageChannelEvent.RepairResponseSent, {
25176
+ detail: {
25177
+ messageId: message.messageId
25178
+ }
25179
+ });
25180
+ }
25181
+ catch (error) {
25182
+ log.error("Failed to rebroadcast repair message:", error);
25183
+ }
25184
+ }
25185
+ }
25186
+ return repairsToSend;
25187
+ }
24648
25188
  /**
24649
25189
  * Send a sync message to the SDS channel.
24650
25190
  *
@@ -24657,15 +25197,19 @@ class MessageChannel extends TypedEventEmitter {
24657
25197
  */
24658
25198
  async pushOutgoingSyncMessage(callback) {
24659
25199
  this.lamportTimestamp = lamportTimestampIncrement(this.lamportTimestamp);
25200
+ // Get repair requests to include in sync message (SDS-R)
25201
+ const repairRequests = this.repairManager?.getRepairRequests(MAX_REPAIR_REQUESTS_PER_MESSAGE) ??
25202
+ [];
24660
25203
  const message = new SyncMessage(
24661
25204
  // does not need to be secure randomness
24662
25205
  `sync-${Math.random().toString(36).substring(2)}`, this.channelId, this.senderId, this.localHistory
24663
25206
  .slice(-this.causalHistorySize)
24664
- .map(({ messageId, retrievalHint }) => {
24665
- return { messageId, retrievalHint };
24666
- }), this.lamportTimestamp, this.filter.toBytes(), undefined);
24667
- if (!message.causalHistory || message.causalHistory.length === 0) {
24668
- log.info(this.senderId, "no causal history in sync message, aborting sending");
25207
+ .map(({ messageId, retrievalHint, senderId }) => {
25208
+ return { messageId, retrievalHint, senderId };
25209
+ }), this.lamportTimestamp, this.filter.toBytes(), undefined, repairRequests);
25210
+ if ((!message.causalHistory || message.causalHistory.length === 0) &&
25211
+ repairRequests.length === 0) {
25212
+ log.info(this.senderId, "no causal history and no repair requests in sync message, aborting sending");
24669
25213
  return false;
24670
25214
  }
24671
25215
  if (callback) {
@@ -24675,6 +25219,15 @@ class MessageChannel extends TypedEventEmitter {
24675
25219
  this.safeSendEvent(MessageChannelEvent.OutSyncSent, {
24676
25220
  detail: message
24677
25221
  });
25222
+ // Emit RepairRequestSent event if repair requests were included
25223
+ if (repairRequests.length > 0) {
25224
+ this.safeSendEvent(MessageChannelEvent.RepairRequestSent, {
25225
+ detail: {
25226
+ messageIds: repairRequests.map((r) => r.messageId),
25227
+ carrierMessageId: message.messageId
25228
+ }
25229
+ });
25230
+ }
24678
25231
  return true;
24679
25232
  }
24680
25233
  catch (error) {
@@ -24722,15 +25275,30 @@ class MessageChannel extends TypedEventEmitter {
24722
25275
  detail: message
24723
25276
  });
24724
25277
  }
25278
+ // SDS-R: Handle received message in repair manager
25279
+ this.repairManager?.markMessageReceived(message.messageId);
25280
+ // SDS-R: Process incoming repair requests
25281
+ if (message.repairRequest && message.repairRequest.length > 0) {
25282
+ // Emit RepairRequestReceived event
25283
+ this.safeSendEvent(MessageChannelEvent.RepairRequestReceived, {
25284
+ detail: {
25285
+ messageIds: message.repairRequest.map((r) => r.messageId),
25286
+ fromSenderId: message.senderId
25287
+ }
25288
+ });
25289
+ this.repairManager?.processIncomingRepairRequests(message.repairRequest, this.localHistory);
25290
+ }
24725
25291
  this.reviewAckStatus(message);
24726
25292
  if (isContentMessage(message)) {
24727
25293
  this.filter.insert(message.messageId);
24728
25294
  }
24729
- const missingDependencies = message.causalHistory.filter((messageHistoryEntry) => !this.localHistory.some(({ messageId }) => messageId === messageHistoryEntry.messageId));
25295
+ const missingDependencies = message.causalHistory.filter((messageHistoryEntry) => !this.isMessageAvailable(messageHistoryEntry.messageId));
24730
25296
  if (missingDependencies.length > 0) {
24731
25297
  this.incomingBuffer.push(message);
24732
25298
  this.timeReceived.set(message.messageId, Date.now());
24733
25299
  log.info(this.senderId, "new incoming message", message.messageId, "is missing dependencies", missingDependencies.map((ch) => ch.messageId));
25300
+ // SDS-R: Track missing dependencies in repair manager
25301
+ this.repairManager?.markDependenciesMissing(missingDependencies);
24734
25302
  this.safeSendEvent(MessageChannelEvent.InMessageMissing, {
24735
25303
  detail: Array.from(missingDependencies)
24736
25304
  });
@@ -24776,11 +25344,13 @@ class MessageChannel extends TypedEventEmitter {
24776
25344
  // It's a new message
24777
25345
  if (!message) {
24778
25346
  log.info(this.senderId, "sending new message", messageId);
25347
+ // Get repair requests to include in the message (SDS-R)
25348
+ const repairRequests = this.repairManager?.getRepairRequests(MAX_REPAIR_REQUESTS_PER_MESSAGE) ?? [];
24779
25349
  message = new ContentMessage(messageId, this.channelId, this.senderId, this.localHistory
24780
25350
  .slice(-this.causalHistorySize)
24781
- .map(({ messageId, retrievalHint }) => {
24782
- return { messageId, retrievalHint };
24783
- }), this.lamportTimestamp, this.filter.toBytes(), payload);
25351
+ .map(({ messageId, retrievalHint, senderId }) => {
25352
+ return { messageId, retrievalHint, senderId };
25353
+ }), this.lamportTimestamp, this.filter.toBytes(), payload, repairRequests);
24784
25354
  this.outgoingBuffer.push(message);
24785
25355
  }
24786
25356
  else {
@@ -24819,6 +25389,25 @@ class MessageChannel extends TypedEventEmitter {
24819
25389
  }
24820
25390
  }
24821
25391
  }
25392
+ /**
25393
+ * Check if a message is available (either in localHistory or incomingBuffer)
25394
+ * This prevents treating messages as "missing" when they've already been received
25395
+ * but are waiting in the incoming buffer for their dependencies.
25396
+ *
25397
+ * @param messageId - The ID of the message to check
25398
+ * @private
25399
+ */
25400
+ isMessageAvailable(messageId) {
25401
+ // Check if in local history
25402
+ if (this.localHistory.some((m) => m.messageId === messageId)) {
25403
+ return true;
25404
+ }
25405
+ // Check if in incoming buffer (already received, waiting for dependencies)
25406
+ if (this.incomingBuffer.some((m) => m.messageId === messageId)) {
25407
+ return true;
25408
+ }
25409
+ return false;
25410
+ }
24822
25411
  /**
24823
25412
  * Return true if the message was "delivered"
24824
25413
  *