@waku/sds 0.0.8-d637425.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/package.json CHANGED
@@ -1 +1,93 @@
1
- {"name":"@waku/sds","version":"0.0.8-d637425.0","description":"Scalable Data Sync implementation for the browser. Based on https://github.com/vacp2p/rfc-index/blob/main/vac/raw/sds.md","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"typesVersions":{"*":{"*":["*","dist/*","dist/*/index"]}},"type":"module","author":"Waku Team","homepage":"https://github.com/waku-org/js-waku/tree/master/packages/scalable-data-sync#readme","repository":{"type":"git","url":"https://github.com/waku-org/js-waku.git"},"bugs":{"url":"https://github.com/waku-org/js-waku/issues"},"license":"MIT OR Apache-2.0","keywords":["waku","decentralized","secure","communication","web3","ethereum","dapps","privacy"],"scripts":{"build":"run-s build:**","build:esm":"tsc","build:bundle":"rollup --config rollup.config.js","fix":"run-s fix:*","fix:lint":"eslint src *.js --fix","check":"run-s check:*","check:lint":"eslint src *.js","check:spelling":"cspell \"{README.md,src/**/*.ts}\"","check:tsc":"tsc -p tsconfig.dev.json","prepublish":"npm run build","reset-hard":"git clean -dfx -e .idea && git reset --hard && npm i && npm run build","test":"NODE_ENV=test run-s test:*","test:node":"NODE_ENV=test TS_NODE_PROJECT=./tsconfig.dev.json mocha","test:browser":"NODE_ENV=test karma start karma.conf.cjs"},"engines":{"node":">=22"},"dependencies":{"@libp2p/interface":"2.10.4","@noble/hashes":"^1.7.1","@waku/proto":"0.0.15-d637425.0","@waku/utils":"0.0.28-d637425.0","chai":"^5.1.2","lodash":"^4.17.21"},"devDependencies":{"@rollup/plugin-commonjs":"^25.0.7","@rollup/plugin-json":"^6.0.0","@rollup/plugin-node-resolve":"^15.2.3","@waku/build-utils":"*","allure-commandline":"^2.27.0","allure-mocha":"^2.9.2","cspell":"^8.6.1","fast-check":"^3.19.0","mocha-multi-reporters":"^1.5.1","npm-run-all":"^4.1.5","rollup":"^4.12.0"},"files":["dist","bundle","src/**/*.ts","!**/*.spec.*","!**/*.json","CHANGELOG.md","LICENSE","README.md"]}
1
+ {
2
+ "name": "@waku/sds",
3
+ "version": "0.0.8",
4
+ "description": "Scalable Data Sync implementation for the browser. Based on https://github.com/vacp2p/rfc-index/blob/main/vac/raw/sds.md",
5
+ "types": "./dist/index.d.ts",
6
+ "module": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "typesVersions": {
14
+ "*": {
15
+ "*": [
16
+ "*",
17
+ "dist/*",
18
+ "dist/*/index"
19
+ ]
20
+ }
21
+ },
22
+ "type": "module",
23
+ "author": "Waku Team",
24
+ "homepage": "https://github.com/waku-org/js-waku/tree/master/packages/scalable-data-sync#readme",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/waku-org/js-waku.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/waku-org/js-waku/issues"
31
+ },
32
+ "license": "MIT OR Apache-2.0",
33
+ "keywords": [
34
+ "waku",
35
+ "decentralized",
36
+ "secure",
37
+ "communication",
38
+ "web3",
39
+ "ethereum",
40
+ "dapps",
41
+ "privacy"
42
+ ],
43
+ "scripts": {
44
+ "build": "run-s build:**",
45
+ "build:esm": "tsc",
46
+ "build:bundle": "rollup --config rollup.config.js",
47
+ "fix": "run-s fix:*",
48
+ "fix:lint": "eslint src *.js --fix",
49
+ "check": "run-s check:*",
50
+ "check:lint": "eslint src *.js",
51
+ "check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
52
+ "check:tsc": "tsc -p tsconfig.dev.json",
53
+ "prepublish": "npm run build",
54
+ "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build",
55
+ "test": "NODE_ENV=test run-s test:*",
56
+ "test:node": "NODE_ENV=test TS_NODE_PROJECT=./tsconfig.dev.json mocha",
57
+ "test:browser": "NODE_ENV=test karma start karma.conf.cjs"
58
+ },
59
+ "engines": {
60
+ "node": ">=22"
61
+ },
62
+ "dependencies": {
63
+ "@libp2p/interface": "2.10.4",
64
+ "@noble/hashes": "^1.7.1",
65
+ "@waku/proto": "^0.0.15",
66
+ "@waku/utils": "^0.0.27",
67
+ "chai": "^5.1.2",
68
+ "lodash": "^4.17.21"
69
+ },
70
+ "devDependencies": {
71
+ "@rollup/plugin-commonjs": "^25.0.7",
72
+ "@rollup/plugin-json": "^6.0.0",
73
+ "@rollup/plugin-node-resolve": "^15.2.3",
74
+ "@waku/build-utils": "*",
75
+ "allure-commandline": "^2.27.0",
76
+ "allure-mocha": "^2.9.2",
77
+ "cspell": "^8.6.1",
78
+ "fast-check": "^3.19.0",
79
+ "mocha-multi-reporters": "^1.5.1",
80
+ "npm-run-all": "^4.1.5",
81
+ "rollup": "^4.12.0"
82
+ },
83
+ "files": [
84
+ "dist",
85
+ "bundle",
86
+ "src/**/*.ts",
87
+ "!**/*.spec.*",
88
+ "!**/*.json",
89
+ "CHANGELOG.md",
90
+ "LICENSE",
91
+ "README.md"
92
+ ]
93
+ }
package/src/index.ts CHANGED
@@ -14,8 +14,14 @@ export {
14
14
  type HistoryEntry,
15
15
  type ChannelId,
16
16
  type MessageChannelEvents,
17
- type SenderId,
17
+ type ParticipantId,
18
18
  type MessageId
19
19
  } from "./message_channel/index.js";
