@waku/sds 0.0.4-383e0b2.0 → 0.0.4-4c18ca2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { TypedEventEmitter } from "@libp2p/interface";
2
2
  import { sha256 } from "@noble/hashes/sha256";
3
3
  import { bytesToHex } from "@noble/hashes/utils";
4
+ import { Logger } from "@waku/utils";
4
5
 
5
6
  import { DefaultBloomFilter } from "../bloom_filter/bloom.js";
6
7
 
@@ -21,6 +22,8 @@ export const DEFAULT_BLOOM_FILTER_OPTIONS = {
21
22
  const DEFAULT_CAUSAL_HISTORY_SIZE = 2;
22
23
  const DEFAULT_RECEIVED_MESSAGE_TIMEOUT = 1000 * 60 * 5; // 5 minutes
23
24
 
25
+ const log = new Logger("sds:message-channel");
26
+
24
27
  interface MessageChannelOptions {
25
28
  causalHistorySize?: number;
26
29
  receivedMessageTimeoutEnabled?: boolean;
@@ -28,13 +31,13 @@ interface MessageChannelOptions {
28
31
  }
29
32
 
30
33
  export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
34
+ public readonly channelId: ChannelId;
31
35
  private lamportTimestamp: number;
32
36
  private filter: DefaultBloomFilter;
33
37
  private outgoingBuffer: Message[];
34
38
  private acknowledgements: Map<string, number>;
35
39
  private incomingBuffer: Message[];
36
40
  private localHistory: { timestamp: number; historyEntry: HistoryEntry }[];
37
- public channelId: ChannelId;
38
41
  private causalHistorySize: number;
39
42
  private acknowledgementCount: number;
40
43
  private timeReceived: Map<string, number>;
@@ -82,8 +85,30 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
82
85
  options.receivedMessageTimeout ?? DEFAULT_RECEIVED_MESSAGE_TIMEOUT;
83
86
  }
84
87
 
85
- // Periodically called by the library consumer to process async operations
86
- // in a sequential manner.
88
+ public static getMessageId(payload: Uint8Array): string {
89
+ return bytesToHex(sha256(payload));
90
+ }
91
+
92
+ /**
93
+ * Processes all queued tasks sequentially to ensure proper message ordering.
94
+ *
95
+ * This method should be called periodically by the library consumer to execute
96
+ * queued send/receive operations in the correct sequence.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const channel = new MessageChannel("my-channel");
101
+ *
102
+ * // Queue some operations
103
+ * await channel.sendMessage(payload, callback);
104
+ * channel.receiveMessage(incomingMessage);
105
+ *
106
+ * // Process all queued operations
107
+ * await channel.processTasks();
108
+ * ```
109
+ *
110
+ * @throws Will emit a 'taskError' event if any task fails, but continues processing remaining tasks
111
+ */
87
112
  public async processTasks(): Promise<void> {
88
113
  while (this.tasks.length > 0) {
89
114
  const item = this.tasks.shift();
@@ -91,35 +116,33 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
91
116
  continue;
92
117
  }
93
118
 
94
- // Use a generic helper function to ensure type safety
95
119
  await this.executeTask(item);
96
120
  }
97
121
  }
98
122
 
