@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.
- package/CHANGELOG.md +26 -0
- package/bundle/index.js +614 -25
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js.map +1 -1
- package/dist/message_channel/events.d.ts +27 -3
- package/dist/message_channel/events.js +6 -0
- package/dist/message_channel/events.js.map +1 -1
- package/dist/message_channel/index.d.ts +1 -1
- package/dist/message_channel/message.d.ts +17 -13
- package/dist/message_channel/message.js +19 -11
- package/dist/message_channel/message.js.map +1 -1
- package/dist/message_channel/message_channel.d.ts +31 -3
- package/dist/message_channel/message_channel.js +99 -10
- package/dist/message_channel/message_channel.js.map +1 -1
- package/dist/message_channel/repair/buffers.d.ts +106 -0
- package/dist/message_channel/repair/buffers.js +206 -0
- package/dist/message_channel/repair/buffers.js.map +1 -0
- package/dist/message_channel/repair/repair.d.ts +92 -0
- package/dist/message_channel/repair/repair.js +209 -0
- package/dist/message_channel/repair/repair.js.map +1 -0
- package/dist/message_channel/repair/utils.d.ts +40 -0
- package/dist/message_channel/repair/utils.js +61 -0
- package/dist/message_channel/repair/utils.js.map +1 -0
- package/package.json +93 -1
- package/src/index.ts +7 -1
- package/src/message_channel/events.ts +28 -3
- package/src/message_channel/index.ts +1 -1
- package/src/message_channel/message.ts +24 -13
- package/src/message_channel/message_channel.ts +151 -17
- package/src/message_channel/repair/buffers.ts +277 -0
- package/src/message_channel/repair/repair.ts +331 -0
- 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$
|
|
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$
|
|
24272
|
+
log$3.error("message received was of unknown type", lamportTimestamp, content);
|
|
24241
24273
|
}
|
|
24242
24274
|
catch (err) {
|
|
24243
|
-
log$
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
*
|