Hanno Cornelius 5334a7fcc9
feat: add SDS-Repair (SDS-R) to the SDS implementation (#2698)
* wip

* feat: integrate sds-r with message channels

* feat: add SDS-R events

* fix: fixed buffer handling incoming and outgoing

* fix: more buffer fixes

* fix: remove some magic numbers

* fix: buffer optimisation, backwards compatible senderId

* fix: fix implementation guide, remove unrelated claude file

* fix: further buffer optimisations

* fix: linting errors

* fix: suggestions from code review

Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
Co-authored-by: fryorcraken <110212804+fryorcraken@users.noreply.github.com>

* fix: remove implementation guide

* fix: build errors, remove override, improve buffer

* fix: consistent use of MessageId and ParticipantId

* fix: switch to conditionally constructed from conditionally executed

---------

Co-authored-by: fryorcraken <commits@fryorcraken.xyz>
Co-authored-by: Sasha <118575614+weboko@users.noreply.github.com>
Co-authored-by: fryorcraken <110212804+fryorcraken@users.noreply.github.com>
2025-10-28 10:27:06 +00:00

250 lines
6.7 KiB
TypeScript

import { proto_sds_message } from "@waku/proto";
import { Logger } from "@waku/utils";
export type MessageId = string;
export type HistoryEntry = proto_sds_message.HistoryEntry;
export type ChannelId = string;
export type ParticipantId = string;
const log = new Logger("sds:message");
export class Message implements proto_sds_message.SdsMessage {
public constructor(
public messageId: MessageId,
public channelId: string,
public senderId: ParticipantId,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp?: bigint | undefined,
public bloomFilter?: Uint8Array<ArrayBufferLike> | undefined,
public content?: Uint8Array<ArrayBufferLike> | undefined,
public repairRequest: proto_sds_message.HistoryEntry[] = [],
/**
* Not encoded, set after it is sent, used to include in follow-up messages
*/
public retrievalHint?: Uint8Array | undefined
) {}
public encode(): Uint8Array {
return proto_sds_message.SdsMessage.encode(this);
}
public static decode(
data: Uint8Array
): undefined | ContentMessage | SyncMessage | EphemeralMessage {
try {
const {
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp,
bloomFilter,
content,
repairRequest
} = proto_sds_message.SdsMessage.decode(data);
if (testContentMessage({ lamportTimestamp, content })) {
return new ContentMessage(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
bloomFilter,
content!,
repairRequest
);
}
if (testEphemeralMessage({ lamportTimestamp, content })) {
return new EphemeralMessage(
messageId,
channelId,
senderId,
causalHistory,
undefined,
bloomFilter,
content!,
repairRequest
);
}
if (testSyncMessage({ lamportTimestamp, content })) {
return new SyncMessage(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
bloomFilter,
undefined,
repairRequest
);
}
log.error(
"message received was of unknown type",
lamportTimestamp,
content
);
} catch (err) {
log.error("failed to decode sds message", err);
}
return undefined;
}
}
export class SyncMessage extends Message {
public constructor(
public messageId: MessageId,
public channelId: string,
public senderId: ParticipantId,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp: bigint,
public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
public content: undefined,
public repairRequest: proto_sds_message.HistoryEntry[] = [],
/**
* Not encoded, set after it is sent, used to include in follow-up messages
*/
public retrievalHint?: Uint8Array | undefined
) {
super(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp,
bloomFilter,
content,
repairRequest,
retrievalHint
);
}
}
function testSyncMessage(message: {
lamportTimestamp?: bigint;
content?: Uint8Array;
}): boolean {
return Boolean(
"lamportTimestamp" in message &&
typeof message.lamportTimestamp === "bigint" &&
(message.content === undefined || message.content.length === 0)
);
}
export function isSyncMessage(
message: Message | ContentMessage | SyncMessage | EphemeralMessage
): message is SyncMessage {
return testSyncMessage(message);
}
export class EphemeralMessage extends Message {
public constructor(
public messageId: MessageId,
public channelId: string,
public senderId: ParticipantId,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp: undefined,
public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
public content: Uint8Array<ArrayBufferLike>,
public repairRequest: proto_sds_message.HistoryEntry[] = [],
/**
* Not encoded, set after it is sent, used to include in follow-up messages
*/
public retrievalHint?: Uint8Array | undefined
) {
if (!content || !content.length) {
throw Error("Ephemeral Message must have content");
}
super(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp,
bloomFilter,
content,
repairRequest,
retrievalHint
);
}
}
export function isEphemeralMessage(
message: Message | ContentMessage | SyncMessage | EphemeralMessage
): message is EphemeralMessage {
return testEphemeralMessage(message);
}
function testEphemeralMessage(message: {
lamportTimestamp?: bigint;
content?: Uint8Array;
}): boolean {
return Boolean(
message.lamportTimestamp === undefined &&
"content" in message &&
message.content &&
message.content.length
);
}
export class ContentMessage extends Message {
public constructor(
public messageId: MessageId,
public channelId: string,
public senderId: ParticipantId,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp: bigint,
public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
public content: Uint8Array<ArrayBufferLike>,
public repairRequest: proto_sds_message.HistoryEntry[] = [],
/**
* Not encoded, set after it is sent, used to include in follow-up messages
*/
public retrievalHint?: Uint8Array | undefined
) {
if (!content.length) {
throw Error("Content Message must have content");
}
super(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp,
bloomFilter,
content,
repairRequest,
retrievalHint
);
}
// `valueOf` is used by comparison operands such as `<`
public valueOf(): string {
// Create a sortable string representation that matches the compare logic
// Pad lamportTimestamp to ensure proper lexicographic ordering
// Use 16 digits to handle up to Number.MAX_SAFE_INTEGER (9007199254740991)
const paddedTimestamp = this.lamportTimestamp.toString().padStart(16, "0");
return `${paddedTimestamp}_${this.messageId}`;
}
}
export function isContentMessage(
message: Message | ContentMessage
): message is ContentMessage {
return testContentMessage(message);
}
function testContentMessage(message: {
lamportTimestamp?: bigint;
content?: Uint8Array;
}): message is { lamportTimestamp: bigint; content: Uint8Array } {
return Boolean(
"lamportTimestamp" in message &&
typeof message.lamportTimestamp === "bigint" &&
message.content &&
message.content.length
);
}