99
- private async executeTask<A extends Command>(item: Task<A>): Promise<void> {
100
- const handler = this.handlers[item.command];
101
- await handler(item.params as ParamsByAction[A]);
102
- }
103
-
104
- public static getMessageId(payload: Uint8Array): string {
105
- return bytesToHex(sha256(payload));
106
- }
107
-
108
123
  /**
109
- * Send a message to the SDS channel.
124
+ * Queues a message to be sent on this channel.
110
125
  *
111
- * Increments the lamport timestamp, constructs a `Message` object
112
- * with the given payload, and adds it to the outgoing buffer.
126
+ * The message will be processed sequentially when processTasks() is called.
127
+ * This ensures proper lamport timestamp ordering and causal history tracking.
113
128
  *
114
- * If the callback is successful, the message is also added to
115
- * the bloom filter and message history. In the context of
116
- * Waku, this likely means the message was published via
117
- * light push or relay.
129
+ * @param payload - The message content as a byte array
130
+ * @param callback - Optional callback function called after the message is processed
131
+ * @returns Promise that resolves when the message is queued (not sent)
118
132
  *
119
- * See https://rfc.vac.dev/vac/raw/sds/#send-message
133
+ * @example
134
+ * ```typescript
135
+ * const channel = new MessageChannel("chat-room");
136
+ * const message = new TextEncoder().encode("Hello, world!");
120
137
  *
121
- * @param payload - The payload to send.
122
- * @param callback - A callback function that returns a boolean indicating whether the message was sent successfully.
138
+ * await channel.sendMessage(message, async (processedMessage) => {
139
+ * console.log("Message processed:", processedMessage.messageId);
140
+ * return { success: true };
141
+ * });
142
+ *
143
+ * // Actually send the message
144
+ * await channel.processTasks();
145
+ * ```
123
146
  */
124
147
  public async sendMessage(
125
148
  payload: Uint8Array,
@@ -137,49 +160,6 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
137
160
  });
138
161
  }
139
162
 
140
- public async _sendMessage(
141
- payload: Uint8Array,
142
- callback?: (message: Message) => Promise<{
143
- success: boolean;
144
- retrievalHint?: Uint8Array;
145
- }>
146
- ): Promise<void> {
147
- this.lamportTimestamp++;
148
-
149
- const messageId = MessageChannel.getMessageId(payload);
150
-
151
- const message: Message = {
152
- messageId,
153
- channelId: this.channelId,
154
- lamportTimestamp: this.lamportTimestamp,
155
- causalHistory: this.localHistory
156
- .slice(-this.causalHistorySize)
157
- .map(({ historyEntry }) => historyEntry),
158
- bloomFilter: this.filter.toBytes(),
159
- content: payload
160
- };
161
-
162
- this.outgoingBuffer.push(message);
163
-
164
- if (callback) {
165
- const { success, retrievalHint } = await callback(message);
166
- if (success) {
167
- this.filter.insert(messageId);
168
- this.localHistory.push({
169
- timestamp: this.lamportTimestamp,
170
- historyEntry: {
171
- messageId,
172
- retrievalHint
173
- }
174
- });
175
- this.timeReceived.set(messageId, Date.now());
176
- this.safeDispatchEvent(MessageChannelEvent.MessageSent, {
177
- detail: message
178
- });
179
- }
180
- }
181
- }
182
-
183
163
  /**
184
164
  * Sends a short-lived message without synchronization or reliability requirements.
185
165
  *
@@ -206,39 +186,25 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
206
186
  });
207
187
  }
208
188
 
209
- public async _sendEphemeralMessage(
210
- payload: Uint8Array,
211
- callback?: (message: Message) => Promise<boolean>
212
- ): Promise<void> {
213
- const message: Message = {
214
- messageId: MessageChannel.getMessageId(payload),
215
- channelId: this.channelId,
216
- content: payload,
217
- lamportTimestamp: undefined,
218
- causalHistory: [],
219
- bloomFilter: undefined
220
- };
221
-
222
- if (callback) {
223
- await callback(message);
224
- }
225
- }
226
-
227
189
  /**
228
- * Process a received SDS message for this channel.
190
+ * Queues a received message for processing.
191
+ *
192
+ * The message will be processed when processTasks() is called, ensuring
193
+ * proper dependency resolution and causal ordering.
229
194
  *
230
- * Review the acknowledgement status of messages in the outgoing buffer
231
- * by inspecting the received message's bloom filter and causal history.
232
- * Add the received message to the bloom filter.
233
- * If the local history contains every message in the received message's
234
- * causal history, deliver the message. Otherwise, add the message to the
235
- * incoming buffer.
195
+ * @param message - The message to receive and process
236
196
  *
237
- * See https://rfc.vac.dev/vac/raw/sds/#receive-message
197
+ * @example
198
+ * ```typescript
199
+ * const channel = new MessageChannel("chat-room");
238
200
  *
239
- * @param message - The received SDS message.
201
+ * // Receive a message from the network
202
+ * channel.receiveMessage(incomingMessage);
203
+ *
204
+ * // Process the received message
205
+ * await channel.processTasks();
206
+ * ```
240
207
  */
