1
+ import { TypedEventEmitter } from "@libp2p/interface" ;
1
2
import { sha256 } from "@noble/hashes/sha256" ;
2
3
import { bytesToHex } from "@noble/hashes/utils" ;
3
4
import { proto_sds_message } from "@waku/proto" ;
4
5
5
6
import { DefaultBloomFilter } from "./bloom.js" ;
6
7
8
+ export enum MessageChannelEvent {
9
+ MessageDelivered = "messageDelivered"
10
+ }
11
+ type MessageChannelEvents = {
12
+ [ MessageChannelEvent . MessageDelivered ] : CustomEvent < string > ;
13
+ } ;
14
+
7
15
export type Message = proto_sds_message . SdsMessage ;
8
16
export type ChannelId = string ;
9
17
@@ -15,7 +23,14 @@ export const DEFAULT_BLOOM_FILTER_OPTIONS = {
15
23
const DEFAULT_CAUSAL_HISTORY_SIZE = 2 ;
16
24
const DEFAULT_RECEIVED_MESSAGE_TIMEOUT = 1000 * 60 * 5 ; // 5 minutes
17
25
18
- export class MessageChannel {
26
+ interface MessageChannelOptions {
27
+ causalHistorySize ?: number ;
28
+ receivedMessageTimeoutEnabled ?: boolean ;
29
+ receivedMessageTimeout ?: number ;
30
+ deliveredMessageCallback ?: ( messageId : string ) => void ;
31
+ }
32
+
33
+ export class MessageChannel extends TypedEventEmitter < MessageChannelEvents > {
19
34
private lamportTimestamp : number ;
20
35
private filter : DefaultBloomFilter ;
21
36
private outgoingBuffer : Message [ ] ;
@@ -26,23 +41,31 @@ export class MessageChannel {
26
41
private causalHistorySize : number ;
27
42
private acknowledgementCount : number ;
28
43
private timeReceived : Map < string , number > ;
44
+ private receivedMessageTimeoutEnabled : boolean ;
45
+ private receivedMessageTimeout : number ;
46
+ private deliveredMessageCallback ?: ( messageId : string ) => void ;
29
47
30
48
public constructor (
31
49
channelId : ChannelId ,
32
- causalHistorySize : number = DEFAULT_CAUSAL_HISTORY_SIZE ,
33
- private receivedMessageTimeoutEnabled : boolean = false ,
34
- private receivedMessageTimeout : number = DEFAULT_RECEIVED_MESSAGE_TIMEOUT
50
+ options : MessageChannelOptions = { }
35
51
) {
52
+ super ( ) ;
36
53
this . channelId = channelId ;
37
54
this . lamportTimestamp = 0 ;
38
55
this . filter = new DefaultBloomFilter ( DEFAULT_BLOOM_FILTER_OPTIONS ) ;
39
56
this . outgoingBuffer = [ ] ;
40
57
this . acknowledgements = new Map ( ) ;
41
58
this . incomingBuffer = [ ] ;
42
59
this . messageIdLog = [ ] ;
43
- this . causalHistorySize = causalHistorySize ;
60
+ this . causalHistorySize =
61
+ options . causalHistorySize ?? DEFAULT_CAUSAL_HISTORY_SIZE ;
44
62
this . acknowledgementCount = this . getAcknowledgementCount ( ) ;
45
63
this . timeReceived = new Map ( ) ;
64
+ this . receivedMessageTimeoutEnabled =
65
+ options . receivedMessageTimeoutEnabled ?? false ;
66
+ this . receivedMessageTimeout =
67
+ options . receivedMessageTimeout ?? DEFAULT_RECEIVED_MESSAGE_TIMEOUT ;
68
+ this . deliveredMessageCallback = options . deliveredMessageCallback ;
46
69
}
47
70
48
71
public static getMessageId ( payload : Uint8Array ) : string {
@@ -95,6 +118,36 @@ export class MessageChannel {
95
118
}
96
119
}
97
120
121
+ /**
122
+ * Sends a short-lived message without synchronization or reliability requirements.
123
+ *
124
+ * Sends a message without a timestamp, causal history, or bloom filter.
125
+ * Ephemeral messages are not added to the outgoing buffer.
126
+ * Upon reception, ephemeral messages are delivered immediately without
127
+ * checking for causal dependencies or including in the local log.
128
+ *
129
+ * See https://rfc.vac.dev/vac/raw/sds/#ephemeral-messages
130
+ *
131
+ * @param payload - The payload to send.
132
+ * @param callback - A callback function that returns a boolean indicating whether the message was sent successfully.
133
+ */
134
+ public sendEphemeralMessage (
135
+ payload : Uint8Array ,
136
+ callback ?: ( message : Message ) => boolean
137
+ ) : void {
138
+ const message : Message = {
139
+ messageId : MessageChannel . getMessageId ( payload ) ,
140
+ channelId : this . channelId ,
141
+ content : payload ,
142
+ lamportTimestamp : undefined ,
143
+ causalHistory : [ ] ,
144
+ bloomFilter : undefined
145
+ } ;
146
+
147
+ if ( callback ) {
148
+ callback ( message ) ;
149
+ }
150
+ }
98
151
/**
99
152
* Process a received SDS message for this channel.
100
153
*
@@ -110,6 +163,11 @@ export class MessageChannel {
110
163
* @param message - The received SDS message.
111
164
*/
112
165
public receiveMessage ( message : Message ) : void {
166
+ if ( ! message . lamportTimestamp ) {
167
+ // Messages with no timestamp are ephemeral messages and should be delivered immediately
168
+ this . deliverMessage ( message ) ;
169
+ return ;
170
+ }
113
171
// review ack status
114
172
this . reviewAckStatus ( message ) ;
115
173
// add to bloom filter (skip for messages with empty content)
@@ -241,13 +299,19 @@ export class MessageChannel {
241
299
242
300
// See https://rfc.vac.dev/vac/raw/sds/#deliver-message
243
301
private deliverMessage ( message : Message ) : void {
302
+ this . notifyDeliveredMessage ( message . messageId ) ;
303
+
244
304
const messageLamportTimestamp = message . lamportTimestamp ?? 0 ;
245
305
if ( messageLamportTimestamp > this . lamportTimestamp ) {
246
306
this . lamportTimestamp = messageLamportTimestamp ;
247
307
}
248
308
249
- if ( message . content ?. length === 0 ) {
309
+ if (
310
+ message . content ?. length === 0 ||
311
+ message . lamportTimestamp === undefined
312
+ ) {
250
313
// Messages with empty content are sync messages.
314
+ // Messages with no timestamp are ephemeral messages.
251
315
// They are not added to the local log or bloom filter.
252
316
return ;
253
317
}
@@ -312,4 +376,15 @@ export class MessageChannel {
312
376
private getAcknowledgementCount ( ) : number {
313
377
return 2 ;
314
378
}
379
+
380
+ private notifyDeliveredMessage ( messageId : string ) : void {
381
+ if ( this . deliveredMessageCallback ) {
382
+ this . deliveredMessageCallback ( messageId ) ;
383
+ }
384
+ this . dispatchEvent (
385
+ new CustomEvent ( MessageChannelEvent . MessageDelivered , {
386
+ detail : messageId
387
+ } )
388
+ ) ;
389
+ }
315
390
}
0 commit comments