mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-04 06:43:12 +00:00
feat(sds): adds ephemeral messages, delivered message callback and event
This commit is contained in:
parent
e45736ff98
commit
6b4848c853
@ -59,6 +59,7 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@libp2p/interface": "^2.7.0",
|
||||||
"@noble/hashes": "^1.7.1",
|
"@noble/hashes": "^1.7.1",
|
||||||
"@waku/message-hash": "^0.1.18",
|
"@waku/message-hash": "^0.1.18",
|
||||||
"@waku/proto": "^0.0.9",
|
"@waku/proto": "^0.0.9",
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { DefaultBloomFilter } from "./bloom.js";
|
|||||||
import {
|
import {
|
||||||
DEFAULT_BLOOM_FILTER_OPTIONS,
|
DEFAULT_BLOOM_FILTER_OPTIONS,
|
||||||
Message,
|
Message,
|
||||||
MessageChannel
|
MessageChannel,
|
||||||
|
MessageChannelEvent
|
||||||
} from "./sds.js";
|
} from "./sds.js";
|
||||||
|
|
||||||
const channelId = "test-channel";
|
const channelId = "test-channel";
|
||||||
@ -399,12 +400,10 @@ describe("MessageChannel", function () {
|
|||||||
it("should remove messages without delivering if timeout is exceeded", async () => {
|
it("should remove messages without delivering if timeout is exceeded", async () => {
|
||||||
const causalHistorySize = (channelA as any).causalHistorySize;
|
const causalHistorySize = (channelA as any).causalHistorySize;
|
||||||
// Create a channel with very very short timeout
|
// Create a channel with very very short timeout
|
||||||
const channelC: MessageChannel = new MessageChannel(
|
const channelC: MessageChannel = new MessageChannel(channelId, {
|
||||||
channelId,
|
receivedMessageTimeoutEnabled: true,
|
||||||
causalHistorySize,
|
receivedMessageTimeout: 10
|
||||||
true,
|
});
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const m of messagesA) {
|
for (const m of messagesA) {
|
||||||
await channelA.sendMessage(utf8ToBytes(m), callback);
|
await channelA.sendMessage(utf8ToBytes(m), callback);
|
||||||
@ -547,4 +546,56 @@ describe("MessageChannel", function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Ephemeral messages", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
channelA = new MessageChannel(channelId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be sent without a timestamp, causal history, or bloom filter", () => {
|
||||||
|
const timestampBefore = (channelA as any).lamportTimestamp;
|
||||||
|
channelA.sendEphemeralMessage(new Uint8Array(), (message) => {
|
||||||
|
expect(message.lamportTimestamp).to.equal(undefined);
|
||||||
|
expect(message.causalHistory).to.deep.equal([]);
|
||||||
|
expect(message.bloomFilter).to.equal(undefined);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const outgoingBuffer = (channelA as any).outgoingBuffer as Message[];
|
||||||
|
expect(outgoingBuffer.length).to.equal(0);
|
||||||
|
|
||||||
|
const timestampAfter = (channelA as any).lamportTimestamp;
|
||||||
|
expect(timestampAfter).to.equal(timestampBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be delivered immediately if received", async () => {
|
||||||
|
let deliveredMessageId: string | undefined;
|
||||||
|
let sentMessage: Message | undefined;
|
||||||
|
|
||||||
|
const channelB = new MessageChannel(channelId, {
|
||||||
|
deliveredMessageCallback: (messageId) => {
|
||||||
|
deliveredMessageId = messageId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitForMessageDelivered = new Promise<string>((resolve) => {
|
||||||
|
channelB.addEventListener(
|
||||||
|
MessageChannelEvent.MessageDelivered,
|
||||||
|
(event) => {
|
||||||
|
resolve(event.detail);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
channelA.sendEphemeralMessage(utf8ToBytes(messagesA[0]), (message) => {
|
||||||
|
sentMessage = message;
|
||||||
|
channelB.receiveMessage(message);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventMessageId = await waitForMessageDelivered;
|
||||||
|
expect(deliveredMessageId).to.equal(sentMessage?.messageId);
|
||||||
|
expect(eventMessageId).to.equal(sentMessage?.messageId);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
|
import { TypedEventEmitter } from "@libp2p/interface";
|
||||||
import { sha256 } from "@noble/hashes/sha256";
|
import { sha256 } from "@noble/hashes/sha256";
|
||||||
import { bytesToHex } from "@noble/hashes/utils";
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
import { proto_sds_message } from "@waku/proto";
|
import { proto_sds_message } from "@waku/proto";
|
||||||
|
|
||||||
import { DefaultBloomFilter } from "./bloom.js";
|
import { DefaultBloomFilter } from "./bloom.js";
|
||||||
|
|
||||||
|
export enum MessageChannelEvent {
|
||||||
|
MessageDelivered = "messageDelivered"
|
||||||
|
}
|
||||||
|
type MessageChannelEvents = {
|
||||||
|
[MessageChannelEvent.MessageDelivered]: CustomEvent<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Message = proto_sds_message.SdsMessage;
|
export type Message = proto_sds_message.SdsMessage;
|
||||||
export type ChannelId = string;
|
export type ChannelId = string;
|
||||||
|
|
||||||
@ -15,7 +23,14 @@ export const DEFAULT_BLOOM_FILTER_OPTIONS = {
|
|||||||
const DEFAULT_CAUSAL_HISTORY_SIZE = 2;
|
const DEFAULT_CAUSAL_HISTORY_SIZE = 2;
|
||||||
const DEFAULT_RECEIVED_MESSAGE_TIMEOUT = 1000 * 60 * 5; // 5 minutes
|
const DEFAULT_RECEIVED_MESSAGE_TIMEOUT = 1000 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
export class MessageChannel {
|
interface MessageChannelOptions {
|
||||||
|
causalHistorySize?: number;
|
||||||
|
receivedMessageTimeoutEnabled?: boolean;
|
||||||
|
receivedMessageTimeout?: number;
|
||||||
|
deliveredMessageCallback?: (messageId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
|
||||||
private lamportTimestamp: number;
|
private lamportTimestamp: number;
|
||||||
private filter: DefaultBloomFilter;
|
private filter: DefaultBloomFilter;
|
||||||
private outgoingBuffer: Message[];
|
private outgoingBuffer: Message[];
|
||||||
@ -26,13 +41,15 @@ export class MessageChannel {
|
|||||||
private causalHistorySize: number;
|
private causalHistorySize: number;
|
||||||
private acknowledgementCount: number;
|
private acknowledgementCount: number;
|
||||||
private timeReceived: Map<string, number>;
|
private timeReceived: Map<string, number>;
|
||||||
|
private receivedMessageTimeoutEnabled: boolean;
|
||||||
|
private receivedMessageTimeout: number;
|
||||||
|
private deliveredMessageCallback?: (messageId: string) => void;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
channelId: ChannelId,
|
channelId: ChannelId,
|
||||||
causalHistorySize: number = DEFAULT_CAUSAL_HISTORY_SIZE,
|
options: MessageChannelOptions = {}
|
||||||
private receivedMessageTimeoutEnabled: boolean = false,
|
|
||||||
private receivedMessageTimeout: number = DEFAULT_RECEIVED_MESSAGE_TIMEOUT
|
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this.channelId = channelId;
|
this.channelId = channelId;
|
||||||
this.lamportTimestamp = 0;
|
this.lamportTimestamp = 0;
|
||||||
this.filter = new DefaultBloomFilter(DEFAULT_BLOOM_FILTER_OPTIONS);
|
this.filter = new DefaultBloomFilter(DEFAULT_BLOOM_FILTER_OPTIONS);
|
||||||
@ -40,9 +57,15 @@ export class MessageChannel {
|
|||||||
this.acknowledgements = new Map();
|
this.acknowledgements = new Map();
|
||||||
this.incomingBuffer = [];
|
this.incomingBuffer = [];
|
||||||
this.messageIdLog = [];
|
this.messageIdLog = [];
|
||||||
this.causalHistorySize = causalHistorySize;
|
this.causalHistorySize =
|
||||||
|
options.causalHistorySize ?? DEFAULT_CAUSAL_HISTORY_SIZE;
|
||||||
this.acknowledgementCount = this.getAcknowledgementCount();
|
this.acknowledgementCount = this.getAcknowledgementCount();
|
||||||
this.timeReceived = new Map();
|
this.timeReceived = new Map();
|
||||||
|
this.receivedMessageTimeoutEnabled =
|
||||||
|
options.receivedMessageTimeoutEnabled ?? false;
|
||||||
|
this.receivedMessageTimeout =
|
||||||
|
options.receivedMessageTimeout ?? DEFAULT_RECEIVED_MESSAGE_TIMEOUT;
|
||||||
|
this.deliveredMessageCallback = options.deliveredMessageCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getMessageId(payload: Uint8Array): string {
|
public static getMessageId(payload: Uint8Array): string {
|
||||||
@ -95,6 +118,36 @@ export class MessageChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a short-lived message without synchronization or reliability requirements.
|
||||||
|
*
|
||||||
|
* Sends a message without a timestamp, causal history, or bloom filter.
|
||||||
|
* Ephemeral messages are not added to the outgoing buffer.
|
||||||
|
* Upon reception, ephemeral messages are delivered immediately without
|
||||||
|
* checking for causal dependencies or including in the local log.
|
||||||
|
*
|
||||||
|
* See https://rfc.vac.dev/vac/raw/sds/#ephemeral-messages
|
||||||
|
*
|
||||||
|
* @param payload - The payload to send.
|
||||||
|
* @param callback - A callback function that returns a boolean indicating whether the message was sent successfully.
|
||||||
|
*/
|
||||||
|
public sendEphemeralMessage(
|
||||||
|
payload: Uint8Array,
|
||||||
|
callback?: (message: Message) => boolean
|
||||||
|
): void {
|
||||||
|
const message: Message = {
|
||||||
|
messageId: MessageChannel.getMessageId(payload),
|
||||||
|
channelId: this.channelId,
|
||||||
|
content: payload,
|
||||||
|
lamportTimestamp: undefined,
|
||||||
|
causalHistory: [],
|
||||||
|
bloomFilter: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Process a received SDS message for this channel.
|
* Process a received SDS message for this channel.
|
||||||
*
|
*
|
||||||
@ -110,6 +163,11 @@ export class MessageChannel {
|
|||||||
* @param message - The received SDS message.
|
* @param message - The received SDS message.
|
||||||
*/
|
*/
|
||||||
public receiveMessage(message: Message): void {
|
public receiveMessage(message: Message): void {
|
||||||
|
if (!message.lamportTimestamp) {
|
||||||
|
// Messages with no timestamp are ephemeral messages and should be delivered immediately
|
||||||
|
this.deliverMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// review ack status
|
// review ack status
|
||||||
this.reviewAckStatus(message);
|
this.reviewAckStatus(message);
|
||||||
// add to bloom filter (skip for messages with empty content)
|
// add to bloom filter (skip for messages with empty content)
|
||||||
@ -241,13 +299,19 @@ export class MessageChannel {
|
|||||||
|
|
||||||
// See https://rfc.vac.dev/vac/raw/sds/#deliver-message
|
// See https://rfc.vac.dev/vac/raw/sds/#deliver-message
|
||||||
private deliverMessage(message: Message): void {
|
private deliverMessage(message: Message): void {
|
||||||
|
this.notifyDeliveredMessage(message.messageId);
|
||||||
|
|
||||||
const messageLamportTimestamp = message.lamportTimestamp ?? 0;
|
const messageLamportTimestamp = message.lamportTimestamp ?? 0;
|
||||||
if (messageLamportTimestamp > this.lamportTimestamp) {
|
if (messageLamportTimestamp > this.lamportTimestamp) {
|
||||||
this.lamportTimestamp = messageLamportTimestamp;
|
this.lamportTimestamp = messageLamportTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content?.length === 0) {
|
if (
|
||||||
|
message.content?.length === 0 ||
|
||||||
|
message.lamportTimestamp === undefined
|
||||||
|
) {
|
||||||
// Messages with empty content are sync messages.
|
// Messages with empty content are sync messages.
|
||||||
|
// Messages with no timestamp are ephemeral messages.
|
||||||
// They are not added to the local log or bloom filter.
|
// They are not added to the local log or bloom filter.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -312,4 +376,15 @@ export class MessageChannel {
|
|||||||
private getAcknowledgementCount(): number {
|
private getAcknowledgementCount(): number {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyDeliveredMessage(messageId: string): void {
|
||||||
|
if (this.deliveredMessageCallback) {
|
||||||
|
this.deliveredMessageCallback(messageId);
|
||||||
|
}
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(MessageChannelEvent.MessageDelivered, {
|
||||||
|
detail: messageId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user