241
-
242
208
  public receiveMessage(message: Message): void {
243
209
  this.tasks.push({
244
210
  command: Command.Receive,
@@ -248,67 +214,17 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
248
214
  });
249
215
  }
250
216
 
251
- public _receiveMessage(message: Message): void {
252
- if (
253
- message.content &&
254
- message.content.length > 0 &&
255
- this.timeReceived.has(message.messageId)
256
- ) {
257
- // Received a duplicate message
258
- return;
259
- }
260
-
261
- if (!message.lamportTimestamp) {
262
- // Messages with no timestamp are ephemeral messages and should be delivered immediately
263
- this.deliverMessage(message);
264
- return;
265
- }
266
- if (message.content?.length === 0) {
267
- this.safeDispatchEvent(MessageChannelEvent.SyncReceived, {
268
- detail: message
269
- });
270
- } else {
271
- this.safeDispatchEvent(MessageChannelEvent.MessageReceived, {
272
- detail: message
273
- });
274
- }
275
- // review ack status
276
- this.reviewAckStatus(message);
277
- // add to bloom filter (skip for messages with empty content)
278
- if (message.content?.length && message.content.length > 0) {
279
- this.filter.insert(message.messageId);
280
- }
281
- // verify causal history
282
- const dependenciesMet = message.causalHistory.every((historyEntry) =>
283
- this.localHistory.some(
284
- ({ historyEntry: { messageId } }) =>
285
- messageId === historyEntry.messageId
286
- )
287
- );
288
- if (!dependenciesMet) {
289
- this.incomingBuffer.push(message);
290
- this.timeReceived.set(message.messageId, Date.now());
291
- } else {
292
- this.deliverMessage(message);
293
- this.safeDispatchEvent(MessageChannelEvent.MessageDelivered, {
294
- detail: {
295
- messageId: message.messageId,
296
- sentOrReceived: "received"
297
- }
298
- });
299
- }
300
- }
301
-
302
- // https://rfc.vac.dev/vac/raw/sds/#periodic-incoming-buffer-sweep
303
- // Note that even though this function has side effects, it is not async
304
- // and does not need to be called through the queue.
217
+ /**
218
+ * Processes messages in the incoming buffer, delivering those with satisfied dependencies.
219
+ *
220
+ * @returns Array of history entries for messages still missing dependencies
221
+ */
305
222
  public sweepIncomingBuffer(): HistoryEntry[] {
306
223
  const { buffer, missing } = this.incomingBuffer.reduce<{
307
224
  buffer: Message[];
308
225
  missing: Set<HistoryEntry>;
309
226
  }>(
310
227
  ({ buffer, missing }, message) => {
311
- // Check each message for missing dependencies
312
228
  const missingDependencies = message.causalHistory.filter(
313
229
  (messageHistoryEntry) =>
314
230
  !this.localHistory.some(
@@ -317,10 +233,8 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
317
233
  )
318
234
  );
319
235
  if (missingDependencies.length === 0) {
320
- // Any message with no missing dependencies is delivered
321
- // and removed from the buffer (implicitly by not adding it to the new incoming buffer)
322
236
  this.deliverMessage(message);
323
- this.safeDispatchEvent(MessageChannelEvent.MessageDelivered, {
237
+ this.safeSendEvent(MessageChannelEvent.MessageDelivered, {
324
238
  detail: {
325
239
  messageId: message.messageId,
326
240
  sentOrReceived: "received"
@@ -340,8 +254,6 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
340
254
  return { buffer, missing };
341
255
  }
342
256
  }
343
- // Any message with missing dependencies stays in the buffer
344
- // and the missing message IDs are returned for processing.
345
257
  missingDependencies.forEach((dependency) => {
346
258
  missing.add(dependency);
347
259
  });
@@ -352,10 +264,9 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
352
264
  },
353
265
  { buffer: new Array<Message>(), missing: new Set<HistoryEntry>() }
354
266
  );
355
- // Update the incoming buffer to only include messages with no missing dependencies
356
267
  this.incomingBuffer = buffer;
357
268
 
358
- this.safeDispatchEvent(MessageChannelEvent.MissedMessages, {
269
+ this.safeSendEvent(MessageChannelEvent.MissedMessages, {
359
270
  detail: Array.from(missing)
360
271
  });
361
272
 
@@ -367,7 +278,6 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
367
278
  unacknowledged: Message[];
368
279
  possiblyAcknowledged: Message[];
369
280
  } {
370
- // Partition all messages in the outgoing buffer into unacknowledged and possibly acknowledged messages
371
281
  return this.outgoingBuffer.reduce<{
372
282
  unacknowledged: Message[];
373
283
  possiblyAcknowledged: Message[];
@@ -420,13 +330,161 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
420
330
  };
421
331
 
422
332
  if (callback) {
423
- await callback(message);
424
- this.safeDispatchEvent(MessageChannelEvent.SyncSent, {
333
+ try {
334
+ await callback(message);
335
+ this.safeSendEvent(MessageChannelEvent.SyncSent, {
336
+ detail: message
337
+ });
338
+ return true;
339
+ } catch (error) {
340
+ log.error("Callback execution failed in sendSyncMessage:", error);
341
+ throw error;
342
+ }
343
+ }
344
+ return false;
345
+ }
346
+
347
+ private _receiveMessage(message: Message): void {
348
+ const isDuplicate =
349
+ message.content &&
350
+ message.content.length > 0 &&
351
+ this.timeReceived.has(message.messageId);
352
+
353
+ if (isDuplicate) {
354
+ return;
355
+ }
356
+
357
+ if (!message.lamportTimestamp) {
358
+ this.deliverMessage(message);
359
+ return;
360
+ }
361
+ if (message.content?.length === 0) {
362
+ this.safeSendEvent(MessageChannelEvent.SyncReceived, {
363
+ detail: message
364
+ });
365
+ } else {
366
+ this.safeSendEvent(MessageChannelEvent.MessageReceived, {
425
367
  detail: message
426
368
  });
427
- return true;
428
369
  }
429
- return false;
370
+ this.reviewAckStatus(message);
371
+ if (message.content?.length && message.content.length > 0) {
372
+ this.filter.insert(message.messageId);
373
+ }
374
+ const dependenciesMet = message.causalHistory.every((historyEntry) =>
375
+ this.localHistory.some(
376
+ ({ historyEntry: { messageId } }) =>
377
+ messageId === historyEntry.messageId
378
+ )
379
+ );
380
+ if (!dependenciesMet) {
381
+ this.incomingBuffer.push(message);
382
+ this.timeReceived.set(message.messageId, Date.now());
383
+ } else {
384
+ this.deliverMessage(message);
385
+ this.safeSendEvent(MessageChannelEvent.MessageDelivered, {
386
+ detail: {
387
+ messageId: message.messageId,
388
+ sentOrReceived: "received"
389
+ }
390
+ });
391
+ }
392
+ }
393
+
394
+ private async executeTask<A extends Command>(item: Task<A>): Promise<void> {
395
+ try {
396
+ const handler = this.handlers[item.command];
397
+ await handler(item.params as ParamsByAction[A]);
398
+ } catch (error) {
399
+ log.error(`Task execution failed for command ${item.command}:`, error);
400
+ this.dispatchEvent(
401
+ new CustomEvent("taskError", {
402
+ detail: { command: item.command, error, params: item.params }
403
+ })
404
+ );
405
+ }
406
+ }
407
+
408
+ private safeSendEvent<T extends MessageChannelEvent>(
409
+ event: T,
410
+ eventInit?: CustomEventInit
411
+ ): void {
412
+ try {
413
+ this.dispatchEvent(new CustomEvent(event, eventInit));
414
+ } catch (error) {
415
+ log.error(`Failed to dispatch event ${event}:`, error);
416
+ }
417
+ }
418
+
419
+ private async _sendMessage(
420
+ payload: Uint8Array,
421
+ callback?: (message: Message) => Promise<{
422
+ success: boolean;
423
+ retrievalHint?: Uint8Array;
424
+ }>
425
+ ): Promise<void> {
426
+ this.lamportTimestamp++;
427
+
428
+ const messageId = MessageChannel.getMessageId(payload);
429
+
430
+ const message: Message = {
431
+ messageId,
432
+ channelId: this.channelId,
433
+ lamportTimestamp: this.lamportTimestamp,
434
+ causalHistory: this.localHistory
435
+ .slice(-this.causalHistorySize)
436
+ .map(({ historyEntry }) => historyEntry),
437
+ bloomFilter: this.filter.toBytes(),
438
+ content: payload
439
+ };
440
+
441
+ this.outgoingBuffer.push(message);
442
+
443
+ if (callback) {
444
+ try {
445
+ const { success, retrievalHint } = await callback(message);
446
+ if (success) {
447
+ this.filter.insert(messageId);
448
+ this.localHistory.push({
449
+ timestamp: this.lamportTimestamp,
450
+ historyEntry: {
451
+ messageId,
452
+ retrievalHint
453
+ }
454
+ });
455
+ this.timeReceived.set(messageId, Date.now());
456
+ this.safeSendEvent(MessageChannelEvent.MessageSent, {
457
+ detail: message
458
+ });
459
+ }
460
+ } catch (error) {
461
+ log.error("Callback execution failed in _sendMessage:", error);
462
+ throw error;
463
+ }
464
+ }
465
+ }
466
+
467
+ private async _sendEphemeralMessage(
468
+ payload: Uint8Array,
469
+ callback?: (message: Message) => Promise<boolean>
470
+ ): Promise<void> {
471
+ const message: Message = {
472
+ messageId: MessageChannel.getMessageId(payload),
473
+ channelId: this.channelId,
474
+ content: payload,
475
+ lamportTimestamp: undefined,
476
+ causalHistory: [],
477
+ bloomFilter: undefined
478
+ };
479
+
480
+ if (callback) {
481
+ try {
482
+ await callback(message);
483
+ } catch (error) {
484
+ log.error("Callback execution failed in _sendEphemeralMessage:", error);
485
+ throw error;
486
+ }
487
+ }
430
488
  }
431
489
 
432
490
  // See https://rfc.vac.dev/vac/raw/sds/#deliver-message
@@ -470,14 +528,13 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
470
528
  // to determine the acknowledgement status of messages in the outgoing buffer.
471
529
  // See https://rfc.vac.dev/vac/raw/sds/#review-ack-status
472
530
  private reviewAckStatus(receivedMessage: Message): void {
473
- // the participant MUST mark all messages in the received causal_history as acknowledged.
474
531
  receivedMessage.causalHistory.forEach(({ messageId }) => {
475
532
  this.outgoingBuffer = this.outgoingBuffer.filter(
476
533
  ({ messageId: outgoingMessageId }) => {
477
534
  if (outgoingMessageId !== messageId) {
478
535
  return true;
479
536
  }
480
- this.safeDispatchEvent(MessageChannelEvent.MessageAcknowledged, {
537
+ this.safeSendEvent(MessageChannelEvent.MessageAcknowledged, {
481
538
  detail: messageId
482
539
  });
483
540
  return false;
@@ -488,7 +545,6 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
488
545
  this.filter.insert(messageId);
489
546
  }
490
547
  });
491
- // the participant MUST mark all messages included in the bloom_filter as possibly acknowledged
492
548
  if (!receivedMessage.bloomFilter) {
493
549
  return;
494
550
  }
@@ -506,7 +562,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
506
562
  const count = (this.acknowledgements.get(message.messageId) ?? 0) + 1;
507
563
  if (count < this.acknowledgementCount) {
508
564
  this.acknowledgements.set(message.messageId, count);
509
- this.safeDispatchEvent(MessageChannelEvent.PartialAcknowledgement, {
565
+ this.safeSendEvent(MessageChannelEvent.PartialAcknowledgement, {
510
566
  detail: {
511
567
  messageId: message.messageId,
512
568
  count