2025-02-11 13:24:43 -08:00
|
|
|
import { utf8ToBytes } from "@waku/utils/bytes";
|
|
|
|
|
import { expect } from "chai";
|
|
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
import { DefaultBloomFilter } from "../bloom_filter/bloom.js";
|
|
|
|
|
|
2025-08-14 10:44:18 +10:00
|
|
|
import { MessageChannelEvent } from "./events.js";
|
2025-11-26 19:11:01 -05:00
|
|
|
import { MemLocalHistory } from "./mem_local_history.js";
|
2025-08-12 10:47:52 +10:00
|
|
|
import {
|
2025-08-14 10:44:18 +10:00
|
|
|
ContentMessage,
|
2025-08-12 10:47:52 +10:00
|
|
|
HistoryEntry,
|
|
|
|
|
Message,
|
2025-08-14 10:44:18 +10:00
|
|
|
MessageId,
|
|
|
|
|
SyncMessage
|
|
|
|
|
} from "./message.js";
|
2025-02-11 13:24:43 -08:00
|
|
|
import {
|
|
|
|
|
DEFAULT_BLOOM_FILTER_OPTIONS,
|
2025-08-14 10:44:18 +10:00
|
|
|
ILocalHistory,
|
2025-06-03 14:46:12 -07:00
|
|
|
MessageChannel
|
|
|
|
|
} from "./message_channel.js";
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
const channelId = "test-channel";
|
2025-03-10 19:55:43 -07:00
|
|
|
const callback = (_message: Message): Promise<{ success: boolean }> => {
|
|
|
|
|
return Promise.resolve({ success: true });
|
2025-02-11 13:24:43 -08:00
|
|
|
};
|
|
|
|
|
|
2025-11-26 19:11:01 -05:00
|
|
|
/**
|
|
|
|
|
* Test helper to create a MessageChannel with MemLocalHistory.
|
|
|
|
|
* This avoids localStorage pollution in tests and tests core functionality.
|
|
|
|
|
*/
|
|
|
|
|
const createTestChannel = (
|
|
|
|
|
channelId: string,
|
|
|
|
|
senderId: string,
|
|
|
|
|
options: {
|
|
|
|
|
causalHistorySize?: number;
|
|
|
|
|
possibleAcksThreshold?: number;
|
|
|
|
|
timeoutForLostMessagesMs?: number;
|
|
|
|
|
enableRepair?: boolean;
|
|
|
|
|
} = {}
|
|
|
|
|
): MessageChannel => {
|
|
|
|
|
return new MessageChannel(
|
|
|
|
|
channelId,
|
|
|
|
|
senderId,
|
|
|
|
|
options,
|
|
|
|
|
new MemLocalHistory()
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-11 13:24:43 -08:00
|
|
|
const getBloomFilter = (channel: MessageChannel): DefaultBloomFilter => {
|
2025-08-28 15:57:23 +10:00
|
|
|
return channel["filter"] as DefaultBloomFilter;
|
2025-02-11 13:24:43 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const messagesA = ["message-1", "message-2"];
|
|
|
|
|
const messagesB = [
|
|
|
|
|
"message-3",
|
|
|
|
|
"message-4",
|
|
|
|
|
"message-5",
|
|
|
|
|
"message-6",
|
|
|
|
|
"message-7"
|
|
|
|
|
];
|
|
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
const sendMessage = async (
|
|
|
|
|
channel: MessageChannel,
|
|
|
|
|
payload: Uint8Array,
|
2025-08-14 10:44:18 +10:00
|
|
|
callback: (message: ContentMessage) => Promise<{ success: boolean }>
|
2025-06-03 14:46:12 -07:00
|
|
|
): Promise<void> => {
|
2025-09-09 12:43:48 +10:00
|
|
|
channel.pushOutgoingMessage(payload, callback);
|
2025-06-03 14:46:12 -07:00
|
|
|
await channel.processTasks();
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-14 10:44:18 +10:00
|
|
|
const sendSyncMessage = async (
|
|
|
|
|
channel: MessageChannel,
|
|
|
|
|
callback: (message: SyncMessage) => Promise<boolean>
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
await channel.pushOutgoingSyncMessage(callback);
|
|
|
|
|
await channel.processTasks();
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
const receiveMessage = async (
|
|
|
|
|
channel: MessageChannel,
|
2025-08-28 15:57:23 +10:00
|
|
|
message: Message,
|
|
|
|
|
retrievalHint?: Uint8Array
|
2025-06-03 14:46:12 -07:00
|
|
|
): Promise<void> => {
|
2025-08-28 15:57:23 +10:00
|
|
|
channel.pushIncomingMessage(message, retrievalHint);
|
2025-06-03 14:46:12 -07:00
|
|
|
await channel.processTasks();
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-11 13:24:43 -08:00
|
|
|
describe("MessageChannel", function () {
|
|
|
|
|
this.timeout(5000);
|
|
|
|
|
let channelA: MessageChannel;
|
|
|
|
|
let channelB: MessageChannel;
|
|
|
|
|
|
|
|
|
|
describe("sending a message ", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice");
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should increase lamport timestamp", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampBefore = channelA["lamportTimestamp"];
|
2025-08-14 10:44:18 +10:00
|
|
|
await sendMessage(channelA, utf8ToBytes("message"), callback);
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampAfter = channelA["lamportTimestamp"];
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
expect(timestampAfter).to.equal(timestampBefore + 1n);
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should push the message to the outgoing buffer", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const bufferLengthBefore = channelA["outgoingBuffer"].length;
|
2025-08-14 10:44:18 +10:00
|
|
|
await sendMessage(channelA, utf8ToBytes("message"), callback);
|
2025-08-28 15:57:23 +10:00
|
|
|
const bufferLengthAfter = channelA["outgoingBuffer"].length;
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(bufferLengthAfter).to.equal(bufferLengthBefore + 1);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should insert message into bloom filter", async () => {
|
2025-08-14 10:44:18 +10:00
|
|
|
const payload = utf8ToBytes("message");
|
|
|
|
|
const messageId = MessageChannel.getMessageId(payload);
|
|
|
|
|
await sendMessage(channelA, payload, callback);
|
2025-02-11 13:24:43 -08:00
|
|
|
const bloomFilter = getBloomFilter(channelA);
|
|
|
|
|
expect(bloomFilter.lookup(messageId)).to.equal(true);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should insert message id into causal history", async () => {
|
2025-08-14 10:44:18 +10:00
|
|
|
const payload = utf8ToBytes("message");
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
const expectedTimestamp = channelA["lamportTimestamp"] + 1n;
|
2025-08-14 10:44:18 +10:00
|
|
|
const messageId = MessageChannel.getMessageId(payload);
|
|
|
|
|
await sendMessage(channelA, payload, callback);
|
2025-08-28 15:57:23 +10:00
|
|
|
const messageIdLog = channelA["localHistory"] as ILocalHistory;
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(messageIdLog.length).to.equal(1);
|
|
|
|
|
expect(
|
|
|
|
|
messageIdLog.some(
|
|
|
|
|
(log) =>
|
2025-08-14 10:44:18 +10:00
|
|
|
log.lamportTimestamp === expectedTimestamp &&
|
|
|
|
|
log.messageId === messageId
|
2025-02-11 13:24:43 -08:00
|
|
|
)
|
|
|
|
|
).to.equal(true);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
it("should add sent message to localHistory with retrievalHint", async () => {
|
|
|
|
|
const payload = utf8ToBytes("message with retrieval hint");
|
|
|
|
|
const messageId = MessageChannel.getMessageId(payload);
|
|
|
|
|
const testRetrievalHint = utf8ToBytes("test-retrieval-hint-data");
|
|
|
|
|
|
|
|
|
|
await sendMessage(channelA, payload, async (_message) => {
|
|
|
|
|
// Simulate successful sending with retrievalHint
|
|
|
|
|
return { success: true, retrievalHint: testRetrievalHint };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const localHistory = channelA["localHistory"] as ILocalHistory;
|
|
|
|
|
expect(localHistory.length).to.equal(1);
|
|
|
|
|
|
|
|
|
|
// Find the message in local history
|
|
|
|
|
const historyEntry = localHistory.find(
|
|
|
|
|
(entry) => entry.messageId === messageId
|
|
|
|
|
);
|
|
|
|
|
expect(historyEntry).to.exist;
|
|
|
|
|
expect(historyEntry!.retrievalHint).to.deep.equal(testRetrievalHint);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should attach causal history and bloom filter to each message", async () => {
|
2025-02-11 13:24:43 -08:00
|
|
|
const bloomFilter = new DefaultBloomFilter(DEFAULT_BLOOM_FILTER_OPTIONS);
|
2025-08-28 15:57:23 +10:00
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-02-11 13:24:43 -08:00
|
|
|
const filterBytes = new Array<Uint8Array>();
|
|
|
|
|
const messages = new Array<string>(causalHistorySize + 5)
|
|
|
|
|
.fill("message")
|
|
|
|
|
.map((message, index) => `${message}-${index}`);
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const message of messages) {
|
2025-02-11 13:24:43 -08:00
|
|
|
filterBytes.push(bloomFilter.toBytes());
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(message), callback);
|
2025-02-11 13:24:43 -08:00
|
|
|
bloomFilter.insert(MessageChannel.getMessageId(utf8ToBytes(message)));
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(outgoingBuffer.length).to.equal(messages.length);
|
|
|
|
|
|
|
|
|
|
outgoingBuffer.forEach((message, index) => {
|
|
|
|
|
expect(message.content).to.deep.equal(utf8ToBytes(messages[index]));
|
|
|
|
|
// Correct bloom filter should be attached to each message
|
|
|
|
|
expect(message.bloomFilter).to.deep.equal(filterBytes[index]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Causal history should only contain the last N messages as defined by causalHistorySize
|
2025-09-11 18:06:54 +10:00
|
|
|
const causalHistory =
|
|
|
|
|
outgoingBuffer[outgoingBuffer.length - 1].causalHistory;
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(causalHistory.length).to.equal(causalHistorySize);
|
|
|
|
|
|
|
|
|
|
const expectedCausalHistory = messages
|
|
|
|
|
.slice(-causalHistorySize - 1, -1)
|
2025-03-10 19:55:43 -07:00
|
|
|
.map((message) => ({
|
|
|
|
|
messageId: MessageChannel.getMessageId(utf8ToBytes(message)),
|
2025-10-28 10:27:06 +00:00
|
|
|
retrievalHint: undefined,
|
|
|
|
|
senderId: "alice"
|
2025-03-10 19:55:43 -07:00
|
|
|
}));
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(causalHistory).to.deep.equal(expectedCausalHistory);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("receiving a message", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice");
|
|
|
|
|
channelB = createTestChannel(channelId, "bob");
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should increase lamport timestamp", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampBefore = channelA["lamportTimestamp"];
|
2025-08-12 10:47:52 +10:00
|
|
|
await sendMessage(channelB, utf8ToBytes("message"), async (message) => {
|
2025-06-03 14:46:12 -07:00
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampAfter = channelA["lamportTimestamp"];
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
expect(timestampAfter).to.equal(timestampBefore + 1n);
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-09-20 09:40:51 +02:00
|
|
|
// TODO: test is failing in CI, investigate in https://github.com/waku-org/js-waku/issues/2648
|
|
|
|
|
it.skip("should update lamport timestamp if greater than current timestamp and dependencies are met", async () => {
|
2025-11-26 19:11:01 -05:00
|
|
|
const testChannelA = createTestChannel(channelId, "alice");
|
|
|
|
|
const testChannelB = createTestChannel(channelId, "bob");
|
2025-09-20 09:40:51 +02:00
|
|
|
|
|
|
|
|
const timestampBefore = testChannelA["lamportTimestamp"];
|
2025-09-11 18:06:54 +10:00
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-09-20 09:40:51 +02:00
|
|
|
await sendMessage(testChannelA, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
|
|
|
|
for (const m of messagesB) {
|
2025-09-20 09:40:51 +02:00
|
|
|
await sendMessage(testChannelB, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(testChannelA, message);
|
2025-06-03 14:46:12 -07:00
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-09-20 09:40:51 +02:00
|
|
|
const timestampAfter = testChannelA["lamportTimestamp"];
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
expect(timestampAfter - timestampBefore).to.equal(
|
|
|
|
|
BigInt(messagesB.length)
|
|
|
|
|
);
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-09-20 09:40:51 +02:00
|
|
|
// TODO: test is failing in CI, investigate in https://github.com/waku-org/js-waku/issues/2648
|
|
|
|
|
it.skip("should maintain proper timestamps if all messages received", async () => {
|
2025-09-11 18:06:54 +10:00
|
|
|
const aTimestampBefore = channelA["lamportTimestamp"];
|
|
|
|
|
let timestamp = channelB["lamportTimestamp"];
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
2025-02-11 13:24:43 -08:00
|
|
|
timestamp++;
|
2025-06-03 14:46:12 -07:00
|
|
|
await receiveMessage(channelB, message);
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelB["lamportTimestamp"]).to.equal(timestamp);
|
2025-06-03 14:46:12 -07:00
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesB) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelB, utf8ToBytes(m), async (message) => {
|
2025-02-11 13:24:43 -08:00
|
|
|
timestamp++;
|
2025-06-03 14:46:12 -07:00
|
|
|
await receiveMessage(channelA, message);
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelA["lamportTimestamp"]).to.equal(timestamp);
|
2025-06-03 14:46:12 -07:00
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
const expectedLength = messagesA.length + messagesB.length;
|
2025-09-11 18:06:54 +10:00
|
|
|
expect(channelA["lamportTimestamp"]).to.equal(
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
aTimestampBefore + BigInt(expectedLength)
|
2025-09-11 18:06:54 +10:00
|
|
|
);
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelA["lamportTimestamp"]).to.equal(
|
|
|
|
|
channelB["lamportTimestamp"]
|
2025-02-11 13:24:43 -08:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should add received messages to bloom filter", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
2025-02-11 13:24:43 -08:00
|
|
|
const bloomFilter = getBloomFilter(channelB);
|
|
|
|
|
expect(bloomFilter.lookup(message.messageId)).to.equal(true);
|
2025-06-03 14:46:12 -07:00
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should add to incoming buffer if dependencies are not met", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
let receivedMessage: Message | null = null;
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampBefore = channelB["lamportTimestamp"];
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
receivedMessage = message;
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const incomingBuffer = channelB["incomingBuffer"];
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(1);
|
|
|
|
|
expect(incomingBuffer[0].messageId).to.equal(receivedMessage!.messageId);
|
|
|
|
|
|
|
|
|
|
// Since the dependency is not met, the lamport timestamp should not increase
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampAfter = channelB["lamportTimestamp"];
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(timestampAfter).to.equal(timestampBefore);
|
|
|
|
|
|
|
|
|
|
// Message should not be in local history
|
2025-08-28 15:57:23 +10:00
|
|
|
const localHistory = channelB["localHistory"];
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(
|
2025-03-10 19:55:43 -07:00
|
|
|
localHistory.some(
|
2025-08-28 15:57:23 +10:00
|
|
|
({ messageId }) => messageId === receivedMessage!.messageId
|
2025-03-10 19:55:43 -07:00
|
|
|
)
|
2025-02-11 13:24:43 -08:00
|
|
|
).to.equal(false);
|
|
|
|
|
});
|
2025-08-28 15:57:23 +10:00
|
|
|
|
|
|
|
|
it("should add received message to localHistory with retrievalHint", async () => {
|
|
|
|
|
const payload = utf8ToBytes("message with retrieval hint");
|
|
|
|
|
const messageId = MessageChannel.getMessageId(payload);
|
|
|
|
|
const testRetrievalHint = utf8ToBytes("test-retrieval-hint-data");
|
|
|
|
|
|
|
|
|
|
await receiveMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
new Message(
|
|
|
|
|
messageId,
|
|
|
|
|
channelA.channelId,
|
|
|
|
|
"not-alice",
|
|
|
|
|
[],
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
1n,
|
2025-08-28 15:57:23 +10:00
|
|
|
undefined,
|
|
|
|
|
payload,
|
2025-10-28 10:27:06 +00:00
|
|
|
undefined,
|
2025-08-28 15:57:23 +10:00
|
|
|
testRetrievalHint
|
|
|
|
|
),
|
|
|
|
|
testRetrievalHint
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const localHistory = channelA["localHistory"] as ILocalHistory;
|
|
|
|
|
expect(localHistory.length).to.equal(1);
|
|
|
|
|
|
|
|
|
|
// Find the message in local history
|
|
|
|
|
const historyEntry = localHistory.find(
|
|
|
|
|
(entry) => entry.messageId === messageId
|
|
|
|
|
);
|
|
|
|
|
expect(historyEntry).to.exist;
|
|
|
|
|
expect(historyEntry!.retrievalHint).to.deep.equal(testRetrievalHint);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should maintain chronological order of messages in localHistory", async () => {
|
|
|
|
|
// Send messages with different timestamps (including own messages)
|
|
|
|
|
const message1Payload = utf8ToBytes("message 1");
|
|
|
|
|
const message2Payload = utf8ToBytes("message 2");
|
|
|
|
|
const message3Payload = utf8ToBytes("message 3");
|
|
|
|
|
|
|
|
|
|
const message1Id = MessageChannel.getMessageId(message1Payload);
|
|
|
|
|
const message2Id = MessageChannel.getMessageId(message2Payload);
|
|
|
|
|
const message3Id = MessageChannel.getMessageId(message3Payload);
|
|
|
|
|
|
2025-09-11 18:06:54 +10:00
|
|
|
const startTimestamp = channelA["lamportTimestamp"];
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
// Send own message first (timestamp will be 1)
|
|
|
|
|
await sendMessage(channelA, message1Payload, callback);
|
|
|
|
|
|
|
|
|
|
// Receive a message from another sender with higher timestamp (3)
|
|
|
|
|
await receiveMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
new ContentMessage(
|
|
|
|
|
message3Id,
|
|
|
|
|
channelA.channelId,
|
|
|
|
|
"bob",
|
|
|
|
|
[],
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
startTimestamp + 3n, // Higher timestamp
|
2025-08-28 15:57:23 +10:00
|
|
|
undefined,
|
|
|
|
|
message3Payload
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Receive a message from another sender with middle timestamp (2)
|
|
|
|
|
await receiveMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
new ContentMessage(
|
|
|
|
|
message2Id,
|
|
|
|
|
channelA.channelId,
|
|
|
|
|
"carol",
|
|
|
|
|
[],
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
startTimestamp + 2n, // Middle timestamp
|
2025-08-28 15:57:23 +10:00
|
|
|
undefined,
|
|
|
|
|
message2Payload
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const localHistory = channelA["localHistory"];
|
|
|
|
|
expect(localHistory.length).to.equal(3);
|
|
|
|
|
|
|
|
|
|
// Verify chronological order: message1 (ts=1), message2 (ts=2), message3 (ts=3)
|
|
|
|
|
|
|
|
|
|
const first = localHistory.findIndex(
|
|
|
|
|
({ messageId, lamportTimestamp }) => {
|
2025-09-11 18:06:54 +10:00
|
|
|
return (
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
messageId === message1Id && lamportTimestamp === startTimestamp + 1n
|
2025-09-11 18:06:54 +10:00
|
|
|
);
|
2025-08-28 15:57:23 +10:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
expect(first).to.eq(0);
|
|
|
|
|
|
|
|
|
|
const second = localHistory.findIndex(
|
|
|
|
|
({ messageId, lamportTimestamp }) => {
|
2025-09-11 18:06:54 +10:00
|
|
|
return (
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
messageId === message2Id && lamportTimestamp === startTimestamp + 2n
|
2025-09-11 18:06:54 +10:00
|
|
|
);
|
2025-08-28 15:57:23 +10:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
expect(second).to.eq(1);
|
|
|
|
|
|
|
|
|
|
const third = localHistory.findIndex(
|
|
|
|
|
({ messageId, lamportTimestamp }) => {
|
2025-09-11 18:06:54 +10:00
|
|
|
return (
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
messageId === message3Id && lamportTimestamp === startTimestamp + 3n
|
2025-09-11 18:06:54 +10:00
|
|
|
);
|
2025-08-28 15:57:23 +10:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
expect(third).to.eq(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle messages with same timestamp ordered by messageId", async () => {
|
|
|
|
|
const message1Payload = utf8ToBytes("message a");
|
|
|
|
|
const message2Payload = utf8ToBytes("message b");
|
|
|
|
|
|
|
|
|
|
const message1Id = MessageChannel.getMessageId(message1Payload);
|
|
|
|
|
const message2Id = MessageChannel.getMessageId(message2Payload);
|
|
|
|
|
|
|
|
|
|
// Receive messages with same timestamp but different message IDs
|
|
|
|
|
// The valueOf() method ensures ordering by messageId when timestamps are equal
|
|
|
|
|
await receiveMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
new ContentMessage(
|
|
|
|
|
message2Id, // This will come second alphabetically by messageId
|
|
|
|
|
channelA.channelId,
|
|
|
|
|
"bob",
|
|
|
|
|
[],
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
5n, // Same timestamp
|
2025-08-28 15:57:23 +10:00
|
|
|
undefined,
|
|
|
|
|
message2Payload
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await receiveMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
new ContentMessage(
|
|
|
|
|
message1Id, // This will come first alphabetically by messageId
|
|
|
|
|
channelA.channelId,
|
|
|
|
|
"carol",
|
|
|
|
|
[],
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
5n, // Same timestamp
|
2025-08-28 15:57:23 +10:00
|
|
|
undefined,
|
|
|
|
|
message1Payload
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const localHistory = channelA["localHistory"] as ILocalHistory;
|
|
|
|
|
expect(localHistory.length).to.equal(2);
|
|
|
|
|
|
|
|
|
|
// When timestamps are equal, should be ordered by messageId lexicographically
|
|
|
|
|
// The valueOf() method creates "000000000000005_messageId" for comparison
|
|
|
|
|
const expectedOrder = [message1Id, message2Id].sort();
|
|
|
|
|
|
|
|
|
|
const first = localHistory.findIndex(
|
|
|
|
|
({ messageId, lamportTimestamp }) => {
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
return messageId === expectedOrder[0] && lamportTimestamp == 5n;
|
2025-08-28 15:57:23 +10:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
expect(first).to.eq(0);
|
|
|
|
|
|
|
|
|
|
const second = localHistory.findIndex(
|
|
|
|
|
({ messageId, lamportTimestamp }) => {
|
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow
The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).
The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.
uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).
Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.
The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.
* change lamportTimestamp to uint64 in protobuf
* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00
|
|
|
return messageId === expectedOrder[1] && lamportTimestamp == 5n;
|
2025-08-28 15:57:23 +10:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
expect(second).to.eq(1);
|
|
|
|
|
});
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("reviewing ack status", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice", {
|
2025-08-12 10:47:52 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
2025-11-26 19:11:01 -05:00
|
|
|
channelB = createTestChannel(channelId, "bob", { causalHistorySize: 2 });
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should mark all messages in causal history as acknowledged", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-06-03 14:46:12 -07:00
|
|
|
await channelA.processTasks();
|
|
|
|
|
await channelB.processTasks();
|
|
|
|
|
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes("not-in-history"),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await channelA.processTasks();
|
|
|
|
|
await channelB.processTasks();
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelA["outgoingBuffer"].length).to.equal(messagesA.length + 1);
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelB,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await channelA.processTasks();
|
|
|
|
|
await channelB.processTasks();
|
2025-02-11 13:24:43 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Channel B only includes the last causalHistorySize messages in its causal history
|
|
|
|
|
// Since B received message-1, message-2, and not-in-history (3 messages),
|
|
|
|
|
// and causalHistorySize is 3, it will only include the last 2 in its causal history
|
|
|
|
|
// So message-1 won't be acknowledged, only message-2 and not-in-history
|
2025-08-28 15:57:23 +10:00
|
|
|
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
|
2025-02-11 13:24:43 -08:00
|
|
|
expect(outgoingBuffer.length).to.equal(1);
|
2025-06-03 14:46:12 -07:00
|
|
|
// The remaining message should be message-1 (not acknowledged)
|
|
|
|
|
expect(outgoingBuffer[0].messageId).to.equal(
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
|
2025-08-08 10:18:01 +10:00
|
|
|
it("should not mark messages in causal history as acknowledged if it's our own message", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
|
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message); // same channel used on purpose
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await channelA.processTasks();
|
|
|
|
|
|
|
|
|
|
// All messages remain in the buffer
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelA["outgoingBuffer"].length).to.equal(messagesA.length);
|
2025-08-08 10:18:01 +10:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should track probabilistic acknowledgements of messages received in bloom filter", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const possibleAcksThreshold = channelA["possibleAcksThreshold"];
|
|
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
const unacknowledgedMessages = [
|
|
|
|
|
"unacknowledged-message-1",
|
|
|
|
|
"unacknowledged-message-2"
|
|
|
|
|
];
|
|
|
|
|
const messages = [...messagesA, ...messagesB.slice(0, -1)];
|
|
|
|
|
// Send messages to be received by channel B
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messages) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
// Send messages not received by channel B
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of unacknowledgedMessages) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
// Channel B sends a message to channel A
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelB,
|
2025-02-11 13:24:43 -08:00
|
|
|
utf8ToBytes(messagesB[messagesB.length - 1]),
|
2025-06-03 14:46:12 -07:00
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const possibleAcks: ReadonlyMap<MessageId, number> =
|
|
|
|
|
channelA["possibleAcks"];
|
2025-02-11 13:24:43 -08:00
|
|
|
// Other than the message IDs which were included in causal history,
|
|
|
|
|
// the remaining messages sent by channel A should be considered possibly acknowledged
|
|
|
|
|
// for having been included in the bloom filter sent from channel B
|
|
|
|
|
const expectedAcknowledgementsSize = messages.length - causalHistorySize;
|
|
|
|
|
if (expectedAcknowledgementsSize <= 0) {
|
|
|
|
|
throw new Error("expectedAcknowledgementsSize must be greater than 0");
|
|
|
|
|
}
|
2025-08-12 10:47:52 +10:00
|
|
|
expect(possibleAcks.size).to.equal(expectedAcknowledgementsSize);
|
2025-02-11 13:24:43 -08:00
|
|
|
// Channel B only included the last N messages in causal history
|
|
|
|
|
messages.slice(0, -causalHistorySize).forEach((m) => {
|
|
|
|
|
expect(
|
2025-08-12 10:47:52 +10:00
|
|
|
possibleAcks.get(MessageChannel.getMessageId(utf8ToBytes(m)))
|
2025-02-11 13:24:43 -08:00
|
|
|
).to.equal(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Messages that never reached channel B should not be acknowledged
|
|
|
|
|
unacknowledgedMessages.forEach((m) => {
|
|
|
|
|
expect(
|
2025-08-12 10:47:52 +10:00
|
|
|
possibleAcks.has(MessageChannel.getMessageId(utf8ToBytes(m)))
|
2025-02-11 13:24:43 -08:00
|
|
|
).to.equal(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// When channel C sends more messages, it will include all the same messages
|
|
|
|
|
// in the bloom filter as before, which should mark them as fully acknowledged in channel A
|
2025-08-12 10:47:52 +10:00
|
|
|
for (let i = 1; i < possibleAcksThreshold; i++) {
|
2025-02-11 13:24:43 -08:00
|
|
|
// Send messages until acknowledgement count is reached
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelB, utf8ToBytes(`x-${i}`), async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 10:47:52 +10:00
|
|
|
// No more possible acknowledgements should be in channel A
|
|
|
|
|
expect(possibleAcks.size).to.equal(0);
|
2025-02-11 13:24:43 -08:00
|
|
|
|
|
|
|
|
// Messages that were not acknowledged should still be in the outgoing buffer
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelA["outgoingBuffer"].length).to.equal(
|
2025-02-11 13:24:43 -08:00
|
|
|
unacknowledgedMessages.length
|
|
|
|
|
);
|
|
|
|
|
unacknowledgedMessages.forEach((m) => {
|
|
|
|
|
expect(
|
2025-08-28 15:57:23 +10:00
|
|
|
(channelA["outgoingBuffer"] as Message[]).some(
|
2025-02-11 13:24:43 -08:00
|
|
|
(message) =>
|
|
|
|
|
message.messageId === MessageChannel.getMessageId(utf8ToBytes(m))
|
|
|
|
|
)
|
|
|
|
|
).to.equal(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-08-08 10:18:01 +10:00
|
|
|
|
|
|
|
|
it("should not track probabilistic acknowledgements of messages received in bloom filter of own messages", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
|
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const possibleAcks: ReadonlyMap<MessageId, number> =
|
|
|
|
|
channelA["possibleAcks"];
|
2025-08-12 10:47:52 +10:00
|
|
|
expect(possibleAcks.size).to.equal(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("First message is missed, then re-sent, should be ack'd", async () => {
|
|
|
|
|
const firstMessage = utf8ToBytes("first message");
|
|
|
|
|
const firstMessageId = MessageChannel.getMessageId(firstMessage);
|
|
|
|
|
let messageAcked = false;
|
|
|
|
|
channelA.addEventListener(
|
|
|
|
|
MessageChannelEvent.OutMessageAcknowledged,
|
|
|
|
|
(event) => {
|
|
|
|
|
if (firstMessageId === event.detail) {
|
|
|
|
|
messageAcked = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await sendMessage(channelA, firstMessage, callback);
|
|
|
|
|
|
|
|
|
|
const secondMessage = utf8ToBytes("second message");
|
|
|
|
|
await sendMessage(channelA, secondMessage, async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const thirdMessage = utf8ToBytes("third message");
|
|
|
|
|
await sendMessage(channelB, thirdMessage, async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(messageAcked).to.be.false;
|
|
|
|
|
|
|
|
|
|
// Now, A resends first message, and B is receiving it.
|
|
|
|
|
await sendMessage(channelA, firstMessage, async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// And be sends a sync message
|
2025-10-02 15:17:10 +10:00
|
|
|
const res = await channelB.pushOutgoingSyncMessage(async (message) => {
|
2025-08-12 10:47:52 +10:00
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
2025-08-08 10:18:01 +10:00
|
|
|
|
2025-10-02 15:17:10 +10:00
|
|
|
expect(res).to.be.true;
|
2025-08-12 10:47:52 +10:00
|
|
|
expect(messageAcked).to.be.true;
|
2025-08-08 10:18:01 +10:00
|
|
|
});
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|
2025-02-21 11:26:22 -08:00
|
|
|
|
|
|
|
|
describe("Sweeping incoming buffer", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice", {
|
2025-08-12 10:47:52 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
2025-11-26 19:11:01 -05:00
|
|
|
channelB = createTestChannel(channelId, "bob", { causalHistorySize: 2 });
|
2025-02-21 11:26:22 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should detect messages with missing dependencies", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const incomingBuffer = channelB["incomingBuffer"];
|
2025-02-21 11:26:22 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(1);
|
|
|
|
|
expect(incomingBuffer[0].messageId).to.equal(
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes(messagesB[0]))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const missingMessages = channelB.sweepIncomingBuffer();
|
|
|
|
|
expect(missingMessages.length).to.equal(causalHistorySize);
|
2025-03-10 19:55:43 -07:00
|
|
|
expect(missingMessages[0].messageId).to.equal(
|
2025-02-21 11:26:22 -08:00
|
|
|
MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should deliver messages after dependencies are met", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-02-21 11:26:22 -08:00
|
|
|
const sentMessages = new Array<Message>();
|
2025-06-03 14:46:12 -07:00
|
|
|
// First, send messages from A but DON'T deliver them to B yet
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
2025-02-21 11:26:22 -08:00
|
|
|
sentMessages.push(message);
|
2025-06-03 14:46:12 -07:00
|
|
|
// Don't receive them at B yet - we want them to be missing dependencies
|
|
|
|
|
return { success: true };
|
2025-02-21 11:26:22 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-06-03 14:46:12 -07:00
|
|
|
await channelA.processTasks();
|
|
|
|
|
|
|
|
|
|
// Now send a message from A to B that depends on messagesA
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await channelA.processTasks();
|
|
|
|
|
await channelB.processTasks();
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Message should be in incoming buffer waiting for dependencies
|
2025-02-21 11:26:22 -08:00
|
|
|
const missingMessages = channelB.sweepIncomingBuffer();
|
|
|
|
|
expect(missingMessages.length).to.equal(causalHistorySize);
|
2025-03-10 19:55:43 -07:00
|
|
|
expect(missingMessages[0].messageId).to.equal(
|
2025-02-21 11:26:22 -08:00
|
|
|
MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
let incomingBuffer = channelB["incomingBuffer"];
|
2025-02-21 11:26:22 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(1);
|
|
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Now deliver the missing dependencies
|
|
|
|
|
for (const m of sentMessages) {
|
|
|
|
|
await receiveMessage(channelB, m);
|
|
|
|
|
}
|
|
|
|
|
await channelB.processTasks();
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Sweep should now deliver the waiting message
|
2025-02-21 11:26:22 -08:00
|
|
|
const missingMessages2 = channelB.sweepIncomingBuffer();
|
|
|
|
|
expect(missingMessages2.length).to.equal(0);
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
incomingBuffer = channelB["incomingBuffer"];
|
2025-02-21 11:26:22 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(0);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-12 10:47:52 +10:00
|
|
|
it("should mark a message as irretrievably lost if timeout is exceeded", async () => {
|
|
|
|
|
// Create a channel with very very short timeout
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelC = createTestChannel(channelId, "carol", {
|
2025-08-14 10:44:18 +10:00
|
|
|
timeoutForLostMessagesMs: 10
|
2025-08-12 10:47:52 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const m of messagesA) {
|
|
|
|
|
await sendMessage(channelA, utf8ToBytes(m), callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let irretrievablyLost = false;
|
|
|
|
|
const messageToBeLostId = MessageChannel.getMessageId(
|
|
|
|
|
utf8ToBytes(messagesA[0])
|
|
|
|
|
);
|
2025-08-14 10:44:18 +10:00
|
|
|
channelC.addEventListener(MessageChannelEvent.InMessageLost, (event) => {
|
|
|
|
|
for (const hist of event.detail) {
|
|
|
|
|
if (hist.messageId === messageToBeLostId) {
|
|
|
|
|
irretrievablyLost = true;
|
2025-08-12 10:47:52 +10:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-14 10:44:18 +10:00
|
|
|
});
|
2025-08-12 10:47:52 +10:00
|
|
|
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelC, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
channelC.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
|
|
|
|
|
|
|
|
channelC.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
expect(irretrievablyLost).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
it("should emit InMessageLost event with retrievalHint when timeout is exceeded", async () => {
|
|
|
|
|
const testRetrievalHint = utf8ToBytes("lost-message-hint");
|
|
|
|
|
let lostMessages: HistoryEntry[] = [];
|
|
|
|
|
|
|
|
|
|
// Create a channel with very short timeout
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelC = createTestChannel(channelId, "carol", {
|
2025-08-28 15:57:23 +10:00
|
|
|
timeoutForLostMessagesMs: 10
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
channelC.addEventListener(MessageChannelEvent.InMessageLost, (event) => {
|
|
|
|
|
lostMessages = event.detail;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send message from A with retrievalHint
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
message.retrievalHint = testRetrievalHint;
|
|
|
|
|
return { success: true, retrievalHint: testRetrievalHint };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send another message from A
|
|
|
|
|
await sendMessage(channelA, utf8ToBytes(messagesA[1]), callback);
|
|
|
|
|
|
|
|
|
|
// Send a message to C that depends on the previous messages
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelC, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// First sweep - should detect missing messages
|
|
|
|
|
channelC.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
// Wait for timeout
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
|
|
|
|
|
|
|
|
// Second sweep - should mark messages as lost
|
|
|
|
|
channelC.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
expect(lostMessages.length).to.equal(2);
|
|
|
|
|
|
|
|
|
|
// Verify retrievalHint is included in the lost message
|
|
|
|
|
const lostMessageWithHint = lostMessages.find(
|
|
|
|
|
(m) =>
|
|
|
|
|
m.messageId === MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
expect(lostMessageWithHint).to.exist;
|
|
|
|
|
expect(lostMessageWithHint!.retrievalHint).to.deep.equal(
|
|
|
|
|
testRetrievalHint
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Verify message without retrievalHint has undefined
|
|
|
|
|
const lostMessageWithoutHint = lostMessages.find(
|
|
|
|
|
(m) =>
|
|
|
|
|
m.messageId === MessageChannel.getMessageId(utf8ToBytes(messagesA[1]))
|
|
|
|
|
);
|
|
|
|
|
expect(lostMessageWithoutHint).to.exist;
|
|
|
|
|
expect(lostMessageWithoutHint!.retrievalHint).to.be.undefined;
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-21 11:26:22 -08:00
|
|
|
it("should remove messages without delivering if timeout is exceeded", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-02-21 11:26:22 -08:00
|
|
|
// Create a channel with very very short timeout
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelC = createTestChannel(channelId, "carol", {
|
2025-08-14 10:44:18 +10:00
|
|
|
timeoutForLostMessagesMs: 10
|
2025-03-07 18:00:33 -08:00
|
|
|
});
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelC, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-02-21 11:26:22 -08:00
|
|
|
|
|
|
|
|
const missingMessages = channelC.sweepIncomingBuffer();
|
|
|
|
|
expect(missingMessages.length).to.equal(causalHistorySize);
|
2025-08-28 15:57:23 +10:00
|
|
|
let incomingBuffer = channelC["incomingBuffer"];
|
2025-02-21 11:26:22 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(1);
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
|
|
|
|
|
|
|
|
channelC.sweepIncomingBuffer();
|
2025-08-28 15:57:23 +10:00
|
|
|
incomingBuffer = channelC["incomingBuffer"];
|
2025-02-21 11:26:22 -08:00
|
|
|
expect(incomingBuffer.length).to.equal(0);
|
|
|
|
|
});
|
2025-08-28 15:57:23 +10:00
|
|
|
|
|
|
|
|
it("should return HistoryEntry with retrievalHint from sweepIncomingBuffer", async () => {
|
|
|
|
|
const testRetrievalHint = utf8ToBytes("test-retrieval-hint");
|
|
|
|
|
|
|
|
|
|
// Send message from A with a retrievalHint
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
message.retrievalHint = testRetrievalHint;
|
|
|
|
|
return { success: true, retrievalHint: testRetrievalHint };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send another message from A that depends on the first one
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[1]),
|
|
|
|
|
async (_message) => {
|
|
|
|
|
// Don't send to B yet - we want B to have missing dependencies
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send a message from A to B that depends on previous messages
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Sweep should detect missing dependencies and return them with retrievalHint
|
|
|
|
|
const missingMessages = channelB.sweepIncomingBuffer();
|
|
|
|
|
expect(missingMessages.length).to.equal(2);
|
|
|
|
|
|
|
|
|
|
// Find the first message in missing dependencies
|
|
|
|
|
const firstMissingMessage = missingMessages.find(
|
|
|
|
|
(m) =>
|
|
|
|
|
m.messageId === MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
expect(firstMissingMessage).to.exist;
|
|
|
|
|
expect(firstMissingMessage!.retrievalHint).to.deep.equal(
|
|
|
|
|
testRetrievalHint
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should emit InMessageMissing event with retrievalHint", async () => {
|
|
|
|
|
const testRetrievalHint1 = utf8ToBytes("hint-for-message-1");
|
|
|
|
|
const testRetrievalHint2 = utf8ToBytes("hint-for-message-2");
|
|
|
|
|
let eventReceived = false;
|
|
|
|
|
let emittedMissingMessages: HistoryEntry[] = [];
|
|
|
|
|
|
|
|
|
|
// Listen for InMessageMissing event
|
|
|
|
|
channelB.addEventListener(
|
|
|
|
|
MessageChannelEvent.InMessageMissing,
|
|
|
|
|
(event) => {
|
|
|
|
|
eventReceived = true;
|
|
|
|
|
emittedMissingMessages = event.detail;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send messages from A with retrievalHints
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
message.retrievalHint = testRetrievalHint1;
|
|
|
|
|
return { success: true, retrievalHint: testRetrievalHint1 };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[1]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
message.retrievalHint = testRetrievalHint2;
|
|
|
|
|
return { success: true, retrievalHint: testRetrievalHint2 };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send a message to B that depends on the previous messages
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Sweep should trigger InMessageMissing event
|
|
|
|
|
channelB.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
expect(eventReceived).to.be.true;
|
|
|
|
|
expect(emittedMissingMessages.length).to.equal(2);
|
|
|
|
|
|
|
|
|
|
// Verify retrievalHints are included in the event
|
|
|
|
|
const firstMissing = emittedMissingMessages.find(
|
|
|
|
|
(m) =>
|
|
|
|
|
m.messageId === MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
const secondMissing = emittedMissingMessages.find(
|
|
|
|
|
(m) =>
|
|
|
|
|
m.messageId === MessageChannel.getMessageId(utf8ToBytes(messagesA[1]))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(firstMissing).to.exist;
|
|
|
|
|
expect(firstMissing!.retrievalHint).to.deep.equal(testRetrievalHint1);
|
|
|
|
|
expect(secondMissing).to.exist;
|
|
|
|
|
expect(secondMissing!.retrievalHint).to.deep.equal(testRetrievalHint2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle missing messages with undefined retrievalHint", async () => {
|
|
|
|
|
let emittedMissingMessages: HistoryEntry[] = [];
|
|
|
|
|
|
|
|
|
|
channelB.addEventListener(
|
|
|
|
|
MessageChannelEvent.InMessageMissing,
|
|
|
|
|
(event) => {
|
|
|
|
|
emittedMissingMessages = event.detail;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send message from A without retrievalHint
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesA[0]),
|
|
|
|
|
async (_message) => {
|
|
|
|
|
// Don't set retrievalHint
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Send a message to B that depends on the previous message
|
|
|
|
|
await sendMessage(
|
|
|
|
|
channelA,
|
|
|
|
|
utf8ToBytes(messagesB[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Sweep should handle missing message with undefined retrievalHint
|
|
|
|
|
const missingMessages = channelB.sweepIncomingBuffer();
|
|
|
|
|
|
|
|
|
|
expect(missingMessages.length).to.equal(1);
|
|
|
|
|
expect(missingMessages[0].messageId).to.equal(
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
|
|
|
|
|
);
|
|
|
|
|
expect(missingMessages[0].retrievalHint).to.be.undefined;
|
|
|
|
|
|
|
|
|
|
// Event should also reflect undefined retrievalHint
|
|
|
|
|
expect(emittedMissingMessages.length).to.equal(1);
|
|
|
|
|
expect(emittedMissingMessages[0].retrievalHint).to.be.undefined;
|
|
|
|
|
});
|
2025-02-21 11:26:22 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Sweeping outgoing buffer", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice", {
|
2025-08-12 10:47:52 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
2025-11-26 19:11:01 -05:00
|
|
|
channelB = createTestChannel(channelId, "bob", { causalHistorySize: 2 });
|
2025-02-21 11:26:22 -08:00
|
|
|
});
|
|
|
|
|
|
2025-03-03 22:02:53 -08:00
|
|
|
it("should partition messages based on acknowledgement status", async () => {
|
|
|
|
|
for (const m of messagesA) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
|
|
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
2025-02-21 11:26:22 -08:00
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-21 11:26:22 -08:00
|
|
|
|
|
|
|
|
let { unacknowledged, possiblyAcknowledged } =
|
|
|
|
|
channelA.sweepOutgoingBuffer();
|
|
|
|
|
expect(unacknowledged.length).to.equal(messagesA.length);
|
|
|
|
|
expect(possiblyAcknowledged.length).to.equal(0);
|
|
|
|
|
|
|
|
|
|
// Make sure messages sent by channel A are not in causal history
|
2025-08-28 15:57:23 +10:00
|
|
|
const causalHistorySize = channelA["causalHistorySize"];
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesB.slice(0, causalHistorySize)) {
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(channelB, utf8ToBytes(m), callback);
|
2025-03-03 22:02:53 -08:00
|
|
|
}
|
2025-02-21 11:26:22 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
await sendMessage(
|
|
|
|
|
channelB,
|
2025-02-21 11:26:22 -08:00
|
|
|
utf8ToBytes(messagesB[causalHistorySize]),
|
2025-06-03 14:46:12 -07:00
|
|
|
async (message) => {
|
|
|
|
|
await receiveMessage(channelA, message);
|
|
|
|
|
return { success: true };
|
2025-02-21 11:26:22 -08:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// All messages that were previously unacknowledged should now be possibly acknowledged
|
|
|
|
|
// since they were included in one of the bloom filters sent from channel B
|
|
|
|
|
({ unacknowledged, possiblyAcknowledged } =
|
|
|
|
|
channelA.sweepOutgoingBuffer());
|
|
|
|
|
expect(unacknowledged.length).to.equal(0);
|
|
|
|
|
expect(possiblyAcknowledged.length).to.equal(messagesA.length);
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
|
|
|
|
|
describe("Sync messages", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice", {
|
2025-08-12 10:47:52 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
2025-11-26 19:11:01 -05:00
|
|
|
channelB = createTestChannel(channelId, "bob", { causalHistorySize: 2 });
|
2025-10-02 15:17:10 +10:00
|
|
|
const message = utf8ToBytes("first message in channel");
|
|
|
|
|
channelA["localHistory"].push(
|
|
|
|
|
new ContentMessage(
|
|
|
|
|
MessageChannel.getMessageId(message),
|
|
|
|
|
"MyChannel",
|
|
|
|
|
"alice",
|
|
|
|
|
[],
|
|
|
|
|
1n,
|
|
|
|
|
undefined,
|
|
|
|
|
message
|
|
|
|
|
)
|
|
|
|
|
);
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should be sent with empty content", async () => {
|
2025-10-02 15:17:10 +10:00
|
|
|
const res = await channelA.pushOutgoingSyncMessage(async (message) => {
|
2025-08-14 10:44:18 +10:00
|
|
|
expect(message.content).to.be.undefined;
|
2025-06-03 14:46:12 -07:00
|
|
|
return true;
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
2025-10-02 15:17:10 +10:00
|
|
|
expect(res).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not be sent when there is no history", async () => {
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelC = createTestChannel(channelId, "carol", {
|
2025-10-02 15:17:10 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
|
|
|
|
const res = await channelC.pushOutgoingSyncMessage(async (_msg) => {
|
|
|
|
|
throw "callback was called when it's not expected";
|
|
|
|
|
});
|
|
|
|
|
expect(res).to.be.false;
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not be added to outgoing buffer, bloom filter, or local log", async () => {
|
2025-10-02 15:17:10 +10:00
|
|
|
const res = await channelA.pushOutgoingSyncMessage();
|
|
|
|
|
expect(res).to.be.true;
|
2025-03-03 22:02:53 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
|
2025-03-03 22:02:53 -08:00
|
|
|
expect(outgoingBuffer.length).to.equal(0);
|
|
|
|
|
|
|
|
|
|
const bloomFilter = getBloomFilter(channelA);
|
|
|
|
|
expect(
|
|
|
|
|
bloomFilter.lookup(MessageChannel.getMessageId(new Uint8Array()))
|
|
|
|
|
).to.equal(false);
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const localLog = channelA["localHistory"];
|
2025-10-02 15:17:10 +10:00
|
|
|
expect(localLog.length).to.equal(1); // beforeEach adds one message
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
|
|
|
|
|
2025-08-12 10:47:52 +10:00
|
|
|
it("should not be delivered", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampBefore = channelB["lamportTimestamp"];
|
2025-10-02 15:17:10 +10:00
|
|
|
const res = await channelA.pushOutgoingSyncMessage(async (message) => {
|
2025-06-03 14:46:12 -07:00
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return true;
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
2025-10-02 15:17:10 +10:00
|
|
|
expect(res).to.be.true;
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampAfter = channelB["lamportTimestamp"];
|
2025-08-12 10:47:52 +10:00
|
|
|
expect(timestampAfter).to.equal(timestampBefore);
|
2025-03-03 22:02:53 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const localLog = channelB["localHistory"];
|
2025-03-03 22:02:53 -08:00
|
|
|
expect(localLog.length).to.equal(0);
|
|
|
|
|
|
|
|
|
|
const bloomFilter = getBloomFilter(channelB);
|
|
|
|
|
expect(
|
|
|
|
|
bloomFilter.lookup(MessageChannel.getMessageId(new Uint8Array()))
|
|
|
|
|
).to.equal(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should update ack status of messages in outgoing buffer", async () => {
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelC = createTestChannel(channelId, "carol", {
|
2025-10-02 15:17:10 +10:00
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
2025-03-03 22:02:53 -08:00
|
|
|
for (const m of messagesA) {
|
2025-10-02 15:17:10 +10:00
|
|
|
await sendMessage(channelC, utf8ToBytes(m), async (message) => {
|
2025-06-03 14:46:12 -07:00
|
|
|
await receiveMessage(channelB, message);
|
|
|
|
|
return { success: true };
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 10:44:18 +10:00
|
|
|
await sendSyncMessage(channelB, async (message) => {
|
2025-10-02 15:17:10 +10:00
|
|
|
await receiveMessage(channelC, message);
|
2025-08-14 10:44:18 +10:00
|
|
|
return true;
|
2025-03-03 22:02:53 -08:00
|
|
|
});
|
|
|
|
|
|
2025-10-02 15:17:10 +10:00
|
|
|
const causalHistorySize = channelC["causalHistorySize"];
|
|
|
|
|
const outgoingBuffer = channelC["outgoingBuffer"] as Message[];
|
2025-03-03 22:02:53 -08:00
|
|
|
expect(outgoingBuffer.length).to.equal(
|
|
|
|
|
messagesA.length - causalHistorySize
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-03-07 18:00:33 -08:00
|
|
|
|
|
|
|
|
describe("Ephemeral messages", () => {
|
|
|
|
|
beforeEach(() => {
|
2025-11-26 19:11:01 -05:00
|
|
|
channelA = createTestChannel(channelId, "alice");
|
2025-03-07 18:00:33 -08:00
|
|
|
});
|
|
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
it("should be sent without a timestamp, causal history, or bloom filter", async () => {
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampBefore = channelA["lamportTimestamp"];
|
2025-08-12 10:47:52 +10:00
|
|
|
await channelA.pushOutgoingEphemeralMessage(
|
|
|
|
|
new Uint8Array(),
|
|
|
|
|
async (message) => {
|
|
|
|
|
expect(message.lamportTimestamp).to.equal(undefined);
|
|
|
|
|
expect(message.causalHistory).to.deep.equal([]);
|
|
|
|
|
expect(message.bloomFilter).to.equal(undefined);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-03-07 18:00:33 -08:00
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
|
2025-03-07 18:00:33 -08:00
|
|
|
expect(outgoingBuffer.length).to.equal(0);
|
|
|
|
|
|
2025-08-28 15:57:23 +10:00
|
|
|
const timestampAfter = channelA["lamportTimestamp"];
|
2025-03-07 18:00:33 -08:00
|
|
|
expect(timestampAfter).to.equal(timestampBefore);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should be delivered immediately if received", async () => {
|
2025-11-26 19:11:01 -05:00
|
|
|
const channelB = createTestChannel(channelId, "bob");
|
2025-03-07 18:00:33 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Track initial state
|
2025-08-28 15:57:23 +10:00
|
|
|
const localHistoryBefore = channelB["localHistory"].length;
|
|
|
|
|
const incomingBufferBefore = channelB["incomingBuffer"].length;
|
|
|
|
|
const timestampBefore = channelB["lamportTimestamp"];
|
2025-03-07 18:00:33 -08:00
|
|
|
|
2025-08-12 10:47:52 +10:00
|
|
|
await channelA.pushOutgoingEphemeralMessage(
|
2025-06-03 14:46:12 -07:00
|
|
|
utf8ToBytes(messagesA[0]),
|
|
|
|
|
async (message) => {
|
|
|
|
|
// Ephemeral messages should have no timestamp
|
|
|
|
|
expect(message.lamportTimestamp).to.be.undefined;
|
|
|
|
|
await receiveMessage(channelB, message);
|
2025-03-07 18:00:33 -08:00
|
|
|
return true;
|
2025-06-03 14:46:12 -07:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
await channelA.processTasks();
|
|
|
|
|
await channelB.processTasks();
|
2025-03-07 18:00:33 -08:00
|
|
|
|
2025-06-03 14:46:12 -07:00
|
|
|
// Verify ephemeral message behavior:
|
|
|
|
|
// 1. Not added to local history
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelB["localHistory"].length).to.equal(localHistoryBefore);
|
2025-06-03 14:46:12 -07:00
|
|
|
// 2. Not added to incoming buffer
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelB["incomingBuffer"].length).to.equal(incomingBufferBefore);
|
2025-06-03 14:46:12 -07:00
|
|
|
// 3. Doesn't update lamport timestamp
|
2025-08-28 15:57:23 +10:00
|
|
|
expect(channelB["lamportTimestamp"]).to.equal(timestampBefore);
|
2025-03-07 18:00:33 -08:00
|
|
|
});
|
|
|
|
|
});
|
2025-11-26 19:30:36 -05:00
|
|
|
|
|
|
|
|
describe("Default localStorage persistence", () => {
|
|
|
|
|
it("should restore messages from localStorage on channel recreation", async () => {
|
|
|
|
|
const persistentChannelId = "persistent-channel";
|
|
|
|
|
|
|
|
|
|
const channel1 = new MessageChannel(persistentChannelId, "alice");
|
|
|
|
|
|
|
|
|
|
await sendMessage(channel1, utf8ToBytes("msg-1"), callback);
|
|
|
|
|
await sendMessage(channel1, utf8ToBytes("msg-2"), callback);
|
|
|
|
|
|
|
|
|
|
expect(channel1["localHistory"].length).to.equal(2);
|
|
|
|
|
|
|
|
|
|
// Recreate channel with same storage - should load history
|
|
|
|
|
const channel2 = new MessageChannel(persistentChannelId, "alice");
|
|
|
|
|
|
|
|
|
|
expect(channel2["localHistory"].length).to.equal(2);
|
|
|
|
|
expect(
|
|
|
|
|
channel2["localHistory"].slice(0).map((m) => m.messageId)
|
|
|
|
|
).to.deep.equal([
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes("msg-1")),
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes("msg-2"))
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should include persisted messages in causal history after restart", async () => {
|
|
|
|
|
const persistentChannelId = "persistent-causal";
|
|
|
|
|
|
|
|
|
|
const channel1 = new MessageChannel(persistentChannelId, "alice", {
|
|
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await sendMessage(channel1, utf8ToBytes("msg-1"), callback);
|
|
|
|
|
await sendMessage(channel1, utf8ToBytes("msg-2"), callback);
|
|
|
|
|
await sendMessage(channel1, utf8ToBytes("msg-3"), callback);
|
|
|
|
|
|
|
|
|
|
const channel2 = new MessageChannel(persistentChannelId, "alice", {
|
|
|
|
|
causalHistorySize: 2
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let capturedMessage: ContentMessage | null = null;
|
|
|
|
|
await sendMessage(channel2, utf8ToBytes("msg-4"), async (message) => {
|
|
|
|
|
capturedMessage = message;
|
|
|
|
|
return { success: true };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(capturedMessage).to.not.be.null;
|
|
|
|
|
expect(capturedMessage!.causalHistory).to.have.lengthOf(2);
|
|
|
|
|
// Should reference the last 2 messages (msg-2 and msg-3)
|
|
|
|
|
expect(capturedMessage!.causalHistory[0].messageId).to.equal(
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes("msg-2"))
|
|
|
|
|
);
|
|
|
|
|
expect(capturedMessage!.causalHistory[1].messageId).to.equal(
|
|
|
|
|
MessageChannel.getMessageId(utf8ToBytes("msg-3"))
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-02-11 13:24:43 -08:00
|
|
|
});
|