20
20
 
21
+ /**
22
+ * @deprecated Use ParticipantId instead. SenderId has been renamed to ParticipantId
23
+ * to better reflect that it represents a channel participant, not just a message sender.
24
+ */
25
+ export type { ParticipantId as SenderId } from "./message_channel/index.js";
26
+
21
27
  export { BloomFilter };
@@ -1,4 +1,4 @@
1
- import { HistoryEntry, Message, MessageId } from "./message.js";
1
+ import { HistoryEntry, Message, MessageId, ParticipantId } from "./message.js";
2
2
 
3
3
  export enum MessageChannelEvent {
4
4
  OutMessageSent = "sds:out:message-sent",
@@ -10,7 +10,13 @@ export enum MessageChannelEvent {
10
10
  OutSyncSent = "sds:out:sync-sent",
11
11
  InSyncReceived = "sds:in:sync-received",
12
12
  InMessageLost = "sds:in:message-irretrievably-lost",
13
- ErrorTask = "sds:error-task"
13
+ ErrorTask = "sds:error-task",
14
+ // SDS-R Repair Events
15
+ RepairRequestQueued = "sds:repair:request-queued",
16
+ RepairRequestSent = "sds:repair:request-sent",
17
+ RepairRequestReceived = "sds:repair:request-received",
18
+ RepairResponseQueued = "sds:repair:response-queued",
19
+ RepairResponseSent = "sds:repair:response-sent"
14
20
  }
15
21
 
16
22
  export type MessageChannelEvents = {
@@ -26,5 +32,24 @@ export type MessageChannelEvents = {
26
32
  [MessageChannelEvent.InMessageLost]: CustomEvent<HistoryEntry[]>;
27
33
  [MessageChannelEvent.OutSyncSent]: CustomEvent<Message>;
28
34
  [MessageChannelEvent.InSyncReceived]: CustomEvent<Message>;
29
- [MessageChannelEvent.ErrorTask]: CustomEvent<any>;
35
+ [MessageChannelEvent.ErrorTask]: CustomEvent<unknown>;
36
+ [MessageChannelEvent.RepairRequestQueued]: CustomEvent<{
37
+ messageId: MessageId;
38
+ tReq: number;
39
+ }>;
40
+ [MessageChannelEvent.RepairRequestSent]: CustomEvent<{
41
+ messageIds: MessageId[];
42
+ carrierMessageId: MessageId;
43
+ }>;
44
+ [MessageChannelEvent.RepairRequestReceived]: CustomEvent<{
45
+ messageIds: MessageId[];
46
+ fromSenderId?: ParticipantId;
47
+ }>;
48
+ [MessageChannelEvent.RepairResponseQueued]: CustomEvent<{
49
+ messageId: MessageId;
50
+ tResp: number;
51
+ }>;
52
+ [MessageChannelEvent.RepairResponseSent]: CustomEvent<{
53
+ messageId: MessageId;
54
+ }>;
30
55
  };
@@ -8,7 +8,7 @@ export {
8
8
  HistoryEntry,
9
9
  Message,
10
10
  MessageId,
11
- SenderId,
11
+ ParticipantId,
12
12
  SyncMessage,
13
13
  isContentMessage,
14
14
  isEphemeralMessage,
@@ -4,19 +4,20 @@ import { Logger } from "@waku/utils";
4
4
  export type MessageId = string;
5
5
  export type HistoryEntry = proto_sds_message.HistoryEntry;
6
6
  export type ChannelId = string;
7
- export type SenderId = string;
7
+ export type ParticipantId = string;
8
8
 
9
9
  const log = new Logger("sds:message");
10
10
 
11
11
  export class Message implements proto_sds_message.SdsMessage {
12
12
  public constructor(
13
- public messageId: string,
13
+ public messageId: MessageId,
14
14
  public channelId: string,
15
- public senderId: string,
15
+ public senderId: ParticipantId,
16
16
  public causalHistory: proto_sds_message.HistoryEntry[],
17
17
  public lamportTimestamp?: bigint | undefined,
18
18
  public bloomFilter?: Uint8Array<ArrayBufferLike> | undefined,
19
19
  public content?: Uint8Array<ArrayBufferLike> | undefined,
20
+ public repairRequest: proto_sds_message.HistoryEntry[] = [],
20
21
  /**
21
22
  * Not encoded, set after it is sent, used to include in follow-up messages
22
23
  */
@@ -38,7 +39,8 @@ export class Message implements proto_sds_message.SdsMessage {
38
39
  causalHistory,
39
40
  lamportTimestamp,
40
41
  bloomFilter,
41
- content
42
+ content,
43
+ repairRequest
42
44
  } = proto_sds_message.SdsMessage.decode(data);
43
45
 
44
46
  if (testContentMessage({ lamportTimestamp, content })) {
@@ -49,7 +51,8 @@ export class Message implements proto_sds_message.SdsMessage {
49
51
  causalHistory,
50
52
  lamportTimestamp!,
51
53
  bloomFilter,
52
- content!
54
+ content!,
55
+ repairRequest
53
56
  );
54
57
  }
55
58
 
@@ -61,7 +64,8 @@ export class Message implements proto_sds_message.SdsMessage {
61
64
  causalHistory,
62
65
  undefined,
63
66
  bloomFilter,
64
- content!
67
+ content!,
68
+ repairRequest
65
69
  );
66
70
  }
67
71
 
@@ -73,7 +77,8 @@ export class Message implements proto_sds_message.SdsMessage {
73
77
  causalHistory,
74
78
  lamportTimestamp!,
75
79
  bloomFilter,
76
- undefined
80
+ undefined,
81
+ repairRequest
77
82
  );
78
83
  }
79
84
  log.error(
@@ -90,13 +95,14 @@ export class Message implements proto_sds_message.SdsMessage {
90
95
 
91
96
  export class SyncMessage extends Message {
92
97
  public constructor(
93
- public messageId: string,
98
+ public messageId: MessageId,
94
99
  public channelId: string,
95
- public senderId: string,
100
+ public senderId: ParticipantId,
96
101
  public causalHistory: proto_sds_message.HistoryEntry[],
97
102
  public lamportTimestamp: bigint,
98
103
  public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
99
104
  public content: undefined,
105
+ public repairRequest: proto_sds_message.HistoryEntry[] = [],
100
106
  /**
101
107
  * Not encoded, set after it is sent, used to include in follow-up messages
102
108
  */
@@ -110,6 +116,7 @@ export class SyncMessage extends Message {
110
116
  lamportTimestamp,
111
117
  bloomFilter,
112
118
  content,
119
+ repairRequest,
113
120
  retrievalHint
114
121
  );
115
122
  }
@@ -134,13 +141,14 @@ export function isSyncMessage(
134
141
 
135
142
  export class EphemeralMessage extends Message {
136
143
  public constructor(
137
- public messageId: string,
144
+ public messageId: MessageId,
138
145
  public channelId: string,
139
- public senderId: string,
146
+ public senderId: ParticipantId,
140
147
  public causalHistory: proto_sds_message.HistoryEntry[],
141
148
  public lamportTimestamp: undefined,
142
149
  public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
143
150
  public content: Uint8Array<ArrayBufferLike>,
151
+ public repairRequest: proto_sds_message.HistoryEntry[] = [],
144
152
  /**
145
153
  * Not encoded, set after it is sent, used to include in follow-up messages
146
154
  */
@@ -157,6 +165,7 @@ export class EphemeralMessage extends Message {
157
165
  lamportTimestamp,
158
166
  bloomFilter,
159
167
  content,
168
+ repairRequest,
160
169
  retrievalHint
161
170
  );
162
171
  }
@@ -182,13 +191,14 @@ function testEphemeralMessage(message: {
182
191
 
183
192
  export class ContentMessage extends Message {
184
193
  public constructor(
185
- public messageId: string,
194
+ public messageId: MessageId,
186
195
  public channelId: string,
187
- public senderId: string,
196
+ public senderId: ParticipantId,
188
197
  public causalHistory: proto_sds_message.HistoryEntry[],
189
198
  public lamportTimestamp: bigint,
190
199
  public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
191
200
  public content: Uint8Array<ArrayBufferLike>,
201
+ public repairRequest: proto_sds_message.HistoryEntry[] = [],
192
202
  /**
193
203
  * Not encoded, set after it is sent, used to include in follow-up messages
194
204
  */
@@ -205,6 +215,7 @@ export class ContentMessage extends Message {
205
215
  lamportTimestamp,
206
216
  bloomFilter,
207
217
  content,
218
+ repairRequest,
208
219
  retrievalHint
209
220
  );
210
221
  }
@@ -18,15 +18,21 @@ import {
18
18
  isSyncMessage,
19
19
  Message,
20
20
  MessageId,
21
- SenderId,
21
+ ParticipantId,
22
22
  SyncMessage
23
23
  } from "./message.js";
24
+ import { RepairConfig, RepairManager } from "./repair/repair.js";
24
25
 
25
26
  export const DEFAULT_BLOOM_FILTER_OPTIONS = {
26
27
  capacity: 10000,
27
28
  errorRate: 0.001
28
29
  };
29
30
 
31
+ /**
32
+ * Maximum number of repair requests to include in a single message
33
+ */
34
+ const MAX_REPAIR_REQUESTS_PER_MESSAGE = 3;
35
+
30
36
  const DEFAULT_CAUSAL_HISTORY_SIZE = 200;
31
37
  const DEFAULT_POSSIBLE_ACKS_THRESHOLD = 2;
32
38
 
@@ -46,6 +52,15 @@ export interface MessageChannelOptions {
46
52
  * How many possible acks does it take to consider it a definitive ack.
47
53
  */
48
54
  possibleAcksThreshold?: number;
55
+ /**
56
+ * Whether to enable SDS-R repair protocol.
57
+ * @default true
58
+ */
59
+ enableRepair?: boolean;
60
+ /**
61
+ * SDS-R repair configuration. Only used if enableRepair is true.
62
+ */
63
+ repairConfig?: RepairConfig;
49
64
  }
50
65
 
51
66
  export type ILocalHistory = Pick<
@@ -55,7 +70,7 @@ export type ILocalHistory = Pick<
55
70
 
56
71
  export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
57
72
  public readonly channelId: ChannelId;
58
- public readonly senderId: SenderId;
73
+ public readonly senderId: ParticipantId;
59
74
  private lamportTimestamp: bigint;
60
75
  private filter: DefaultBloomFilter;
61
76
  private outgoingBuffer: ContentMessage[];
@@ -66,6 +81,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
66
81
  private readonly causalHistorySize: number;
67
82
  private readonly possibleAcksThreshold: number;
68
83
  private readonly timeoutForLostMessagesMs?: number;
84
+ private readonly repairManager?: RepairManager;
69
85
 
70
86
  private tasks: Task[] = [];
71
87
  private handlers: Handlers = {
@@ -88,7 +104,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
88
104
 
89
105
  public constructor(
90
106
  channelId: ChannelId,
91
- senderId: SenderId,
107
+ senderId: ParticipantId,
92
108
  options: MessageChannelOptions = {},
93
109
  localHistory: ILocalHistory = new MemLocalHistory()
94
110
  ) {
@@ -109,6 +125,17 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
109
125
  options.possibleAcksThreshold ?? DEFAULT_POSSIBLE_ACKS_THRESHOLD;
110
126
  this.timeReceived = new Map();
111
127
  this.timeoutForLostMessagesMs = options.timeoutForLostMessagesMs;
128
+
129
+ // Only construct RepairManager if repair is enabled (default: true)
130
+ if (options.enableRepair ?? true) {
131
+ this.repairManager = new RepairManager(
132
+ senderId,
133
+ options.repairConfig,
134
+ (event: string, detail: unknown) => {
135
+ this.safeSendEvent(event as MessageChannelEvent, { detail });
136
+ }
137
+ );
138
+ }
112
139
  }
113
140
 
114
141
  public static getMessageId(payload: Uint8Array): MessageId {
@@ -272,9 +299,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
272
299
  );
273
300
  const missingDependencies = message.causalHistory.filter(
274
301
  (messageHistoryEntry) =>
275
- !this.localHistory.some(
276
- ({ messageId }) => messageId === messageHistoryEntry.messageId
277
- )
302
+ !this.isMessageAvailable(messageHistoryEntry.messageId)
278
303
  );
279
304
  if (missingDependencies.length === 0) {
280
305
  if (isContentMessage(message) && this.deliverMessage(message)) {
@@ -355,6 +380,44 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
355
380
  );
356
381
  }
357
382
 
383
+ /**
384
+ * Sweep repair incoming buffer and rebroadcast messages ready for repair.
385
+ * Per SDS-R spec: periodically check for repair responses that are due.
386
+ *
387
+ * @param callback - callback to rebroadcast the message
388
+ * @returns Promise that resolves when all ready repairs have been sent
389
+ */
390
+ public async sweepRepairIncomingBuffer(
391
+ callback?: (message: Message) => Promise<boolean>
392
+ ): Promise<Message[]> {
393
+ const repairsToSend =
394
+ this.repairManager?.sweepIncomingBuffer(this.localHistory) ?? [];
395
+
396
+ if (callback) {
397
+ for (const message of repairsToSend) {
398
+ try {
399
+ await callback(message);
400
+ log.info(
401
+ this.senderId,
402
+ "repair message rebroadcast",
403
+ message.messageId
404
+ );
405
+
406
+ // Emit RepairResponseSent event
407
+ this.safeSendEvent(MessageChannelEvent.RepairResponseSent, {
408
+ detail: {
409
+ messageId: message.messageId
410
+ }
411
+ });
412
+ } catch (error) {
413
+ log.error("Failed to rebroadcast repair message:", error);
414
+ }
415
+ }
416
+ }
417
+
418
+ return repairsToSend;
419
+ }
420
+
358
421
  /**
359
422
  * Send a sync message to the SDS channel.
360
423
  *
@@ -369,6 +432,12 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
369
432
  callback?: (message: SyncMessage) => Promise<boolean>
370
433
  ): Promise<boolean> {
371
434
  this.lamportTimestamp = lamportTimestampIncrement(this.lamportTimestamp);
435
+
436
+ // Get repair requests to include in sync message (SDS-R)
437
+ const repairRequests =
438
+ this.repairManager?.getRepairRequests(MAX_REPAIR_REQUESTS_PER_MESSAGE) ??
439
+ [];
440
+
372
441
  const message = new SyncMessage(
373
442
  // does not need to be secure randomness
374
443
  `sync-${Math.random().toString(36).substring(2)}`,
@@ -376,18 +445,22 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
376
445
  this.senderId,
377
446
  this.localHistory
378
447
  .slice(-this.causalHistorySize)
379
- .map(({ messageId, retrievalHint }) => {
380
- return { messageId, retrievalHint };
448
+ .map(({ messageId, retrievalHint, senderId }) => {
449
+ return { messageId, retrievalHint, senderId };
381
450
  }),
382
451
  this.lamportTimestamp,
383
452
  this.filter.toBytes(),
384
- undefined
453
+ undefined,
454
+ repairRequests
385
455
  );
386
456
 
387
- if (!message.causalHistory || message.causalHistory.length === 0) {
457
+ if (
458
+ (!message.causalHistory || message.causalHistory.length === 0) &&
459
+ repairRequests.length === 0
460
+ ) {
388
461
  log.info(
389
462
  this.senderId,
390
- "no causal history in sync message, aborting sending"
463
+ "no causal history and no repair requests in sync message, aborting sending"
391
464
  );
392
465
  return false;
393
466
  }
@@ -399,6 +472,17 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
399
472
  this.safeSendEvent(MessageChannelEvent.OutSyncSent, {
400
473
  detail: message
401
474
  });
475
+
476
+ // Emit RepairRequestSent event if repair requests were included
477
+ if (repairRequests.length > 0) {
478
+ this.safeSendEvent(MessageChannelEvent.RepairRequestSent, {
479
+ detail: {
480
+ messageIds: repairRequests.map((r) => r.messageId),
481
+ carrierMessageId: message.messageId
482
+ }
483
+ });
484
+ }
485
+
402
486
  return true;
403
487
  } catch (error) {
404
488
  log.error(
@@ -464,6 +548,26 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
464
548
  detail: message
465
549
  });
466
550
  }
551
+
552
+ // SDS-R: Handle received message in repair manager
553
+ this.repairManager?.markMessageReceived(message.messageId);
554
+
555
+ // SDS-R: Process incoming repair requests
556
+ if (message.repairRequest && message.repairRequest.length > 0) {
557
+ // Emit RepairRequestReceived event
558
+ this.safeSendEvent(MessageChannelEvent.RepairRequestReceived, {
559
+ detail: {
560
+ messageIds: message.repairRequest.map((r) => r.messageId),
561
+ fromSenderId: message.senderId
562
+ }
563
+ });
564
+
565
+ this.repairManager?.processIncomingRepairRequests(
566
+ message.repairRequest,
567
+ this.localHistory
568
+ );
569
+ }
570
+
467
571
  this.reviewAckStatus(message);
468
572
  if (isContentMessage(message)) {
469
573
  this.filter.insert(message.messageId);
@@ -471,9 +575,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
471
575
 
472
576
  const missingDependencies = message.causalHistory.filter(
473
577
  (messageHistoryEntry) =>
474
- !this.localHistory.some(
475
- ({ messageId }) => messageId === messageHistoryEntry.messageId
476
- )
578
+ !this.isMessageAvailable(messageHistoryEntry.messageId)
477
579
  );
478
580
 
479
581
  if (missingDependencies.length > 0) {
@@ -487,6 +589,9 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
487
589
  missingDependencies.map((ch) => ch.messageId)
488
590
  );
489
591
 
592
+ // SDS-R: Track missing dependencies in repair manager
593
+ this.repairManager?.markDependenciesMissing(missingDependencies);
594
+
490
595
  this.safeSendEvent(MessageChannelEvent.InMessageMissing, {
491
596
  detail: Array.from(missingDependencies)
492
597
  });
@@ -549,18 +654,26 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
549
654
  // It's a new message
550
655
  if (!message) {
551
656
  log.info(this.senderId, "sending new message", messageId);
657
+
658
+ // Get repair requests to include in the message (SDS-R)
659
+ const repairRequests =
660
+ this.repairManager?.getRepairRequests(
661
+ MAX_REPAIR_REQUESTS_PER_MESSAGE
662
+ ) ?? [];
663
+
552
664
  message = new ContentMessage(
553
665
  messageId,
554
666
  this.channelId,
555
667
  this.senderId,
556
668
  this.localHistory
557
669
  .slice(-this.causalHistorySize)
558
- .map(({ messageId, retrievalHint }) => {
559
- return { messageId, retrievalHint };
670
+ .map(({ messageId, retrievalHint, senderId }) => {
671
+ return { messageId, retrievalHint, senderId };
560
672
  }),
561
673
  this.lamportTimestamp,
562
674
  this.filter.toBytes(),
563
- payload
675
+ payload,
676
+ repairRequests
564
677
  );
565
678
 
566
679
  this.outgoingBuffer.push(message);
@@ -616,6 +729,26 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
616
729
  }
617
730
  }
618
731
 
732
+ /**
733
+ * Check if a message is available (either in localHistory or incomingBuffer)
734
+ * This prevents treating messages as "missing" when they've already been received
735
+ * but are waiting in the incoming buffer for their dependencies.
736
+ *
737
+ * @param messageId - The ID of the message to check
738
+ * @private
739
+ */
740
+ private isMessageAvailable(messageId: MessageId): boolean {
741
+ // Check if in local history
742
+ if (this.localHistory.some((m) => m.messageId === messageId)) {
743
+ return true;
744
+ }
745
+ // Check if in incoming buffer (already received, waiting for dependencies)
746
+ if (this.incomingBuffer.some((m) => m.messageId === messageId)) {
747
+ return true;
748
+ }
749
+ return false;
750
+ }
751
+
619
752
  /**
620
753
  * Return true if the message was "delivered"
621
754
  *
@@ -657,6 +790,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
657
790
  }
658
791
 
659
792
  this.localHistory.push(message);
793
+
660
794
  return true;
661
795
  }
662
796