Merge pull request #2299 from waku-org/feat/sds-message-history

feat(sds): add retrieval hint to causal history
This commit is contained in:
Arseniy Klempner 2025-04-22 08:05:41 -07:00 committed by GitHub
commit 4da382d594
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 165 additions and 64 deletions

1
package-lock.json generated
View File

@ -42806,6 +42806,7 @@
"version": "0.0.2", "version": "0.0.2",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"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",

View File

@ -7,11 +7,81 @@
import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, MaxLengthError, message } from 'protons-runtime' import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, MaxLengthError, message } from 'protons-runtime'
import type { Uint8ArrayList } from 'uint8arraylist' import type { Uint8ArrayList } from 'uint8arraylist'
export interface HistoryEntry {
messageId: string
retrievalHint?: Uint8Array
}
export namespace HistoryEntry {
let _codec: Codec<HistoryEntry>
export const codec = (): Codec<HistoryEntry> => {
if (_codec == null) {
_codec = message<HistoryEntry>((obj, w, opts = {}) => {
if (opts.lengthDelimited !== false) {
w.fork()
}
if ((obj.messageId != null && obj.messageId !== '')) {
w.uint32(10)
w.string(obj.messageId)
}
if (obj.retrievalHint != null) {
w.uint32(18)
w.bytes(obj.retrievalHint)
}
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length, opts = {}) => {
const obj: any = {
messageId: ''
}
const end = length == null ? reader.len : reader.pos + length
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
obj.messageId = reader.string()
break
}
case 2: {
obj.retrievalHint = reader.bytes()
break
}
default: {
reader.skipType(tag & 7)
break
}
}
}
return obj
})
}
return _codec
}
export const encode = (obj: Partial<HistoryEntry>): Uint8Array => {
return encodeMessage(obj, HistoryEntry.codec())
}
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<HistoryEntry>): HistoryEntry => {
return decodeMessage(buf, HistoryEntry.codec(), opts)
}
}
export interface SdsMessage { export interface SdsMessage {
messageId: string messageId: string
channelId: string channelId: string
lamportTimestamp?: number lamportTimestamp?: number
causalHistory: string[] causalHistory: HistoryEntry[]
bloomFilter?: Uint8Array bloomFilter?: Uint8Array
content?: Uint8Array content?: Uint8Array
} }
@ -44,7 +114,7 @@ export namespace SdsMessage {
if (obj.causalHistory != null) { if (obj.causalHistory != null) {
for (const value of obj.causalHistory) { for (const value of obj.causalHistory) {
w.uint32(90) w.uint32(90)
w.string(value) HistoryEntry.codec().encode(value, w)
} }
} }
@ -91,7 +161,9 @@ export namespace SdsMessage {
throw new MaxLengthError('Decode error - map field "causalHistory" had too many elements') throw new MaxLengthError('Decode error - map field "causalHistory" had too many elements')
} }
obj.causalHistory.push(reader.string()) obj.causalHistory.push(HistoryEntry.codec().decode(reader, reader.uint32(), {
limits: opts.limits?.causalHistory$
}))
break break
} }
case 12: { case 12: {

View File

@ -1,11 +1,16 @@
syntax = "proto3"; syntax = "proto3";
message HistoryEntry {
string message_id = 1; // Unique identifier of the SDS message, as defined in `Message`
optional bytes retrieval_hint = 2; // Optional information to help remote parties retrieve this SDS message; For example, A Waku deterministic message hash or routing payload hash
}
message SdsMessage { message SdsMessage {
// 1 Reserved for sender/participant id // 1 Reserved for sender/participant id
string message_id = 2; // Unique identifier of the message string message_id = 2; // Unique identifier of the message
string channel_id = 3; // Identifier of the channel to which the message belongs string channel_id = 3; // Identifier of the channel to which the message belongs
optional int32 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel optional int32 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel
repeated string causal_history = 11; // List of preceding message IDs that this message causally depends on. Generally 2 or 3 message IDs are included. repeated HistoryEntry causal_history = 11; // List of preceding message IDs that this message causally depends on. Generally 2 or 3 message IDs are included.
optional bytes bloom_filter = 12; // Bloom filter representing received message IDs in channel optional bytes bloom_filter = 12; // Bloom filter representing received message IDs in channel
optional bytes content = 20; // Actual content of the message optional bytes content = 20; // Actual content of the message
} }

View File

@ -59,7 +59,7 @@
"node": ">=20" "node": ">=20"
}, },
"dependencies": { "dependencies": {
"@libp2p/interface": "^2.7.0", "@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",

View File

@ -4,14 +4,15 @@ import { expect } from "chai";
import { DefaultBloomFilter } from "./bloom.js"; import { DefaultBloomFilter } from "./bloom.js";
import { import {
DEFAULT_BLOOM_FILTER_OPTIONS, DEFAULT_BLOOM_FILTER_OPTIONS,
HistoryEntry,
Message, Message,
MessageChannel, MessageChannel,
MessageChannelEvent MessageChannelEvent
} from "./sds.js"; } from "./sds.js";
const channelId = "test-channel"; const channelId = "test-channel";
const callback = (_message: Message): Promise<boolean> => { const callback = (_message: Message): Promise<{ success: boolean }> => {
return Promise.resolve(true); return Promise.resolve({ success: true });
}; };
const getBloomFilter = (channel: MessageChannel): DefaultBloomFilter => { const getBloomFilter = (channel: MessageChannel): DefaultBloomFilter => {
@ -62,15 +63,16 @@ describe("MessageChannel", function () {
const expectedTimestamp = (channelA as any).lamportTimestamp + 1; const expectedTimestamp = (channelA as any).lamportTimestamp + 1;
const messageId = MessageChannel.getMessageId(new Uint8Array()); const messageId = MessageChannel.getMessageId(new Uint8Array());
await channelA.sendMessage(new Uint8Array(), callback); await channelA.sendMessage(new Uint8Array(), callback);
const messageIdLog = (channelA as any).messageIdLog as { const messageIdLog = (channelA as any).localHistory as {
timestamp: number; timestamp: number;
messageId: string; historyEntry: HistoryEntry;
}[]; }[];
expect(messageIdLog.length).to.equal(1); expect(messageIdLog.length).to.equal(1);
expect( expect(
messageIdLog.some( messageIdLog.some(
(log) => (log) =>
log.timestamp === expectedTimestamp && log.messageId === messageId log.timestamp === expectedTimestamp &&
log.historyEntry.messageId === messageId
) )
).to.equal(true); ).to.equal(true);
}); });
@ -100,12 +102,15 @@ describe("MessageChannel", function () {
// Causal history should only contain the last N messages as defined by causalHistorySize // Causal history should only contain the last N messages as defined by causalHistorySize
const causalHistory = outgoingBuffer[outgoingBuffer.length - 1] const causalHistory = outgoingBuffer[outgoingBuffer.length - 1]
.causalHistory as string[]; .causalHistory as HistoryEntry[];
expect(causalHistory.length).to.equal(causalHistorySize); expect(causalHistory.length).to.equal(causalHistorySize);
const expectedCausalHistory = messages const expectedCausalHistory = messages
.slice(-causalHistorySize - 1, -1) .slice(-causalHistorySize - 1, -1)
.map((message) => MessageChannel.getMessageId(utf8ToBytes(message))); .map((message) => ({
messageId: MessageChannel.getMessageId(utf8ToBytes(message)),
retrievalHint: undefined
}));
expect(causalHistory).to.deep.equal(expectedCausalHistory); expect(causalHistory).to.deep.equal(expectedCausalHistory);
}); });
}); });
@ -120,7 +125,7 @@ describe("MessageChannel", function () {
const timestampBefore = (channelA as any).lamportTimestamp; const timestampBefore = (channelA as any).lamportTimestamp;
await channelB.sendMessage(new Uint8Array(), (message) => { await channelB.sendMessage(new Uint8Array(), (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
const timestampAfter = (channelA as any).lamportTimestamp; const timestampAfter = (channelA as any).lamportTimestamp;
expect(timestampAfter).to.equal(timestampBefore + 1); expect(timestampAfter).to.equal(timestampBefore + 1);
@ -133,7 +138,7 @@ describe("MessageChannel", function () {
for (const m of messagesB) { for (const m of messagesB) {
await channelB.sendMessage(utf8ToBytes(m), (message) => { await channelB.sendMessage(utf8ToBytes(m), (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
const timestampAfter = (channelA as any).lamportTimestamp; const timestampAfter = (channelA as any).lamportTimestamp;
@ -147,7 +152,7 @@ describe("MessageChannel", function () {
timestamp++; timestamp++;
channelB.receiveMessage(message); channelB.receiveMessage(message);
expect((channelB as any).lamportTimestamp).to.equal(timestamp); expect((channelB as any).lamportTimestamp).to.equal(timestamp);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
@ -156,7 +161,7 @@ describe("MessageChannel", function () {
timestamp++; timestamp++;
channelA.receiveMessage(message); channelA.receiveMessage(message);
expect((channelA as any).lamportTimestamp).to.equal(timestamp); expect((channelA as any).lamportTimestamp).to.equal(timestamp);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
@ -173,7 +178,7 @@ describe("MessageChannel", function () {
channelB.receiveMessage(message); channelB.receiveMessage(message);
const bloomFilter = getBloomFilter(channelB); const bloomFilter = getBloomFilter(channelB);
expect(bloomFilter.lookup(message.messageId)).to.equal(true); expect(bloomFilter.lookup(message.messageId)).to.equal(true);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
}); });
@ -189,7 +194,7 @@ describe("MessageChannel", function () {
await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => { await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => {
receivedMessage = message; receivedMessage = message;
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
const incomingBuffer = (channelB as any).incomingBuffer as Message[]; const incomingBuffer = (channelB as any).incomingBuffer as Message[];
@ -201,12 +206,15 @@ describe("MessageChannel", function () {
expect(timestampAfter).to.equal(timestampBefore); expect(timestampAfter).to.equal(timestampBefore);
// Message should not be in local history // Message should not be in local history
const localHistory = (channelB as any).messageIdLog as { const localHistory = (channelB as any).localHistory as {
timestamp: number; timestamp: number;
messageId: string; historyEntry: HistoryEntry;
}[]; }[];
expect( expect(
localHistory.some((m) => m.messageId === receivedMessage!.messageId) localHistory.some(
({ historyEntry: { messageId } }) =>
messageId === receivedMessage!.messageId
)
).to.equal(false); ).to.equal(false);
}); });
}); });
@ -221,14 +229,14 @@ describe("MessageChannel", function () {
for (const m of messagesA) { for (const m of messagesA) {
await channelA.sendMessage(utf8ToBytes(m), (message) => { await channelA.sendMessage(utf8ToBytes(m), (message) => {
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
let notInHistory: Message | null = null; let notInHistory: Message | null = null;
await channelA.sendMessage(utf8ToBytes("not-in-history"), (message) => { await channelA.sendMessage(utf8ToBytes("not-in-history"), (message) => {
notInHistory = message; notInHistory = message;
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
expect((channelA as any).outgoingBuffer.length).to.equal( expect((channelA as any).outgoingBuffer.length).to.equal(
@ -237,7 +245,7 @@ describe("MessageChannel", function () {
await channelB.sendMessage(utf8ToBytes(messagesB[0]), (message) => { await channelB.sendMessage(utf8ToBytes(messagesB[0]), (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
// Since messagesA are in causal history of channel B's message // Since messagesA are in causal history of channel B's message
@ -262,7 +270,7 @@ describe("MessageChannel", function () {
for (const m of messages) { for (const m of messages) {
await channelA.sendMessage(utf8ToBytes(m), (message) => { await channelA.sendMessage(utf8ToBytes(m), (message) => {
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
@ -276,7 +284,7 @@ describe("MessageChannel", function () {
utf8ToBytes(messagesB[messagesB.length - 1]), utf8ToBytes(messagesB[messagesB.length - 1]),
(message) => { (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
} }
); );
@ -310,7 +318,7 @@ describe("MessageChannel", function () {
// Send messages until acknowledgement count is reached // Send messages until acknowledgement count is reached
await channelB.sendMessage(utf8ToBytes(`x-${i}`), (message) => { await channelB.sendMessage(utf8ToBytes(`x-${i}`), (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
@ -344,9 +352,9 @@ describe("MessageChannel", function () {
await channelA.sendMessage(utf8ToBytes(m), callback); await channelA.sendMessage(utf8ToBytes(m), callback);
} }
await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => { await channelA.sendMessage(utf8ToBytes(messagesB[0]), async (message) => {
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
const incomingBuffer = (channelB as any).incomingBuffer as Message[]; const incomingBuffer = (channelB as any).incomingBuffer as Message[];
@ -357,7 +365,7 @@ describe("MessageChannel", function () {
const missingMessages = channelB.sweepIncomingBuffer(); const missingMessages = channelB.sweepIncomingBuffer();
expect(missingMessages.length).to.equal(causalHistorySize); expect(missingMessages.length).to.equal(causalHistorySize);
expect(missingMessages[0]).to.equal( expect(missingMessages[0].messageId).to.equal(
MessageChannel.getMessageId(utf8ToBytes(messagesA[0])) MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
); );
}); });
@ -368,18 +376,18 @@ describe("MessageChannel", function () {
for (const m of messagesA) { for (const m of messagesA) {
await channelA.sendMessage(utf8ToBytes(m), (message) => { await channelA.sendMessage(utf8ToBytes(m), (message) => {
sentMessages.push(message); sentMessages.push(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => { await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => {
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
const missingMessages = channelB.sweepIncomingBuffer(); const missingMessages = channelB.sweepIncomingBuffer();
expect(missingMessages.length).to.equal(causalHistorySize); expect(missingMessages.length).to.equal(causalHistorySize);
expect(missingMessages[0]).to.equal( expect(missingMessages[0].messageId).to.equal(
MessageChannel.getMessageId(utf8ToBytes(messagesA[0])) MessageChannel.getMessageId(utf8ToBytes(messagesA[0]))
); );
@ -411,7 +419,7 @@ describe("MessageChannel", function () {
await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => { await channelA.sendMessage(utf8ToBytes(messagesB[0]), (message) => {
channelC.receiveMessage(message); channelC.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
const missingMessages = channelC.sweepIncomingBuffer(); const missingMessages = channelC.sweepIncomingBuffer();
@ -439,7 +447,7 @@ describe("MessageChannel", function () {
await channelA.sendMessage(utf8ToBytes(m), (message) => { await channelA.sendMessage(utf8ToBytes(m), (message) => {
unacknowledgedMessages.push(message); unacknowledgedMessages.push(message);
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }
@ -458,7 +466,7 @@ describe("MessageChannel", function () {
utf8ToBytes(messagesB[causalHistorySize]), utf8ToBytes(messagesB[causalHistorySize]),
(message) => { (message) => {
channelA.receiveMessage(message); channelA.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
} }
); );
@ -495,7 +503,7 @@ describe("MessageChannel", function () {
bloomFilter.lookup(MessageChannel.getMessageId(new Uint8Array())) bloomFilter.lookup(MessageChannel.getMessageId(new Uint8Array()))
).to.equal(false); ).to.equal(false);
const localLog = (channelA as any).messageIdLog as { const localLog = (channelA as any).localHistory as {
timestamp: number; timestamp: number;
messageId: string; messageId: string;
}[]; }[];
@ -514,7 +522,7 @@ describe("MessageChannel", function () {
expect(timestampAfter).to.equal(expectedTimestamp); expect(timestampAfter).to.equal(expectedTimestamp);
expect(timestampAfter).to.be.greaterThan(timestampBefore); expect(timestampAfter).to.be.greaterThan(timestampBefore);
const localLog = (channelB as any).messageIdLog as { const localLog = (channelB as any).localHistory as {
timestamp: number; timestamp: number;
messageId: string; messageId: string;
}[]; }[];
@ -530,7 +538,7 @@ describe("MessageChannel", function () {
for (const m of messagesA) { for (const m of messagesA) {
await channelA.sendMessage(utf8ToBytes(m), (message) => { await channelA.sendMessage(utf8ToBytes(m), (message) => {
channelB.receiveMessage(message); channelB.receiveMessage(message);
return Promise.resolve(true); return Promise.resolve({ success: true });
}); });
} }

View File

@ -13,6 +13,7 @@ type MessageChannelEvents = {
}; };
export type Message = proto_sds_message.SdsMessage; export type Message = proto_sds_message.SdsMessage;
export type HistoryEntry = proto_sds_message.HistoryEntry;
export type ChannelId = string; export type ChannelId = string;
export const DEFAULT_BLOOM_FILTER_OPTIONS = { export const DEFAULT_BLOOM_FILTER_OPTIONS = {
@ -36,7 +37,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
private outgoingBuffer: Message[]; private outgoingBuffer: Message[];
private acknowledgements: Map<string, number>; private acknowledgements: Map<string, number>;
private incomingBuffer: Message[]; private incomingBuffer: Message[];
private messageIdLog: { timestamp: number; messageId: string }[]; private localHistory: { timestamp: number; historyEntry: HistoryEntry }[];
private channelId: ChannelId; private channelId: ChannelId;
private causalHistorySize: number; private causalHistorySize: number;
private acknowledgementCount: number; private acknowledgementCount: number;
@ -56,7 +57,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
this.outgoingBuffer = []; this.outgoingBuffer = [];
this.acknowledgements = new Map(); this.acknowledgements = new Map();
this.incomingBuffer = []; this.incomingBuffer = [];
this.messageIdLog = []; this.localHistory = [];
this.causalHistorySize = this.causalHistorySize =
options.causalHistorySize ?? DEFAULT_CAUSAL_HISTORY_SIZE; options.causalHistorySize ?? DEFAULT_CAUSAL_HISTORY_SIZE;
this.acknowledgementCount = this.getAcknowledgementCount(); this.acknowledgementCount = this.getAcknowledgementCount();
@ -90,7 +91,10 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
*/ */
public async sendMessage( public async sendMessage(
payload: Uint8Array, payload: Uint8Array,
callback?: (message: Message) => Promise<boolean> callback?: (message: Message) => Promise<{
success: boolean;
retrievalHint?: Uint8Array;
}>
): Promise<void> { ): Promise<void> {
this.lamportTimestamp++; this.lamportTimestamp++;
@ -100,9 +104,9 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
messageId, messageId,
channelId: this.channelId, channelId: this.channelId,
lamportTimestamp: this.lamportTimestamp, lamportTimestamp: this.lamportTimestamp,
causalHistory: this.messageIdLog causalHistory: this.localHistory
.slice(-this.causalHistorySize) .slice(-this.causalHistorySize)
.map(({ messageId }) => messageId), .map(({ historyEntry }) => historyEntry),
bloomFilter: this.filter.toBytes(), bloomFilter: this.filter.toBytes(),
content: payload content: payload
}; };
@ -110,10 +114,16 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
this.outgoingBuffer.push(message); this.outgoingBuffer.push(message);
if (callback) { if (callback) {
const success = await callback(message); const { success, retrievalHint } = await callback(message);
if (success) { if (success) {
this.filter.insert(messageId); this.filter.insert(messageId);
this.messageIdLog.push({ timestamp: this.lamportTimestamp, messageId }); this.localHistory.push({
timestamp: this.lamportTimestamp,
historyEntry: {
messageId,
retrievalHint
}
});
} }
} }
} }
@ -175,9 +185,10 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
this.filter.insert(message.messageId); this.filter.insert(message.messageId);
} }
// verify causal history // verify causal history
const dependenciesMet = message.causalHistory.every((messageId) => const dependenciesMet = message.causalHistory.every((historyEntry) =>
this.messageIdLog.some( this.localHistory.some(
({ messageId: logMessageId }) => logMessageId === messageId ({ historyEntry: { messageId } }) =>
messageId === historyEntry.messageId
) )
); );
if (!dependenciesMet) { if (!dependenciesMet) {
@ -189,17 +200,18 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
} }
// https://rfc.vac.dev/vac/raw/sds/#periodic-incoming-buffer-sweep // https://rfc.vac.dev/vac/raw/sds/#periodic-incoming-buffer-sweep
public sweepIncomingBuffer(): string[] { public sweepIncomingBuffer(): HistoryEntry[] {
const { buffer, missing } = this.incomingBuffer.reduce<{ const { buffer, missing } = this.incomingBuffer.reduce<{
buffer: Message[]; buffer: Message[];
missing: string[]; missing: HistoryEntry[];
}>( }>(
({ buffer, missing }, message) => { ({ buffer, missing }, message) => {
// Check each message for missing dependencies // Check each message for missing dependencies
const missingDependencies = message.causalHistory.filter( const missingDependencies = message.causalHistory.filter(
(messageId) => (messageHistoryEntry) =>
!this.messageIdLog.some( !this.localHistory.some(
({ messageId: logMessageId }) => logMessageId === messageId ({ historyEntry: { messageId } }) =>
messageId === messageHistoryEntry.messageId
) )
); );
if (missingDependencies.length === 0) { if (missingDependencies.length === 0) {
@ -227,7 +239,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
missing: missing.concat(missingDependencies) missing: missing.concat(missingDependencies)
}; };
}, },
{ buffer: new Array<Message>(), missing: new Array<string>() } { buffer: new Array<Message>(), missing: new Array<HistoryEntry>() }
); );
// Update the incoming buffer to only include messages with no missing dependencies // Update the incoming buffer to only include messages with no missing dependencies
this.incomingBuffer = buffer; this.incomingBuffer = buffer;
@ -284,9 +296,9 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
messageId: MessageChannel.getMessageId(emptyMessage), messageId: MessageChannel.getMessageId(emptyMessage),
channelId: this.channelId, channelId: this.channelId,
lamportTimestamp: this.lamportTimestamp, lamportTimestamp: this.lamportTimestamp,
causalHistory: this.messageIdLog causalHistory: this.localHistory
.slice(-this.causalHistorySize) .slice(-this.causalHistorySize)
.map(({ messageId }) => messageId), .map(({ historyEntry }) => historyEntry),
bloomFilter: this.filter.toBytes(), bloomFilter: this.filter.toBytes(),
content: emptyMessage content: emptyMessage
}; };
@ -298,7 +310,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
} }
// 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, retrievalHint?: Uint8Array): void {
this.notifyDeliveredMessage(message.messageId); this.notifyDeliveredMessage(message.messageId);
const messageLamportTimestamp = message.lamportTimestamp ?? 0; const messageLamportTimestamp = message.lamportTimestamp ?? 0;
@ -321,15 +333,18 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
// If one or more message IDs with the same Lamport timestamp already exists, // If one or more message IDs with the same Lamport timestamp already exists,
// the participant MUST follow the Resolve Conflicts procedure. // the participant MUST follow the Resolve Conflicts procedure.
// https://rfc.vac.dev/vac/raw/sds/#resolve-conflicts // https://rfc.vac.dev/vac/raw/sds/#resolve-conflicts
this.messageIdLog.push({ this.localHistory.push({
timestamp: messageLamportTimestamp, timestamp: messageLamportTimestamp,
messageId: message.messageId historyEntry: {
messageId: message.messageId,
retrievalHint
}
}); });
this.messageIdLog.sort((a, b) => { this.localHistory.sort((a, b) => {
if (a.timestamp !== b.timestamp) { if (a.timestamp !== b.timestamp) {
return a.timestamp - b.timestamp; return a.timestamp - b.timestamp;
} }
return a.messageId.localeCompare(b.messageId); return a.historyEntry.messageId.localeCompare(b.historyEntry.messageId);
}); });
} }
@ -338,9 +353,9 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
// See https://rfc.vac.dev/vac/raw/sds/#review-ack-status // See https://rfc.vac.dev/vac/raw/sds/#review-ack-status
private reviewAckStatus(receivedMessage: Message): void { private reviewAckStatus(receivedMessage: Message): void {
// the participant MUST mark all messages in the received causal_history as acknowledged. // the participant MUST mark all messages in the received causal_history as acknowledged.
receivedMessage.causalHistory.forEach((messageId) => { receivedMessage.causalHistory.forEach(({ messageId }) => {
this.outgoingBuffer = this.outgoingBuffer.filter( this.outgoingBuffer = this.outgoingBuffer.filter(
(msg) => msg.messageId !== messageId ({ messageId: outgoingMessageId }) => outgoingMessageId !== messageId
); );
this.acknowledgements.delete(messageId); this.acknowledgements.delete(messageId);
if (!this.filter.lookup(messageId)) { if (!this.filter.lookup(messageId)) {