From d29994d457eacf9ac3cbbb03fb59265b6d041f55 Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 20 Aug 2025 22:54:20 +0200 Subject: [PATCH 01/16] add draft --- packages/interfaces/src/waku.ts | 13 ++++ packages/sdk/src/messaging/index.ts | 1 + packages/sdk/src/messaging/messaging.ts | 87 +++++++++++++++++++++++++ packages/sdk/src/waku/waku.ts | 17 +++++ 4 files changed, 118 insertions(+) create mode 100644 packages/sdk/src/messaging/index.ts create mode 100644 packages/sdk/src/messaging/messaging.ts diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index 5c99f716e6..394e87a0b5 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -53,9 +53,22 @@ export type IWakuEventEmitter = TypedEventEmitter; export interface IWaku { libp2p: Libp2p; + + /** + * @deprecated should not be accessed directly, use {@link IWaku.send} and {@link IWaku.subscribe} instead + */ relay?: IRelay; + store?: IStore; + + /** + * @deprecated should not be accessed directly, use {@link IWaku.subscribe} instead + */ filter?: IFilter; + + /** + * @deprecated should not be accessed directly, use {@link IWaku.send} instead + */ lightPush?: ILightPush; /** diff --git a/packages/sdk/src/messaging/index.ts b/packages/sdk/src/messaging/index.ts new file mode 100644 index 0000000000..0035e4eb2f --- /dev/null +++ b/packages/sdk/src/messaging/index.ts @@ -0,0 +1 @@ +export { Messaging } from "./messaging.js"; diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts new file mode 100644 index 0000000000..1324961d36 --- /dev/null +++ b/packages/sdk/src/messaging/messaging.ts @@ -0,0 +1,87 @@ +import { messageHashStr } from "@waku/core"; +import { + IDecodedMessage, + IEncoder, + IFilter, + ILightPush, + IMessage, + IStore +} from "@waku/interfaces"; + +interface IMessaging { + send(encoder: IEncoder, message: IMessage): Promise; +} + +type MessagingConstructorParams = { + lightPush: ILightPush; + filter: IFilter; + store: IStore; +}; + +export class Messaging implements IMessaging { + public constructor(params: MessagingConstructorParams) {} + + public send(encoder: IEncoder, message: IMessage): Promise { + return Promise.resolve(); + } +} + +class MessageStore { + // const hash: { encoder, message, filterAck, storeAck } + // filterAck(hash) + // storeAck(hash) + // markSent(hash) + // queue(encoder, message) + // getMessagesToSend() + // -> not sent yet (first) + // -> sent more than 2s ago but not acked yet (store or filter) +} + +type ICodec = null; + +interface IAckManager { + start(): void; + stop(): void; + subscribe(codec: ICodec): void; +} + +class FilterAckManager implements IAckManager { + private subscriptions: Set = new Set(); + + public constructor( + private messageStore: MessageStore, + private filter: IFilter + ) {} + + public start(): void {} + + public stop(): void {} + + public async subscribe(codec: ICodec): Promise { + return this.filter.subscribe(codec, this.onMessage.bind(this)); + } + + private async onMessage(message: IDecodedMessage): Promise { + const hash = messageHashStr(message.pubsubTopic, message); + + if (this.messageStore.has(message)) { + this.messageStore.markFilterAck(hash); + } else { + this.messageStore.put(message); + this.messageStore.markFilterAck(hash); + } + } +} + +class StoreAckManager implements IAckManager { + public constructor( + private messageStore: MessageStore, + private store: IStore + ) {} + + public start(): void {} + + public stop(): void {} + + public subscribe(codec: ICodec): void {} +} diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 38dfec9ffa..a3c36ef384 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -15,6 +15,7 @@ import type { IEncoder, IFilter, ILightPush, + IMessage, IRelay, IRoutingInfo, IStore, @@ -33,6 +34,7 @@ import { createRoutingInfo, Logger } from "@waku/utils"; import { Filter } from "../filter/index.js"; import { HealthIndicator } from "../health_indicator/index.js"; import { LightPush } from "../light_push/index.js"; +import { Messaging } from "../messaging/index.js"; import { PeerManager } from "../peer_manager/index.js"; import { Store } from "../store/index.js"; @@ -64,6 +66,7 @@ export class WakuNode implements IWaku { private readonly connectionManager: ConnectionManager; private readonly peerManager: PeerManager; private readonly healthIndicator: HealthIndicator; + private readonly messaging: Messaging | null = null; public constructor( options: CreateNodeOptions, @@ -126,6 +129,14 @@ export class WakuNode implements IWaku { }); } + if (this.lightPush && this.filter && this.store) { + this.messaging = new Messaging({ + lightPush: this.lightPush, + filter: this.filter, + store: this.store + }); + } + log.info( "Waku node created", peerId, @@ -220,6 +231,7 @@ export class WakuNode implements IWaku { this.peerManager.start(); this.healthIndicator.start(); this.lightPush?.start(); + this.sender?.start(); this._nodeStateLock = false; this._nodeStarted = true; @@ -230,6 +242,7 @@ export class WakuNode implements IWaku { this._nodeStateLock = true; + this.sender?.stop(); this.lightPush?.stop(); this.healthIndicator.stop(); this.peerManager.stop(); @@ -280,6 +293,10 @@ export class WakuNode implements IWaku { }); } + public send(encoder: IEncoder, message: IMessage): Promise { + return this.messaging?.send(encoder, message) ?? Promise.resolve(); + } + private createRoutingInfo( contentTopic?: string, shardId?: number From 01288ff5e2032467fd403db7b655eb3707f3a535 Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 5 Sep 2025 00:18:02 +0200 Subject: [PATCH 02/16] add draft api --- packages/sdk/src/messaging/messaging.ts | 76 ++++++++++++++++++++----- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index 1324961d36..f68708311b 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -1,6 +1,6 @@ -import { messageHashStr } from "@waku/core"; import { IDecodedMessage, + IDecoder, IEncoder, IFilter, ILightPush, @@ -37,7 +37,7 @@ class MessageStore { // -> sent more than 2s ago but not acked yet (store or filter) } -type ICodec = null; +type ICodec = IEncoder & IDecoder; interface IAckManager { start(): void; @@ -46,42 +46,88 @@ interface IAckManager { } class FilterAckManager implements IAckManager { - private subscriptions: Set = new Set(); + private codecs: Set = new Set(); public constructor( private messageStore: MessageStore, private filter: IFilter ) {} - public start(): void {} + public start(): void { + // noop + } + + public async stop(): Promise { + const promises = Array.from(this.codecs.entries()).map((codec) => { + return this.filter.unsubscribe(codec); + }); - public stop(): void {} + await Promise.all(promises); + this.codecs.clear(); + } public async subscribe(codec: ICodec): Promise { return this.filter.subscribe(codec, this.onMessage.bind(this)); } private async onMessage(message: IDecodedMessage): Promise { - const hash = messageHashStr(message.pubsubTopic, message); - - if (this.messageStore.has(message)) { - this.messageStore.markFilterAck(hash); - } else { - this.messageStore.put(message); - this.messageStore.markFilterAck(hash); + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); } + + this.messageStore.markFilterAck(message.hashStr); } } class StoreAckManager implements IAckManager { + private interval: ReturnType | null = null; + + private codecs: Set = new Set(); + public constructor( private messageStore: MessageStore, private store: IStore ) {} - public start(): void {} + public start(): void { + if (this.interval) { + return; + } - public stop(): void {} + this.interval = setInterval(() => { + void this.query(); + }, 1000); + } - public subscribe(codec: ICodec): void {} + public stop(): void { + if (!this.interval) { + return; + } + + clearInterval(this.interval); + this.interval = null; + } + + public subscribe(codec: ICodec): void { + this.codecs.add(codec); + } + + private async query(): Promise { + for (const codec of this.codecs) { + await this.store.queryWithOrderedCallback( + [codec], + (message) => { + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); + } + + this.messageStore.markStoreAck(message.hashStr); + }, + { + timeStart: new Date(Date.now() - 60 * 60 * 1000), + timeEnd: new Date() + } + ); + } + } } From 3de906a78a1a1789a355304cd9e4694858838e06 Mon Sep 17 00:00:00 2001 From: Sasha Date: Thu, 25 Sep 2025 00:53:29 +0200 Subject: [PATCH 03/16] implement basic entites and structure, decouple into separate files --- packages/sdk/src/messaging/fitler_ack.ts | 44 +++++ packages/sdk/src/messaging/message_store.ts | 101 +++++++++++ packages/sdk/src/messaging/messaging.spec.ts | 170 +++++++++++++++++++ packages/sdk/src/messaging/messaging.ts | 131 +++----------- packages/sdk/src/messaging/store_ack.ts | 58 +++++++ packages/sdk/src/messaging/utils.ts | 10 ++ packages/sdk/src/waku/waku.ts | 4 +- 7 files changed, 413 insertions(+), 105 deletions(-) create mode 100644 packages/sdk/src/messaging/fitler_ack.ts create mode 100644 packages/sdk/src/messaging/message_store.ts create mode 100644 packages/sdk/src/messaging/messaging.spec.ts create mode 100644 packages/sdk/src/messaging/store_ack.ts create mode 100644 packages/sdk/src/messaging/utils.ts diff --git a/packages/sdk/src/messaging/fitler_ack.ts b/packages/sdk/src/messaging/fitler_ack.ts new file mode 100644 index 0000000000..b5ed7562c3 --- /dev/null +++ b/packages/sdk/src/messaging/fitler_ack.ts @@ -0,0 +1,44 @@ +import { IDecodedMessage, IFilter } from "@waku/interfaces"; + +import { MessageStore } from "./message_store.js"; +import { IAckManager, ICodec } from "./utils.js"; + +export class FilterAckManager implements IAckManager { + private codecs: Set = new Set(); + + public constructor( + private messageStore: MessageStore, + private filter: IFilter + ) {} + + public start(): void { + return; + } + + public async stop(): Promise { + const promises = Array.from(this.codecs.entries()).map((codec) => + this.filter.unsubscribe(codec) + ); + await Promise.all(promises); + this.codecs.clear(); + } + + public async subscribe(codec: ICodec): Promise { + const success = await this.filter.subscribe( + codec, + this.onMessage.bind(this) + ); + if (success) { + this.codecs.add(codec); + } + return success; + } + + private async onMessage(message: IDecodedMessage): Promise { + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); + } + + this.messageStore.markFilterAck(message.hashStr); + } +} diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts new file mode 100644 index 0000000000..910f5cee60 --- /dev/null +++ b/packages/sdk/src/messaging/message_store.ts @@ -0,0 +1,101 @@ +import { messageHashStr } from "@waku/core"; +import { IDecodedMessage, IEncoder, IMessage } from "@waku/interfaces"; + +type QueuedMessage = { + encoder?: IEncoder; + message?: IMessage; + filterAck: boolean; + storeAck: boolean; + lastSentAt?: number; + createdAt: number; +}; + +type MessageStoreOptions = { + resendIntervalMs?: number; +}; + +export class MessageStore { + private readonly messages: Map = new Map(); + private readonly resendIntervalMs: number; + + public constructor(options: MessageStoreOptions = {}) { + this.resendIntervalMs = options.resendIntervalMs ?? 2000; + } + + public has(hashStr: string): boolean { + return this.messages.has(hashStr); + } + + public add(message: IDecodedMessage): void { + if (!this.messages.has(message.hashStr)) { + this.messages.set(message.hashStr, { + filterAck: false, + storeAck: false, + createdAt: Date.now() + }); + } + } + + public markFilterAck(hashStr: string): void { + const entry = this.messages.get(hashStr); + if (!entry) return; + entry.filterAck = true; + } + + public markStoreAck(hashStr: string): void { + const entry = this.messages.get(hashStr); + if (!entry) return; + entry.storeAck = true; + } + + public markSent(hashStr: string): void { + const entry = this.messages.get(hashStr); + if (!entry) return; + entry.lastSentAt = Date.now(); + } + + public async queue( + encoder: IEncoder, + message: IMessage + ): Promise { + const proto = await encoder.toProtoObj(message); + if (!proto) return undefined; + const hashStr = messageHashStr(encoder.pubsubTopic, proto); + const existing = this.messages.get(hashStr); + if (!existing) { + this.messages.set(hashStr, { + encoder, + message, + filterAck: false, + storeAck: false, + createdAt: Date.now() + }); + } + return hashStr; + } + + public getMessagesToSend(): Array<{ + hashStr: string; + encoder: IEncoder; + message: IMessage; + }> { + const now = Date.now(); + const res: Array<{ + hashStr: string; + encoder: IEncoder; + message: IMessage; + }> = []; + for (const [hashStr, entry] of this.messages.entries()) { + if (!entry.encoder || !entry.message) continue; + const isAcknowledged = entry.filterAck || entry.storeAck; + if (isAcknowledged) continue; + if ( + !entry.lastSentAt || + now - entry.lastSentAt >= this.resendIntervalMs + ) { + res.push({ hashStr, encoder: entry.encoder, message: entry.message }); + } + } + return res; + } +} diff --git a/packages/sdk/src/messaging/messaging.spec.ts b/packages/sdk/src/messaging/messaging.spec.ts new file mode 100644 index 0000000000..209336634c --- /dev/null +++ b/packages/sdk/src/messaging/messaging.spec.ts @@ -0,0 +1,170 @@ +import { createDecoder, createEncoder } from "@waku/core"; +import type { + IDecodedMessage, + IDecoder, + IEncoder, + IFilter, + ILightPush, + IMessage, + IStore +} from "@waku/interfaces"; +import { createRoutingInfo } from "@waku/utils"; +import { utf8ToBytes } from "@waku/utils/bytes"; +import { expect } from "chai"; +import sinon from "sinon"; + +import { + FilterAckManager, + MessageStore, + Messaging, + StoreAckManager +} from "./messaging.js"; + +const testContentTopic = "/test/1/waku-messaging/utf8"; +const testNetworkconfig = { + clusterId: 0, + numShardsInCluster: 9 +}; +const testRoutingInfo = createRoutingInfo(testNetworkconfig, { + contentTopic: testContentTopic +}); + +describe("MessageStore", () => { + it("queues, marks sent and acks", async () => { + const encoder = createEncoder({ + contentTopic: testContentTopic, + routingInfo: testRoutingInfo + }); + const store = new MessageStore({ resendIntervalMs: 1 }); + const msg: IMessage = { payload: utf8ToBytes("hello") }; + + const hash = await store.queue(encoder as IEncoder, msg); + expect(hash).to.be.a("string"); + if (!hash) return; + expect(store.has(hash)).to.be.true; + store.markSent(hash); + store.markFilterAck(hash); + store.markStoreAck(hash); + + const toSend = store.getMessagesToSend(); + expect(toSend.length).to.eq(0); + }); +}); +describe("FilterAckManager", () => { + it("subscribes and marks filter ack on messages", async () => { + const store = new MessageStore(); + const filter: IFilter = { + multicodec: "filter", + start: sinon.stub().resolves(), + stop: sinon.stub().resolves(), + subscribe: sinon.stub().callsFake(async (_dec, cb: any) => { + const decoder = createDecoder(testContentTopic, testRoutingInfo); + const proto = await decoder.fromProtoObj(decoder.pubsubTopic, { + payload: utf8ToBytes("x"), + contentTopic: testContentTopic, + version: 0, + timestamp: BigInt(Date.now()), + meta: undefined, + rateLimitProof: undefined, + ephemeral: false + } as any); + if (proto) { + await cb({ ...proto, hashStr: "hash" } as IDecodedMessage); + } + return true; + }), + unsubscribe: sinon.stub().resolves(true), + unsubscribeAll: sinon.stub() + } as unknown as IFilter; + + const mgr = new FilterAckManager(store, filter); + const encoder = createEncoder({ + contentTopic: testContentTopic, + routingInfo: testRoutingInfo + }); + + const subscribed = await mgr.subscribe({ + ...encoder, + fromWireToProtoObj: (b: Uint8Array) => + createDecoder(testContentTopic, testRoutingInfo).fromWireToProtoObj(b), + fromProtoObj: (pubsub: string, p: any) => + createDecoder(testContentTopic, testRoutingInfo).fromProtoObj(pubsub, p) + } as unknown as IDecoder & IEncoder); + expect(subscribed).to.be.true; + }); +}); + +describe("StoreAckManager", () => { + it("queries and marks store ack", async () => { + const store = new MessageStore(); + const decoder = createDecoder(testContentTopic, testRoutingInfo); + const d = decoder as IDecoder & IEncoder; + + const mockStore: IStore = { + multicodec: "store", + createCursor: sinon.stub() as any, + queryGenerator: sinon.stub() as any, + queryWithOrderedCallback: sinon + .stub() + .callsFake(async (_decs: any, cb: any) => { + const proto = await decoder.fromProtoObj(decoder.pubsubTopic, { + payload: utf8ToBytes("x"), + contentTopic: testContentTopic, + version: 0, + timestamp: BigInt(Date.now()), + meta: undefined, + rateLimitProof: undefined, + ephemeral: false + } as any); + if (proto) { + await cb({ ...proto, hashStr: "hash2" }); + } + }), + queryWithPromiseCallback: sinon.stub() as any + } as unknown as IStore; + + const mgr = new StoreAckManager(store, mockStore); + await mgr.subscribe(d); + mgr.start(); + await new Promise((r) => setTimeout(r, 5)); + mgr.stop(); + }); +}); + +describe("Messaging", () => { + it("queues and sends via light push, marks sent", async () => { + const encoder = createEncoder({ + contentTopic: testContentTopic, + routingInfo: testRoutingInfo + }); + + const lightPush: ILightPush = { + multicodec: "lightpush", + start: () => {}, + stop: () => {}, + send: sinon.stub().resolves({ successes: [], failures: [] }) as any + } as unknown as ILightPush; + + const filter: IFilter = { + multicodec: "filter", + start: sinon.stub().resolves(), + stop: sinon.stub().resolves(), + subscribe: sinon.stub().resolves(true), + unsubscribe: sinon.stub().resolves(true), + unsubscribeAll: sinon.stub() + } as unknown as IFilter; + + const store: IStore = { + multicodec: "store", + createCursor: sinon.stub() as any, + queryGenerator: sinon.stub() as any, + queryWithOrderedCallback: sinon.stub().resolves(), + queryWithPromiseCallback: sinon.stub().resolves() + } as unknown as IStore; + + const messaging = new Messaging({ lightPush, filter, store }); + + await messaging.send(encoder, { payload: utf8ToBytes("hello") }); + expect((lightPush.send as any).calledOnce).to.be.true; + }); +}); diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index f68708311b..a3e058ed07 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -1,6 +1,4 @@ import { - IDecodedMessage, - IDecoder, IEncoder, IFilter, ILightPush, @@ -8,6 +6,10 @@ import { IStore } from "@waku/interfaces"; +import { FilterAckManager } from "./fitler_ack.js"; +import { MessageStore } from "./message_store.js"; +import { StoreAckManager } from "./store_ack.js"; + interface IMessaging { send(encoder: IEncoder, message: IMessage): Promise; } @@ -19,115 +21,38 @@ type MessagingConstructorParams = { }; export class Messaging implements IMessaging { - public constructor(params: MessagingConstructorParams) {} + private readonly lightPush: ILightPush; + private readonly messageStore: MessageStore; + private readonly filterAckManager: FilterAckManager; + private readonly storeAckManager: StoreAckManager; - public send(encoder: IEncoder, message: IMessage): Promise { - return Promise.resolve(); + public constructor(params: MessagingConstructorParams) { + this.lightPush = params.lightPush; + this.messageStore = new MessageStore(); + this.filterAckManager = new FilterAckManager( + this.messageStore, + params.filter + ); + this.storeAckManager = new StoreAckManager(this.messageStore, params.store); } -} - -class MessageStore { - // const hash: { encoder, message, filterAck, storeAck } - // filterAck(hash) - // storeAck(hash) - // markSent(hash) - // queue(encoder, message) - // getMessagesToSend() - // -> not sent yet (first) - // -> sent more than 2s ago but not acked yet (store or filter) -} - -type ICodec = IEncoder & IDecoder; - -interface IAckManager { - start(): void; - stop(): void; - subscribe(codec: ICodec): void; -} - -class FilterAckManager implements IAckManager { - private codecs: Set = new Set(); - - public constructor( - private messageStore: MessageStore, - private filter: IFilter - ) {} public start(): void { - // noop + this.filterAckManager.start(); + this.storeAckManager.start(); } public async stop(): Promise { - const promises = Array.from(this.codecs.entries()).map((codec) => { - return this.filter.unsubscribe(codec); - }); - - await Promise.all(promises); - this.codecs.clear(); - } - - public async subscribe(codec: ICodec): Promise { - return this.filter.subscribe(codec, this.onMessage.bind(this)); + await this.filterAckManager.stop(); + this.storeAckManager.stop(); } - private async onMessage(message: IDecodedMessage): Promise { - if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); - } - - this.messageStore.markFilterAck(message.hashStr); - } -} - -class StoreAckManager implements IAckManager { - private interval: ReturnType | null = null; - - private codecs: Set = new Set(); - - public constructor( - private messageStore: MessageStore, - private store: IStore - ) {} - - public start(): void { - if (this.interval) { - return; - } - - this.interval = setInterval(() => { - void this.query(); - }, 1000); - } - - public stop(): void { - if (!this.interval) { - return; - } - - clearInterval(this.interval); - this.interval = null; - } - - public subscribe(codec: ICodec): void { - this.codecs.add(codec); - } - - private async query(): Promise { - for (const codec of this.codecs) { - await this.store.queryWithOrderedCallback( - [codec], - (message) => { - if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); - } - - this.messageStore.markStoreAck(message.hashStr); - }, - { - timeStart: new Date(Date.now() - 60 * 60 * 1000), - timeEnd: new Date() - } - ); - } + public send(encoder: IEncoder, message: IMessage): Promise { + return (async () => { + const hash = await this.messageStore.queue(encoder, message); + await this.lightPush.send(encoder, message); + if (hash) { + this.messageStore.markSent(hash); + } + })(); } } diff --git a/packages/sdk/src/messaging/store_ack.ts b/packages/sdk/src/messaging/store_ack.ts new file mode 100644 index 0000000000..94511d1c92 --- /dev/null +++ b/packages/sdk/src/messaging/store_ack.ts @@ -0,0 +1,58 @@ +import { IStore } from "@waku/interfaces"; + +import { MessageStore } from "./message_store.js"; +import { IAckManager, ICodec } from "./utils.js"; + +export class StoreAckManager implements IAckManager { + private interval: ReturnType | null = null; + + private codecs: Set = new Set(); + + public constructor( + private messageStore: MessageStore, + private store: IStore + ) {} + + public start(): void { + if (this.interval) { + return; + } + + this.interval = setInterval(() => { + void this.query(); + }, 1000); + } + + public stop(): void { + if (!this.interval) { + return; + } + + clearInterval(this.interval); + this.interval = null; + } + + public async subscribe(codec: ICodec): Promise { + this.codecs.add(codec); + return true; + } + + private async query(): Promise { + for (const codec of this.codecs) { + await this.store.queryWithOrderedCallback( + [codec], + (message) => { + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); + } + + this.messageStore.markStoreAck(message.hashStr); + }, + { + timeStart: new Date(Date.now() - 60 * 60 * 1000), + timeEnd: new Date() + } + ); + } + } +} diff --git a/packages/sdk/src/messaging/utils.ts b/packages/sdk/src/messaging/utils.ts new file mode 100644 index 0000000000..db4b5ba758 --- /dev/null +++ b/packages/sdk/src/messaging/utils.ts @@ -0,0 +1,10 @@ +import { IDecodedMessage, IDecoder, IEncoder } from "@waku/interfaces"; + +// TODO: create a local entity for that that will literally extend existing encoder and decoder from package/core +export type ICodec = IEncoder & IDecoder; + +export interface IAckManager { + start(): void; + stop(): void; + subscribe(codec: ICodec): Promise; +} diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 6fc689d1b1..31098782b5 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -232,7 +232,7 @@ export class WakuNode implements IWaku { this.peerManager.start(); this.healthIndicator.start(); this.lightPush?.start(); - this.sender?.start(); + this.messaging?.start(); this._nodeStateLock = false; this._nodeStarted = true; @@ -243,7 +243,7 @@ export class WakuNode implements IWaku { this._nodeStateLock = true; - this.sender?.stop(); + await this.messaging?.stop(); this.lightPush?.stop(); await this.filter?.stop(); this.healthIndicator.stop(); From 4fe8bfdd886a9ae66113d0b2cb540dead2e27aa3 Mon Sep 17 00:00:00 2001 From: Sasha Date: Thu, 25 Sep 2025 01:32:12 +0200 Subject: [PATCH 04/16] implement main ack manager, improve message store, implement Sender entity --- packages/sdk/src/messaging/ack_manager.ts | 138 ++++++++++++++++++++ packages/sdk/src/messaging/fitler_ack.ts | 44 ------- packages/sdk/src/messaging/message_store.ts | 87 ++++++++---- packages/sdk/src/messaging/messaging.ts | 40 +++--- packages/sdk/src/messaging/sender.ts | 26 ++++ packages/sdk/src/messaging/store_ack.ts | 58 -------- 6 files changed, 242 insertions(+), 151 deletions(-) create mode 100644 packages/sdk/src/messaging/ack_manager.ts delete mode 100644 packages/sdk/src/messaging/fitler_ack.ts create mode 100644 packages/sdk/src/messaging/sender.ts delete mode 100644 packages/sdk/src/messaging/store_ack.ts diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts new file mode 100644 index 0000000000..5f41d1a771 --- /dev/null +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -0,0 +1,138 @@ +import { IDecodedMessage, IFilter, IStore } from "@waku/interfaces"; + +import { MessageStore } from "./message_store.js"; +import { IAckManager, ICodec } from "./utils.js"; + +type AckManagerConstructorParams = { + messageStore: MessageStore; + filter: IFilter; + store: IStore; +}; + +export class AckManager implements IAckManager { + private readonly messageStore: MessageStore; + private readonly filterAckManager: FilterAckManager; + private readonly storeAckManager: StoreAckManager; + + public constructor(params: AckManagerConstructorParams) { + this.messageStore = params.messageStore; + + this.filterAckManager = new FilterAckManager( + this.messageStore, + params.filter + ); + + this.storeAckManager = new StoreAckManager(this.messageStore, params.store); + } + + public start(): void { + this.filterAckManager.start(); + this.storeAckManager.start(); + } + + public async stop(): Promise { + await this.filterAckManager.stop(); + this.storeAckManager.stop(); + } + + public async subscribe(codec: ICodec): Promise { + return ( + (await this.filterAckManager.subscribe(codec)) || + (await this.storeAckManager.subscribe(codec)) + ); + } +} + +class FilterAckManager implements IAckManager { + private codecs: Set = new Set(); + + public constructor( + private messageStore: MessageStore, + private filter: IFilter + ) {} + + public start(): void { + return; + } + + public async stop(): Promise { + const promises = Array.from(this.codecs.entries()).map((codec) => + this.filter.unsubscribe(codec) + ); + await Promise.all(promises); + this.codecs.clear(); + } + + public async subscribe(codec: ICodec): Promise { + const success = await this.filter.subscribe( + codec, + this.onMessage.bind(this) + ); + if (success) { + this.codecs.add(codec); + } + return success; + } + + private async onMessage(message: IDecodedMessage): Promise { + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); + } + + this.messageStore.markFilterAck(message.hashStr); + } +} + +class StoreAckManager implements IAckManager { + private interval: ReturnType | null = null; + + private codecs: Set = new Set(); + + public constructor( + private messageStore: MessageStore, + private store: IStore + ) {} + + public start(): void { + if (this.interval) { + return; + } + + this.interval = setInterval(() => { + void this.query(); + }, 1000); + } + + public stop(): void { + if (!this.interval) { + return; + } + + clearInterval(this.interval); + this.interval = null; + } + + public async subscribe(codec: ICodec): Promise { + this.codecs.add(codec); + return true; + } + + private async query(): Promise { + for (const codec of this.codecs) { + await this.store.queryWithOrderedCallback( + [codec], + (message) => { + if (!this.messageStore.has(message.hashStr)) { + this.messageStore.add(message); + } + + this.messageStore.markStoreAck(message.hashStr); + }, + { + timeStart: new Date(Date.now() - 60 * 60 * 1000), + timeEnd: new Date() + } + ); + } + } +} diff --git a/packages/sdk/src/messaging/fitler_ack.ts b/packages/sdk/src/messaging/fitler_ack.ts deleted file mode 100644 index b5ed7562c3..0000000000 --- a/packages/sdk/src/messaging/fitler_ack.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { IDecodedMessage, IFilter } from "@waku/interfaces"; - -import { MessageStore } from "./message_store.js"; -import { IAckManager, ICodec } from "./utils.js"; - -export class FilterAckManager implements IAckManager { - private codecs: Set = new Set(); - - public constructor( - private messageStore: MessageStore, - private filter: IFilter - ) {} - - public start(): void { - return; - } - - public async stop(): Promise { - const promises = Array.from(this.codecs.entries()).map((codec) => - this.filter.unsubscribe(codec) - ); - await Promise.all(promises); - this.codecs.clear(); - } - - public async subscribe(codec: ICodec): Promise { - const success = await this.filter.subscribe( - codec, - this.onMessage.bind(this) - ); - if (success) { - this.codecs.add(codec); - } - return success; - } - - private async onMessage(message: IDecodedMessage): Promise { - if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); - } - - this.messageStore.markFilterAck(message.hashStr); - } -} diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 910f5cee60..caed532bf7 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,4 +1,4 @@ -import { messageHashStr } from "@waku/core"; +import { message, messageHashStr } from "@waku/core"; import { IDecodedMessage, IEncoder, IMessage } from "@waku/interfaces"; type QueuedMessage = { @@ -14,8 +14,12 @@ type MessageStoreOptions = { resendIntervalMs?: number; }; +type RequestId = string; + export class MessageStore { private readonly messages: Map = new Map(); + private readonly pendingRequests: Map = new Map(); + private readonly resendIntervalMs: number; public constructor(options: MessageStoreOptions = {}) { @@ -40,62 +44,91 @@ export class MessageStore { const entry = this.messages.get(hashStr); if (!entry) return; entry.filterAck = true; + // TODO: implement events } public markStoreAck(hashStr: string): void { const entry = this.messages.get(hashStr); if (!entry) return; entry.storeAck = true; + // TODO: implement events } - public markSent(hashStr: string): void { - const entry = this.messages.get(hashStr); - if (!entry) return; - entry.lastSentAt = Date.now(); + public async markSent(requestId: RequestId): Promise { + const entry = this.pendingRequests.get(requestId); + + if (!entry || !entry.encoder || !entry.message) { + return; + } + + try { + entry.lastSentAt = Date.now(); + this.pendingRequests.delete(requestId); + + const proto = await entry.encoder.toProtoObj(entry.message); + + if (!proto) { + return; + } + + const hashStr = messageHashStr(entry.encoder.pubsubTopic, proto); + + this.messages.set(hashStr, entry); + } catch (error) { + // TODO: better recovery + this.pendingRequests.set(requestId, entry); + } } public async queue( encoder: IEncoder, message: IMessage - ): Promise { - const proto = await encoder.toProtoObj(message); - if (!proto) return undefined; - const hashStr = messageHashStr(encoder.pubsubTopic, proto); - const existing = this.messages.get(hashStr); - if (!existing) { - this.messages.set(hashStr, { - encoder, - message, - filterAck: false, - storeAck: false, - createdAt: Date.now() - }); - } - return hashStr; + ): Promise { + const requestId = crypto.randomUUID(); + + this.pendingRequests.set(requestId, { + encoder, + message, + filterAck: false, + storeAck: false, + createdAt: Date.now() + }); + + return requestId; } public getMessagesToSend(): Array<{ - hashStr: string; + requestId: string; encoder: IEncoder; message: IMessage; }> { const now = Date.now(); + const res: Array<{ - hashStr: string; + requestId: string; encoder: IEncoder; message: IMessage; }> = []; - for (const [hashStr, entry] of this.messages.entries()) { - if (!entry.encoder || !entry.message) continue; - const isAcknowledged = entry.filterAck || entry.storeAck; - if (isAcknowledged) continue; + + for (const [requestId, entry] of this.pendingRequests.entries()) { + if (!entry.encoder || !entry.message) { + continue; + } + + const isAcknowledged = entry.filterAck || entry.storeAck; // TODO: make sure it works with message and pending requests and returns messages to re-sent that are not ack yet + + if (isAcknowledged) { + continue; + } + if ( !entry.lastSentAt || now - entry.lastSentAt >= this.resendIntervalMs ) { - res.push({ hashStr, encoder: entry.encoder, message: entry.message }); + res.push({ requestId, encoder: entry.encoder, message: entry.message }); } } + return res; } } diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index a3e058ed07..94e2d72006 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -6,9 +6,9 @@ import { IStore } from "@waku/interfaces"; -import { FilterAckManager } from "./fitler_ack.js"; +import { AckManager } from "./ack_manager.js"; import { MessageStore } from "./message_store.js"; -import { StoreAckManager } from "./store_ack.js"; +import { Sender } from "./sender.js"; interface IMessaging { send(encoder: IEncoder, message: IMessage): Promise; @@ -21,38 +21,34 @@ type MessagingConstructorParams = { }; export class Messaging implements IMessaging { - private readonly lightPush: ILightPush; private readonly messageStore: MessageStore; - private readonly filterAckManager: FilterAckManager; - private readonly storeAckManager: StoreAckManager; + private readonly ackManager: AckManager; + private readonly sender: Sender; public constructor(params: MessagingConstructorParams) { - this.lightPush = params.lightPush; this.messageStore = new MessageStore(); - this.filterAckManager = new FilterAckManager( - this.messageStore, - params.filter - ); - this.storeAckManager = new StoreAckManager(this.messageStore, params.store); + + this.ackManager = new AckManager({ + messageStore: this.messageStore, + filter: params.filter, + store: params.store + }); + + this.sender = new Sender({ + messageStore: this.messageStore, + lightPush: params.lightPush + }); } public start(): void { - this.filterAckManager.start(); - this.storeAckManager.start(); + this.ackManager.start(); } public async stop(): Promise { - await this.filterAckManager.stop(); - this.storeAckManager.stop(); + await this.ackManager.stop(); } public send(encoder: IEncoder, message: IMessage): Promise { - return (async () => { - const hash = await this.messageStore.queue(encoder, message); - await this.lightPush.send(encoder, message); - if (hash) { - this.messageStore.markSent(hash); - } - })(); + return this.sender.send(encoder, message); } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts new file mode 100644 index 0000000000..cb21af4192 --- /dev/null +++ b/packages/sdk/src/messaging/sender.ts @@ -0,0 +1,26 @@ +import { IEncoder, ILightPush, IMessage } from "@waku/interfaces"; + +import type { MessageStore } from "./message_store.js"; + +type SenderConstructorParams = { + messageStore: MessageStore; + lightPush: ILightPush; +}; + +export class Sender { + private readonly messageStore: MessageStore; + private readonly lightPush: ILightPush; + + public constructor(params: SenderConstructorParams) { + this.messageStore = params.messageStore; + this.lightPush = params.lightPush; + } + + public async send(encoder: IEncoder, message: IMessage): Promise { + const requestId = await this.messageStore.queue(encoder, message); + await this.lightPush.send(encoder, message); + if (requestId) { + await this.messageStore.markSent(requestId); + } + } +} diff --git a/packages/sdk/src/messaging/store_ack.ts b/packages/sdk/src/messaging/store_ack.ts deleted file mode 100644 index 94511d1c92..0000000000 --- a/packages/sdk/src/messaging/store_ack.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IStore } from "@waku/interfaces"; - -import { MessageStore } from "./message_store.js"; -import { IAckManager, ICodec } from "./utils.js"; - -export class StoreAckManager implements IAckManager { - private interval: ReturnType | null = null; - - private codecs: Set = new Set(); - - public constructor( - private messageStore: MessageStore, - private store: IStore - ) {} - - public start(): void { - if (this.interval) { - return; - } - - this.interval = setInterval(() => { - void this.query(); - }, 1000); - } - - public stop(): void { - if (!this.interval) { - return; - } - - clearInterval(this.interval); - this.interval = null; - } - - public async subscribe(codec: ICodec): Promise { - this.codecs.add(codec); - return true; - } - - private async query(): Promise { - for (const codec of this.codecs) { - await this.store.queryWithOrderedCallback( - [codec], - (message) => { - if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); - } - - this.messageStore.markStoreAck(message.hashStr); - }, - { - timeStart: new Date(Date.now() - 60 * 60 * 1000), - timeEnd: new Date() - } - ); - } - } -} From 37ee49011461857203575386866794656b86bc37 Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 26 Sep 2025 01:34:37 +0200 Subject: [PATCH 05/16] implement background send, requestID --- packages/sdk/src/messaging/message_store.ts | 5 +-- packages/sdk/src/messaging/sender.ts | 36 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index caed532bf7..a315fca268 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -80,10 +80,7 @@ export class MessageStore { } } - public async queue( - encoder: IEncoder, - message: IMessage - ): Promise { + public async queue(encoder: IEncoder, message: IMessage): Promise { const requestId = crypto.randomUUID(); this.pendingRequests.set(requestId, { diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index cb21af4192..bc1fc4ece3 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -11,16 +11,46 @@ export class Sender { private readonly messageStore: MessageStore; private readonly lightPush: ILightPush; + private sendInterval: ReturnType | null = null; + public constructor(params: SenderConstructorParams) { this.messageStore = params.messageStore; this.lightPush = params.lightPush; } - public async send(encoder: IEncoder, message: IMessage): Promise { + public start(): void { + this.sendInterval = setInterval(() => void this.backgroundSend(), 1000); + } + + public stop(): void { + if (this.sendInterval) { + clearInterval(this.sendInterval); + this.sendInterval = null; + } + } + + public async send(encoder: IEncoder, message: IMessage): Promise { const requestId = await this.messageStore.queue(encoder, message); - await this.lightPush.send(encoder, message); - if (requestId) { + const response = await this.lightPush.send(encoder, message); + + if (response.successes.length > 0) { await this.messageStore.markSent(requestId); } + + return requestId; + } + + private async backgroundSend(): Promise { + const pendingRequests = this.messageStore.getMessagesToSend(); + + // todo: implement chunking, error handling, retry, etc. + // todo: implement backoff and batching potentially + for (const { requestId, encoder, message } of pendingRequests) { + const response = await this.lightPush.send(encoder, message); + + if (response.successes.length > 0) { + await this.messageStore.markSent(requestId); + } + } } } From 0c852e42012b86304c13b7a119fc404ed81f32f4 Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 29 Sep 2025 22:40:53 +0200 Subject: [PATCH 06/16] add utils, fix typings --- packages/sdk/src/messaging/message_store.ts | 2 +- packages/sdk/src/messaging/messaging.ts | 5 +++-- packages/sdk/src/messaging/sender.ts | 3 ++- packages/sdk/src/messaging/utils.ts | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index a315fca268..a1babfde75 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,4 +1,4 @@ -import { message, messageHashStr } from "@waku/core"; +import { messageHashStr } from "@waku/core"; import { IDecodedMessage, IEncoder, IMessage } from "@waku/interfaces"; type QueuedMessage = { diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index 94e2d72006..e843bd122a 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -9,9 +9,10 @@ import { import { AckManager } from "./ack_manager.js"; import { MessageStore } from "./message_store.js"; import { Sender } from "./sender.js"; +import type { RequestId } from "./utils.js"; interface IMessaging { - send(encoder: IEncoder, message: IMessage): Promise; + send(encoder: IEncoder, message: IMessage): Promise; } type MessagingConstructorParams = { @@ -48,7 +49,7 @@ export class Messaging implements IMessaging { await this.ackManager.stop(); } - public send(encoder: IEncoder, message: IMessage): Promise { + public send(encoder: IEncoder, message: IMessage): Promise { return this.sender.send(encoder, message); } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index bc1fc4ece3..a7a49aa7d6 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -1,6 +1,7 @@ import { IEncoder, ILightPush, IMessage } from "@waku/interfaces"; import type { MessageStore } from "./message_store.js"; +import type { RequestId } from "./utils.js"; type SenderConstructorParams = { messageStore: MessageStore; @@ -29,7 +30,7 @@ export class Sender { } } - public async send(encoder: IEncoder, message: IMessage): Promise { + public async send(encoder: IEncoder, message: IMessage): Promise { const requestId = await this.messageStore.queue(encoder, message); const response = await this.lightPush.send(encoder, message); diff --git a/packages/sdk/src/messaging/utils.ts b/packages/sdk/src/messaging/utils.ts index db4b5ba758..3f270e5bbe 100644 --- a/packages/sdk/src/messaging/utils.ts +++ b/packages/sdk/src/messaging/utils.ts @@ -3,6 +3,8 @@ import { IDecodedMessage, IDecoder, IEncoder } from "@waku/interfaces"; // TODO: create a local entity for that that will literally extend existing encoder and decoder from package/core export type ICodec = IEncoder & IDecoder; +export type RequestId = string; + export interface IAckManager { start(): void; stop(): void; From 9e7719e5191ba78894e2f033d5f66116f09d4786 Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 29 Sep 2025 23:01:43 +0200 Subject: [PATCH 07/16] add ICodec --- packages/core/src/index.ts | 2 + packages/core/src/lib/message/codec.ts | 76 ++++++++++++++++++++++ packages/core/src/lib/message/constants.ts | 2 + packages/core/src/lib/message/index.ts | 2 + packages/core/src/lib/message/version_0.ts | 4 +- packages/interfaces/src/message.ts | 2 + packages/interfaces/src/waku.ts | 38 ++++++++++- packages/rln/src/message.ts | 2 +- packages/sdk/src/messaging/ack_manager.ts | 14 ++-- packages/sdk/src/messaging/index.ts | 1 + packages/sdk/src/messaging/utils.ts | 7 +- packages/sdk/src/waku/waku.ts | 31 ++++++++- packages/utils/src/common/mock_node.ts | 5 ++ 13 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/lib/message/codec.ts create mode 100644 packages/core/src/lib/message/constants.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8021ac0a91..5688e5e937 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,9 +1,11 @@ export { createEncoder, createDecoder } from "./lib/message/version_0.js"; +export { createCodec } from "./lib/message/index.js"; export type { Encoder, Decoder, DecodedMessage } from "./lib/message/version_0.js"; +export type { Codec } from "./lib/message/index.js"; export * as message from "./lib/message/index.js"; export * as waku_filter from "./lib/filter/index.js"; diff --git a/packages/core/src/lib/message/codec.ts b/packages/core/src/lib/message/codec.ts new file mode 100644 index 0000000000..c0212331bf --- /dev/null +++ b/packages/core/src/lib/message/codec.ts @@ -0,0 +1,76 @@ +import type { + ICodec, + IDecodedMessage, + IDecoder, + IEncoder, + IMessage, + IMetaSetter, + IProtoMessage, + IRoutingInfo, + PubsubTopic +} from "@waku/interfaces"; + +import { Decoder, Encoder } from "./version_0.js"; + +export class Codec implements ICodec { + private encoder: IEncoder; + private decoder: IDecoder; + + public constructor( + public contentTopic: string, + public ephemeral: boolean = false, + public routingInfo: IRoutingInfo, + public metaSetter?: IMetaSetter + ) { + this.encoder = new Encoder( + contentTopic, + ephemeral, + routingInfo, + metaSetter + ); + this.decoder = new Decoder(contentTopic, routingInfo); + } + + public get pubsubTopic(): PubsubTopic { + return this.routingInfo.pubsubTopic; + } + + public async toWire(message: IMessage): Promise { + return this.encoder.toWire(message); + } + + public async toProtoObj( + message: IMessage + ): Promise { + return this.encoder.toProtoObj(message); + } + + public fromWireToProtoObj( + bytes: Uint8Array + ): Promise { + return this.decoder.fromWireToProtoObj(bytes); + } + + public async fromProtoObj( + pubsubTopic: string, + proto: IProtoMessage + ): Promise { + return this.decoder.fromProtoObj(pubsubTopic, proto); + } +} + +type CodecParams = { + contentTopic: string; + ephemeral: boolean; + routingInfo: IRoutingInfo; + metaSetter?: IMetaSetter; +}; + +export function createCodec(params: CodecParams): Codec { + return new Codec( + params.contentTopic, + params.ephemeral, + params.routingInfo, + params.metaSetter + ); +} diff --git a/packages/core/src/lib/message/constants.ts b/packages/core/src/lib/message/constants.ts new file mode 100644 index 0000000000..0d54071228 --- /dev/null +++ b/packages/core/src/lib/message/constants.ts @@ -0,0 +1,2 @@ +export const OneMillion = BigInt(1_000_000); +export const Version = 0; diff --git a/packages/core/src/lib/message/index.ts b/packages/core/src/lib/message/index.ts index e4736e54e1..8add5fba21 100644 --- a/packages/core/src/lib/message/index.ts +++ b/packages/core/src/lib/message/index.ts @@ -1 +1,3 @@ export * as version_0 from "./version_0.js"; +export { Codec, createCodec } from "./codec.js"; +export { OneMillion, Version } from "./constants.js"; diff --git a/packages/core/src/lib/message/version_0.ts b/packages/core/src/lib/message/version_0.ts index c127120e74..d3d8a7d333 100644 --- a/packages/core/src/lib/message/version_0.ts +++ b/packages/core/src/lib/message/version_0.ts @@ -16,10 +16,10 @@ import { bytesToHex } from "@waku/utils/bytes"; import { messageHash } from "../message_hash/index.js"; +import { OneMillion, Version } from "./constants.js"; + const log = new Logger("message:version-0"); -const OneMillion = BigInt(1_000_000); -export const Version = 0; export { proto }; export class DecodedMessage implements IDecodedMessage { diff --git a/packages/interfaces/src/message.ts b/packages/interfaces/src/message.ts index caecb73aec..e97a5d0304 100644 --- a/packages/interfaces/src/message.ts +++ b/packages/interfaces/src/message.ts @@ -111,3 +111,5 @@ export interface IDecoder { proto: IProtoMessage ) => Promise; } + +export type ICodec = IEncoder & IDecoder; diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index 1fbec20cce..a0c83ac746 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -10,7 +10,7 @@ import type { IFilter } from "./filter.js"; import type { HealthStatus } from "./health_status.js"; import type { Libp2p } from "./libp2p.js"; import type { ILightPush } from "./light_push.js"; -import { IDecodedMessage, IDecoder, IEncoder } from "./message.js"; +import { ICodec, IDecodedMessage, IDecoder, IEncoder } from "./message.js"; import type { Protocols } from "./protocols.js"; import type { IRelay } from "./relay.js"; import type { ShardId } from "./sharding.js"; @@ -25,6 +25,8 @@ export type CreateEncoderParams = CreateDecoderParams & { ephemeral?: boolean; }; +export type CreateCodecParams = CreateDecoderParams & CreateEncoderParams; + export enum WakuEvent { Connection = "waku:connection", Health = "waku:health" @@ -206,6 +208,8 @@ export interface IWaku { waitForPeers(protocols?: Protocols[], timeoutMs?: number): Promise; /** + * @deprecated Use {@link createCodec} instead + * * Creates a decoder for Waku messages on a specific content topic. * * A decoder is used to decode messages from the Waku network format. @@ -235,6 +239,8 @@ export interface IWaku { createDecoder(params: CreateDecoderParams): IDecoder; /** + * @deprecated Use {@link createCodec} instead + * * Creates an encoder for Waku messages on a specific content topic. * * An encoder is used to encode messages into the Waku network format. @@ -264,6 +270,36 @@ export interface IWaku { */ createEncoder(params: CreateEncoderParams): IEncoder; + /** + * Creates a codec for Waku messages on a specific content topic. + * + * A codec is used to encode and decode messages from the Waku network format. + * The codec automatically handles shard configuration based on the Waku node's network settings. + * + * @param {CreateCodecParams} params - Configuration for the codec including content topic and optionally shard information and ephemeral flag + * @returns {ICodec} A codec instance configured for the specified content topic + * @throws {Error} If the shard configuration is incompatible with the node's network settings + * + * @example + * ```typescript + * // Create a codec with default network shard settings + * const codec = waku.createCodec({ + * contentTopic: "/my-app/1/chat/proto" + * }); + * + * // Create a codec with custom shard settings + * const customCodec = waku.createCodec({ + * contentTopic: "/my-app/1/chat/proto", + * ephemeral: true, + * shardInfo: { + * clusterId: 1, + * shard: 5 + * } + * }); + * ``` + */ + createCodec(params: CreateCodecParams): ICodec; + /** * @returns {boolean} `true` if the node was started and `false` otherwise */ diff --git a/packages/rln/src/message.ts b/packages/rln/src/message.ts index 1cca8a4ed2..d4fa44e84d 100644 --- a/packages/rln/src/message.ts +++ b/packages/rln/src/message.ts @@ -17,7 +17,7 @@ export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array { export class RlnMessage implements IRlnMessage { public pubsubTopic = ""; - public version = message.version_0.Version; + public version = message.Version; public constructor( private rlnInstance: RLNInstance, diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts index 5f41d1a771..c7874c31b1 100644 --- a/packages/sdk/src/messaging/ack_manager.ts +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -1,7 +1,7 @@ -import { IDecodedMessage, IFilter, IStore } from "@waku/interfaces"; +import { ICodec, IDecodedMessage, IFilter, IStore } from "@waku/interfaces"; import { MessageStore } from "./message_store.js"; -import { IAckManager, ICodec } from "./utils.js"; +import { IAckManager } from "./utils.js"; type AckManagerConstructorParams = { messageStore: MessageStore; @@ -35,7 +35,7 @@ export class AckManager implements IAckManager { this.storeAckManager.stop(); } - public async subscribe(codec: ICodec): Promise { + public async subscribe(codec: ICodec): Promise { return ( (await this.filterAckManager.subscribe(codec)) || (await this.storeAckManager.subscribe(codec)) @@ -44,7 +44,7 @@ export class AckManager implements IAckManager { } class FilterAckManager implements IAckManager { - private codecs: Set = new Set(); + private codecs: Set> = new Set(); public constructor( private messageStore: MessageStore, @@ -63,7 +63,7 @@ class FilterAckManager implements IAckManager { this.codecs.clear(); } - public async subscribe(codec: ICodec): Promise { + public async subscribe(codec: ICodec): Promise { const success = await this.filter.subscribe( codec, this.onMessage.bind(this) @@ -86,7 +86,7 @@ class FilterAckManager implements IAckManager { class StoreAckManager implements IAckManager { private interval: ReturnType | null = null; - private codecs: Set = new Set(); + private codecs: Set> = new Set(); public constructor( private messageStore: MessageStore, @@ -112,7 +112,7 @@ class StoreAckManager implements IAckManager { this.interval = null; } - public async subscribe(codec: ICodec): Promise { + public async subscribe(codec: ICodec): Promise { this.codecs.add(codec); return true; } diff --git a/packages/sdk/src/messaging/index.ts b/packages/sdk/src/messaging/index.ts index 0035e4eb2f..04928b7037 100644 --- a/packages/sdk/src/messaging/index.ts +++ b/packages/sdk/src/messaging/index.ts @@ -1 +1,2 @@ export { Messaging } from "./messaging.js"; +export type { RequestId } from "./utils.js"; diff --git a/packages/sdk/src/messaging/utils.ts b/packages/sdk/src/messaging/utils.ts index 3f270e5bbe..f76cb7920f 100644 --- a/packages/sdk/src/messaging/utils.ts +++ b/packages/sdk/src/messaging/utils.ts @@ -1,12 +1,9 @@ -import { IDecodedMessage, IDecoder, IEncoder } from "@waku/interfaces"; - -// TODO: create a local entity for that that will literally extend existing encoder and decoder from package/core -export type ICodec = IEncoder & IDecoder; +import { ICodec, IDecodedMessage } from "@waku/interfaces"; export type RequestId = string; export interface IAckManager { start(): void; stop(): void; - subscribe(codec: ICodec): Promise; + subscribe(codec: ICodec): Promise; } diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 4324e48a4c..d558a13369 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -5,11 +5,18 @@ import { TypedEventEmitter } from "@libp2p/interface"; import type { MultiaddrInput } from "@multiformats/multiaddr"; -import { ConnectionManager, createDecoder, createEncoder } from "@waku/core"; +import { + ConnectionManager, + createCodec, + createDecoder, + createEncoder +} from "@waku/core"; import type { + CreateCodecParams, CreateDecoderParams, CreateEncoderParams, CreateNodeOptions, + ICodec, IDecodedMessage, IDecoder, IEncoder, @@ -35,6 +42,7 @@ import { Filter } from "../filter/index.js"; import { HealthIndicator } from "../health_indicator/index.js"; import { LightPush } from "../light_push/index.js"; import { Messaging } from "../messaging/index.js"; +import type { RequestId } from "../messaging/index.js"; import { PeerManager } from "../peer_manager/index.js"; import { Store } from "../store/index.js"; @@ -295,8 +303,25 @@ export class WakuNode implements IWaku { }); } - public send(encoder: IEncoder, message: IMessage): Promise { - return this.messaging?.send(encoder, message) ?? Promise.resolve(); + public send(encoder: IEncoder, message: IMessage): Promise { + if (!this.messaging) { + throw new Error("Messaging not initialized"); + } + + return this.messaging.send(encoder, message); + } + + public createCodec(params: CreateCodecParams): ICodec { + const routingInfo = this.createRoutingInfo( + params.contentTopic, + params.shardId + ); + + return createCodec({ + contentTopic: params.contentTopic, + ephemeral: params.ephemeral ?? false, + routingInfo: routingInfo + }); } private createRoutingInfo( diff --git a/packages/utils/src/common/mock_node.ts b/packages/utils/src/common/mock_node.ts index 40472fea17..531709e0dd 100644 --- a/packages/utils/src/common/mock_node.ts +++ b/packages/utils/src/common/mock_node.ts @@ -2,9 +2,11 @@ import { Peer, PeerId, Stream, TypedEventEmitter } from "@libp2p/interface"; import { MultiaddrInput } from "@multiformats/multiaddr"; import { Callback, + CreateCodecParams, CreateDecoderParams, CreateEncoderParams, HealthStatus, + ICodec, IDecodedMessage, IDecoder, IEncoder, @@ -154,6 +156,9 @@ export class MockWakuNode implements IWaku { public createEncoder(_params: CreateEncoderParams): IEncoder { throw new Error("Method not implemented."); } + public createCodec(_params: CreateCodecParams): ICodec { + throw new Error("Method not implemented."); + } public isStarted(): boolean { throw new Error("Method not implemented."); } From ad675d81749bbbe405995c0795147023066a3e0f Mon Sep 17 00:00:00 2001 From: Sasha Date: Tue, 30 Sep 2025 00:12:43 +0200 Subject: [PATCH 08/16] implement send on waku --- packages/interfaces/src/waku.ts | 17 ++++++++- packages/sdk/src/messaging/ack_manager.ts | 4 +- packages/sdk/src/messaging/message_store.ts | 42 +++++++++++---------- packages/sdk/src/messaging/messaging.ts | 14 +++++-- packages/sdk/src/messaging/sender.ts | 22 +++++++---- packages/sdk/src/waku/waku.ts | 7 +++- packages/utils/src/common/mock_node.ts | 6 +++ 7 files changed, 76 insertions(+), 36 deletions(-) diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index a0c83ac746..9608dc58e4 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -10,7 +10,13 @@ import type { IFilter } from "./filter.js"; import type { HealthStatus } from "./health_status.js"; import type { Libp2p } from "./libp2p.js"; import type { ILightPush } from "./light_push.js"; -import { ICodec, IDecodedMessage, IDecoder, IEncoder } from "./message.js"; +import { + ICodec, + IDecodedMessage, + IDecoder, + IEncoder, + IMessage +} from "./message.js"; import type { Protocols } from "./protocols.js"; import type { IRelay } from "./relay.js"; import type { ShardId } from "./sharding.js"; @@ -300,6 +306,15 @@ export interface IWaku { */ createCodec(params: CreateCodecParams): ICodec; + /** + * Sends a message to the Waku network. + * + * @param {ICodec} codec - The codec to use for encoding the message + * @param {IMessage} message - The message to send + * @returns {Promise} A promise that resolves to the request ID + */ + send(codec: ICodec, message: IMessage): Promise; + /** * @returns {boolean} `true` if the node was started and `false` otherwise */ diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts index c7874c31b1..d9a6211c17 100644 --- a/packages/sdk/src/messaging/ack_manager.ts +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -76,7 +76,7 @@ class FilterAckManager implements IAckManager { private async onMessage(message: IDecodedMessage): Promise { if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); + this.messageStore.add(message, { filterAck: true }); } this.messageStore.markFilterAck(message.hashStr); @@ -123,7 +123,7 @@ class StoreAckManager implements IAckManager { [codec], (message) => { if (!this.messageStore.has(message.hashStr)) { - this.messageStore.add(message); + this.messageStore.add(message, { storeAck: true }); } this.messageStore.markStoreAck(message.hashStr); diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index a1babfde75..540ef38efd 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,8 +1,8 @@ import { messageHashStr } from "@waku/core"; -import { IDecodedMessage, IEncoder, IMessage } from "@waku/interfaces"; +import { ICodec, IDecodedMessage, IMessage } from "@waku/interfaces"; type QueuedMessage = { - encoder?: IEncoder; + codec?: ICodec; message?: IMessage; filterAck: boolean; storeAck: boolean; @@ -10,6 +10,11 @@ type QueuedMessage = { createdAt: number; }; +type AddMessageOptions = { + filterAck?: boolean; + storeAck?: boolean; +}; + type MessageStoreOptions = { resendIntervalMs?: number; }; @@ -30,11 +35,11 @@ export class MessageStore { return this.messages.has(hashStr); } - public add(message: IDecodedMessage): void { + public add(message: IDecodedMessage, options: AddMessageOptions = {}): void { if (!this.messages.has(message.hashStr)) { this.messages.set(message.hashStr, { - filterAck: false, - storeAck: false, + filterAck: options.filterAck ?? false, + storeAck: options.storeAck ?? false, createdAt: Date.now() }); } @@ -57,7 +62,7 @@ export class MessageStore { public async markSent(requestId: RequestId): Promise { const entry = this.pendingRequests.get(requestId); - if (!entry || !entry.encoder || !entry.message) { + if (!entry || !entry.codec || !entry.message) { return; } @@ -65,13 +70,13 @@ export class MessageStore { entry.lastSentAt = Date.now(); this.pendingRequests.delete(requestId); - const proto = await entry.encoder.toProtoObj(entry.message); + const proto = await entry.codec.toProtoObj(entry.message); if (!proto) { return; } - const hashStr = messageHashStr(entry.encoder.pubsubTopic, proto); + const hashStr = messageHashStr(entry.codec.pubsubTopic, proto); this.messages.set(hashStr, entry); } catch (error) { @@ -80,11 +85,14 @@ export class MessageStore { } } - public async queue(encoder: IEncoder, message: IMessage): Promise { + public async queue( + codec: ICodec, + message: IMessage + ): Promise { const requestId = crypto.randomUUID(); this.pendingRequests.set(requestId, { - encoder, + codec, message, filterAck: false, storeAck: false, @@ -96,25 +104,21 @@ export class MessageStore { public getMessagesToSend(): Array<{ requestId: string; - encoder: IEncoder; + codec: ICodec; message: IMessage; }> { const now = Date.now(); const res: Array<{ requestId: string; - encoder: IEncoder; + codec: ICodec; message: IMessage; }> = []; for (const [requestId, entry] of this.pendingRequests.entries()) { - if (!entry.encoder || !entry.message) { - continue; - } - - const isAcknowledged = entry.filterAck || entry.storeAck; // TODO: make sure it works with message and pending requests and returns messages to re-sent that are not ack yet + const isAcknowledged = entry.filterAck || entry.storeAck; - if (isAcknowledged) { + if (!entry.codec || !entry.message || isAcknowledged) { continue; } @@ -122,7 +126,7 @@ export class MessageStore { !entry.lastSentAt || now - entry.lastSentAt >= this.resendIntervalMs ) { - res.push({ requestId, encoder: entry.encoder, message: entry.message }); + res.push({ requestId, codec: entry.codec, message: entry.message }); } } diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index e843bd122a..a1fbf91e58 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -1,5 +1,6 @@ import { - IEncoder, + ICodec, + IDecodedMessage, IFilter, ILightPush, IMessage, @@ -12,7 +13,7 @@ import { Sender } from "./sender.js"; import type { RequestId } from "./utils.js"; interface IMessaging { - send(encoder: IEncoder, message: IMessage): Promise; + send(codec: ICodec, message: IMessage): Promise; } type MessagingConstructorParams = { @@ -43,13 +44,18 @@ export class Messaging implements IMessaging { public start(): void { this.ackManager.start(); + this.sender.start(); } public async stop(): Promise { await this.ackManager.stop(); + this.sender.stop(); } - public send(encoder: IEncoder, message: IMessage): Promise { - return this.sender.send(encoder, message); + public send( + codec: ICodec, + message: IMessage + ): Promise { + return this.sender.send(codec, message); } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index a7a49aa7d6..46d15f486d 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -1,4 +1,9 @@ -import { IEncoder, ILightPush, IMessage } from "@waku/interfaces"; +import { + ICodec, + IDecodedMessage, + ILightPush, + IMessage +} from "@waku/interfaces"; import type { MessageStore } from "./message_store.js"; import type { RequestId } from "./utils.js"; @@ -30,9 +35,12 @@ export class Sender { } } - public async send(encoder: IEncoder, message: IMessage): Promise { - const requestId = await this.messageStore.queue(encoder, message); - const response = await this.lightPush.send(encoder, message); + public async send( + codec: ICodec, + message: IMessage + ): Promise { + const requestId = await this.messageStore.queue(codec, message); + const response = await this.lightPush.send(codec, message); if (response.successes.length > 0) { await this.messageStore.markSent(requestId); @@ -44,10 +52,8 @@ export class Sender { private async backgroundSend(): Promise { const pendingRequests = this.messageStore.getMessagesToSend(); - // todo: implement chunking, error handling, retry, etc. - // todo: implement backoff and batching potentially - for (const { requestId, encoder, message } of pendingRequests) { - const response = await this.lightPush.send(encoder, message); + for (const { requestId, codec, message } of pendingRequests) { + const response = await this.lightPush.send(codec, message); if (response.successes.length > 0) { await this.messageStore.markSent(requestId); diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index d558a13369..720e196883 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -303,12 +303,15 @@ export class WakuNode implements IWaku { }); } - public send(encoder: IEncoder, message: IMessage): Promise { + public send( + codec: ICodec, + message: IMessage + ): Promise { if (!this.messaging) { throw new Error("Messaging not initialized"); } - return this.messaging.send(encoder, message); + return this.messaging.send(codec, message); } public createCodec(params: CreateCodecParams): ICodec { diff --git a/packages/utils/src/common/mock_node.ts b/packages/utils/src/common/mock_node.ts index 531709e0dd..d33d83e88e 100644 --- a/packages/utils/src/common/mock_node.ts +++ b/packages/utils/src/common/mock_node.ts @@ -159,6 +159,12 @@ export class MockWakuNode implements IWaku { public createCodec(_params: CreateCodecParams): ICodec { throw new Error("Method not implemented."); } + public send( + _codec: ICodec, + _message: IMessage + ): Promise { + throw new Error("Method not implemented."); + } public isStarted(): boolean { throw new Error("Method not implemented."); } From 7c67abec1eadb384d681297714d80520f09bedc8 Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 1 Oct 2025 01:16:15 +0200 Subject: [PATCH 09/16] implement and fix queuing mechanics of message store --- package-lock.json | 3 +- packages/sdk/package.json | 3 +- packages/sdk/src/messaging/message_store.ts | 123 +++++++++++++------- packages/sdk/src/messaging/sender.ts | 27 +++-- 4 files changed, 103 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa49a49a1b..ceb583fcd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37624,7 +37624,8 @@ "@waku/sds": "^0.0.7", "@waku/utils": "0.0.27", "libp2p": "2.8.11", - "lodash.debounce": "^4.0.8" + "lodash.debounce": "^4.0.8", + "uuid": "^10.0.0" }, "devDependencies": { "@libp2p/interface": "2.10.4", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a72b4025d7..691d426aee 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -75,7 +75,8 @@ "@waku/sds": "^0.0.7", "@waku/utils": "0.0.27", "libp2p": "2.8.11", - "lodash.debounce": "^4.0.8" + "lodash.debounce": "^4.0.8", + "uuid": "^10.0.0" }, "devDependencies": { "@libp2p/interface": "2.10.4", diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 540ef38efd..7bde1b153b 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,9 +1,10 @@ -import { messageHashStr } from "@waku/core"; import { ICodec, IDecodedMessage, IMessage } from "@waku/interfaces"; +import { v4 as uuidv4 } from "uuid"; type QueuedMessage = { codec?: ICodec; - message?: IMessage; + messageRequest?: IMessage; + sentMessage?: IMessage; filterAck: boolean; storeAck: boolean; lastSentAt?: number; @@ -20,10 +21,13 @@ type MessageStoreOptions = { }; type RequestId = string; +type MessageHashStr = string; export class MessageStore { - private readonly messages: Map = new Map(); + private readonly messages: Map = new Map(); + private readonly pendingRequests: Map = new Map(); + private readonly pendingMessages: Map = new Map(); private readonly resendIntervalMs: number; @@ -46,54 +50,39 @@ export class MessageStore { } public markFilterAck(hashStr: string): void { - const entry = this.messages.get(hashStr); - if (!entry) return; - entry.filterAck = true; - // TODO: implement events + this.ackMessage(hashStr, { filterAck: true }); + this.replacePendingWithMessage(hashStr); } public markStoreAck(hashStr: string): void { - const entry = this.messages.get(hashStr); - if (!entry) return; - entry.storeAck = true; - // TODO: implement events + this.ackMessage(hashStr, { storeAck: true }); + this.replacePendingWithMessage(hashStr); } - public async markSent(requestId: RequestId): Promise { + public async markSent( + requestId: RequestId, + sentMessage: IDecodedMessage + ): Promise { const entry = this.pendingRequests.get(requestId); - if (!entry || !entry.codec || !entry.message) { + if (!entry || !entry.codec || !entry.messageRequest) { return; } - try { - entry.lastSentAt = Date.now(); - this.pendingRequests.delete(requestId); - - const proto = await entry.codec.toProtoObj(entry.message); - - if (!proto) { - return; - } - - const hashStr = messageHashStr(entry.codec.pubsubTopic, proto); - - this.messages.set(hashStr, entry); - } catch (error) { - // TODO: better recovery - this.pendingRequests.set(requestId, entry); - } + entry.lastSentAt = Number(sentMessage.timestamp); + entry.sentMessage = sentMessage; + this.pendingMessages.set(sentMessage.hashStr, requestId); } public async queue( codec: ICodec, message: IMessage ): Promise { - const requestId = crypto.randomUUID(); + const requestId = uuidv4(); - this.pendingRequests.set(requestId, { + this.pendingRequests.set(requestId.toString(), { codec, - message, + messageRequest: message, filterAck: false, storeAck: false, createdAt: Date.now() @@ -107,8 +96,6 @@ export class MessageStore { codec: ICodec; message: IMessage; }> { - const now = Date.now(); - const res: Array<{ requestId: string; codec: ICodec; @@ -118,18 +105,72 @@ export class MessageStore { for (const [requestId, entry] of this.pendingRequests.entries()) { const isAcknowledged = entry.filterAck || entry.storeAck; - if (!entry.codec || !entry.message || isAcknowledged) { + if (!entry.codec || !entry.messageRequest || isAcknowledged) { continue; } - if ( - !entry.lastSentAt || - now - entry.lastSentAt >= this.resendIntervalMs - ) { - res.push({ requestId, codec: entry.codec, message: entry.message }); + const notSent = !entry.lastSentAt; + const notAcknowledged = + entry.lastSentAt && + Date.now() - entry.lastSentAt >= this.resendIntervalMs && + !isAcknowledged; + + if (notSent || notAcknowledged) { + res.push({ + requestId, + codec: entry.codec, + message: entry.messageRequest + }); } } return res; } + + private ackMessage( + hashStr: MessageHashStr, + ackParams: AddMessageOptions = {} + ): void { + let entry = this.messages.get(hashStr); + + if (entry) { + entry.filterAck = true; + entry.storeAck = true; + return; + } + + const requestId = this.pendingMessages.get(hashStr); + + if (!requestId) { + return; + } + + entry = this.pendingRequests.get(requestId); + + if (!entry) { + return; + } + + entry.filterAck = ackParams.filterAck ?? entry.filterAck; + entry.storeAck = ackParams.storeAck ?? entry.storeAck; + } + + private replacePendingWithMessage(hashStr: MessageHashStr): void { + const requestId = this.pendingMessages.get(hashStr); + + if (!requestId) { + return; + } + + const entry = this.pendingRequests.get(requestId); + + if (!entry) { + return; + } + + this.pendingRequests.delete(requestId); + this.pendingMessages.delete(hashStr); + + this.messages.set(hashStr, entry); + } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index 46d15f486d..0206d22d80 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -15,13 +15,13 @@ type SenderConstructorParams = { export class Sender { private readonly messageStore: MessageStore; - private readonly lightPush: ILightPush; + // private readonly lightPush: ILightPush; private sendInterval: ReturnType | null = null; public constructor(params: SenderConstructorParams) { this.messageStore = params.messageStore; - this.lightPush = params.lightPush; + // this.lightPush = params.lightPush; } public start(): void { @@ -40,11 +40,14 @@ export class Sender { message: IMessage ): Promise { const requestId = await this.messageStore.queue(codec, message); - const response = await this.lightPush.send(codec, message); + // const response = await this.lightPush.send(codec, message); - if (response.successes.length > 0) { - await this.messageStore.markSent(requestId); - } + // if (response.successes.length > 0) { + await this.messageStore.markSent( + requestId, + (await codec.toProtoObj(message)) as IDecodedMessage + ); + // } return requestId; } @@ -53,11 +56,15 @@ export class Sender { const pendingRequests = this.messageStore.getMessagesToSend(); for (const { requestId, codec, message } of pendingRequests) { - const response = await this.lightPush.send(codec, message); + // const response = await this.lightPush.send(codec, message); - if (response.successes.length > 0) { - await this.messageStore.markSent(requestId); - } + // if (response.successes.length > 0) { + const sentMessage = await codec.toProtoObj(message); + await this.messageStore.markSent( + requestId, + sentMessage as IDecodedMessage + ); + // } } } } From 43851045cbb80e380617dd2ca93925b06e373958 Mon Sep 17 00:00:00 2001 From: Sasha Date: Thu, 2 Oct 2025 18:53:21 +0200 Subject: [PATCH 10/16] implement acks and message hash saving for sent messages --- packages/sdk/src/messaging/message_store.ts | 4 +- packages/sdk/src/messaging/messaging.ts | 3 +- packages/sdk/src/messaging/sender.ts | 52 ++++++++++++++------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 7bde1b153b..2a9159fcaa 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -134,8 +134,8 @@ export class MessageStore { let entry = this.messages.get(hashStr); if (entry) { - entry.filterAck = true; - entry.storeAck = true; + entry.filterAck = ackParams.filterAck ?? entry.filterAck; + entry.storeAck = ackParams.storeAck ?? entry.storeAck; return; } diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index a1fbf91e58..3568c0d831 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -38,7 +38,8 @@ export class Messaging implements IMessaging { this.sender = new Sender({ messageStore: this.messageStore, - lightPush: params.lightPush + lightPush: params.lightPush, + ackManager: this.ackManager }); } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index 0206d22d80..db772e2591 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -2,26 +2,31 @@ import { ICodec, IDecodedMessage, ILightPush, - IMessage + IMessage, + IProtoMessage } from "@waku/interfaces"; +import { AckManager } from "./ack_manager.js"; import type { MessageStore } from "./message_store.js"; import type { RequestId } from "./utils.js"; type SenderConstructorParams = { messageStore: MessageStore; lightPush: ILightPush; + ackManager: AckManager; }; export class Sender { private readonly messageStore: MessageStore; - // private readonly lightPush: ILightPush; + private readonly lightPush: ILightPush; + private readonly ackManager: AckManager; private sendInterval: ReturnType | null = null; public constructor(params: SenderConstructorParams) { this.messageStore = params.messageStore; - // this.lightPush = params.lightPush; + this.lightPush = params.lightPush; + this.ackManager = params.ackManager; } public start(): void { @@ -40,14 +45,20 @@ export class Sender { message: IMessage ): Promise { const requestId = await this.messageStore.queue(codec, message); - // const response = await this.lightPush.send(codec, message); - // if (response.successes.length > 0) { - await this.messageStore.markSent( - requestId, - (await codec.toProtoObj(message)) as IDecodedMessage - ); - // } + await this.ackManager.subscribe(codec); + + const response = await this.lightPush.send(codec, message); // todo: add to light push return of proto message or decoded message + + if (response.successes.length > 0) { + const protoObj = await codec.toProtoObj(message); + const decodedMessage = await codec.fromProtoObj( + codec.pubsubTopic, + protoObj as IProtoMessage + ); + + await this.messageStore.markSent(requestId, decodedMessage!); + } return requestId; } @@ -56,15 +67,20 @@ export class Sender { const pendingRequests = this.messageStore.getMessagesToSend(); for (const { requestId, codec, message } of pendingRequests) { - // const response = await this.lightPush.send(codec, message); + const response = await this.lightPush.send(codec, message); - // if (response.successes.length > 0) { - const sentMessage = await codec.toProtoObj(message); - await this.messageStore.markSent( - requestId, - sentMessage as IDecodedMessage - ); - // } + if (response.successes.length > 0) { + const protoObj = await codec.toProtoObj(message); + const decodedMessage = await codec.fromProtoObj( + codec.pubsubTopic, + protoObj as IProtoMessage + ); + + await this.messageStore.markSent( + requestId, + decodedMessage as IDecodedMessage + ); + } } } } From 7f98bb183de1f733bcb6f3f16fcf4cd27b641c3a Mon Sep 17 00:00:00 2001 From: Sasha Date: Thu, 2 Oct 2025 19:39:30 +0200 Subject: [PATCH 11/16] move from encoder/decoder/codec to simple message parameter --- packages/interfaces/src/waku.ts | 10 +-- packages/sdk/src/messaging/ack_manager.ts | 54 ++++++++++----- packages/sdk/src/messaging/index.ts | 3 +- packages/sdk/src/messaging/message_store.ts | 26 +++---- packages/sdk/src/messaging/messaging.ts | 27 +++----- packages/sdk/src/messaging/sender.ts | 77 ++++++++++++++++----- packages/sdk/src/messaging/utils.ts | 12 +++- packages/sdk/src/waku/waku.ts | 13 ++-- 8 files changed, 131 insertions(+), 91 deletions(-) diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index 9608dc58e4..be754bc255 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -10,13 +10,7 @@ import type { IFilter } from "./filter.js"; import type { HealthStatus } from "./health_status.js"; import type { Libp2p } from "./libp2p.js"; import type { ILightPush } from "./light_push.js"; -import { - ICodec, - IDecodedMessage, - IDecoder, - IEncoder, - IMessage -} from "./message.js"; +import { ICodec, IDecodedMessage, IDecoder, IEncoder } from "./message.js"; import type { Protocols } from "./protocols.js"; import type { IRelay } from "./relay.js"; import type { ShardId } from "./sharding.js"; @@ -313,7 +307,7 @@ export interface IWaku { * @param {IMessage} message - The message to send * @returns {Promise} A promise that resolves to the request ID */ - send(codec: ICodec, message: IMessage): Promise; + // send(codec: ICodec, message: IMessage): Promise; /** * @returns {boolean} `true` if the node was started and `false` otherwise diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts index d9a6211c17..21d856073f 100644 --- a/packages/sdk/src/messaging/ack_manager.ts +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -1,4 +1,12 @@ -import { ICodec, IDecodedMessage, IFilter, IStore } from "@waku/interfaces"; +import { createDecoder } from "@waku/core"; +import { + IDecodedMessage, + IDecoder, + IFilter, + IStore, + NetworkConfig +} from "@waku/interfaces"; +import { createRoutingInfo } from "@waku/utils"; import { MessageStore } from "./message_store.js"; import { IAckManager } from "./utils.js"; @@ -7,15 +15,18 @@ type AckManagerConstructorParams = { messageStore: MessageStore; filter: IFilter; store: IStore; + networkConfig: NetworkConfig; }; export class AckManager implements IAckManager { private readonly messageStore: MessageStore; private readonly filterAckManager: FilterAckManager; private readonly storeAckManager: StoreAckManager; + private readonly networkConfig: NetworkConfig; public constructor(params: AckManagerConstructorParams) { this.messageStore = params.messageStore; + this.networkConfig = params.networkConfig; this.filterAckManager = new FilterAckManager( this.messageStore, @@ -35,16 +46,23 @@ export class AckManager implements IAckManager { this.storeAckManager.stop(); } - public async subscribe(codec: ICodec): Promise { + public async subscribe(contentTopic: string): Promise { + const decoder = createDecoder( + contentTopic, + createRoutingInfo(this.networkConfig, { + contentTopic + }) + ); + return ( - (await this.filterAckManager.subscribe(codec)) || - (await this.storeAckManager.subscribe(codec)) + (await this.filterAckManager.subscribe(decoder)) || + (await this.storeAckManager.subscribe(decoder)) ); } } -class FilterAckManager implements IAckManager { - private codecs: Set> = new Set(); +class FilterAckManager { + private decoders: Set> = new Set(); public constructor( private messageStore: MessageStore, @@ -56,20 +74,20 @@ class FilterAckManager implements IAckManager { } public async stop(): Promise { - const promises = Array.from(this.codecs.entries()).map((codec) => - this.filter.unsubscribe(codec) + const promises = Array.from(this.decoders.entries()).map((decoder) => + this.filter.unsubscribe(decoder) ); await Promise.all(promises); - this.codecs.clear(); + this.decoders.clear(); } - public async subscribe(codec: ICodec): Promise { + public async subscribe(decoder: IDecoder): Promise { const success = await this.filter.subscribe( - codec, + decoder, this.onMessage.bind(this) ); if (success) { - this.codecs.add(codec); + this.decoders.add(decoder); } return success; } @@ -83,10 +101,10 @@ class FilterAckManager implements IAckManager { } } -class StoreAckManager implements IAckManager { +class StoreAckManager { private interval: ReturnType | null = null; - private codecs: Set> = new Set(); + private decoders: Set> = new Set(); public constructor( private messageStore: MessageStore, @@ -112,15 +130,15 @@ class StoreAckManager implements IAckManager { this.interval = null; } - public async subscribe(codec: ICodec): Promise { - this.codecs.add(codec); + public async subscribe(decoder: IDecoder): Promise { + this.decoders.add(decoder); return true; } private async query(): Promise { - for (const codec of this.codecs) { + for (const decoder of this.decoders) { await this.store.queryWithOrderedCallback( - [codec], + [decoder], (message) => { if (!this.messageStore.has(message.hashStr)) { this.messageStore.add(message, { storeAck: true }); diff --git a/packages/sdk/src/messaging/index.ts b/packages/sdk/src/messaging/index.ts index 04928b7037..9dcca8b113 100644 --- a/packages/sdk/src/messaging/index.ts +++ b/packages/sdk/src/messaging/index.ts @@ -1,2 +1,3 @@ export { Messaging } from "./messaging.js"; -export type { RequestId } from "./utils.js"; +// todo: do not export this +export type { RequestId, WakuLikeMessage } from "./utils.js"; diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 2a9159fcaa..5e9da4f2b6 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,10 +1,10 @@ -import { ICodec, IDecodedMessage, IMessage } from "@waku/interfaces"; +import { IDecodedMessage } from "@waku/interfaces"; import { v4 as uuidv4 } from "uuid"; +import { WakuLikeMessage } from "./utils.js"; + type QueuedMessage = { - codec?: ICodec; - messageRequest?: IMessage; - sentMessage?: IMessage; + messageRequest?: WakuLikeMessage; filterAck: boolean; storeAck: boolean; lastSentAt?: number; @@ -65,23 +65,18 @@ export class MessageStore { ): Promise { const entry = this.pendingRequests.get(requestId); - if (!entry || !entry.codec || !entry.messageRequest) { + if (!entry || !entry.messageRequest) { return; } entry.lastSentAt = Number(sentMessage.timestamp); - entry.sentMessage = sentMessage; this.pendingMessages.set(sentMessage.hashStr, requestId); } - public async queue( - codec: ICodec, - message: IMessage - ): Promise { + public async queue(message: WakuLikeMessage): Promise { const requestId = uuidv4(); this.pendingRequests.set(requestId.toString(), { - codec, messageRequest: message, filterAck: false, storeAck: false, @@ -93,19 +88,17 @@ export class MessageStore { public getMessagesToSend(): Array<{ requestId: string; - codec: ICodec; - message: IMessage; + message: WakuLikeMessage; }> { const res: Array<{ requestId: string; - codec: ICodec; - message: IMessage; + message: WakuLikeMessage; }> = []; for (const [requestId, entry] of this.pendingRequests.entries()) { const isAcknowledged = entry.filterAck || entry.storeAck; - if (!entry.codec || !entry.messageRequest || isAcknowledged) { + if (!entry.messageRequest || isAcknowledged) { continue; } @@ -118,7 +111,6 @@ export class MessageStore { if (notSent || notAcknowledged) { res.push({ requestId, - codec: entry.codec, message: entry.messageRequest }); } diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index 3568c0d831..05909a764a 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -1,25 +1,19 @@ -import { - ICodec, - IDecodedMessage, - IFilter, - ILightPush, - IMessage, - IStore -} from "@waku/interfaces"; +import { IFilter, ILightPush, IStore, NetworkConfig } from "@waku/interfaces"; import { AckManager } from "./ack_manager.js"; import { MessageStore } from "./message_store.js"; import { Sender } from "./sender.js"; -import type { RequestId } from "./utils.js"; +import type { RequestId, WakuLikeMessage } from "./utils.js"; interface IMessaging { - send(codec: ICodec, message: IMessage): Promise; + send(wakuLikeMessage: WakuLikeMessage): Promise; } type MessagingConstructorParams = { lightPush: ILightPush; filter: IFilter; store: IStore; + networkConfig: NetworkConfig; }; export class Messaging implements IMessaging { @@ -33,13 +27,15 @@ export class Messaging implements IMessaging { this.ackManager = new AckManager({ messageStore: this.messageStore, filter: params.filter, - store: params.store + store: params.store, + networkConfig: params.networkConfig }); this.sender = new Sender({ messageStore: this.messageStore, lightPush: params.lightPush, - ackManager: this.ackManager + ackManager: this.ackManager, + networkConfig: params.networkConfig }); } @@ -53,10 +49,7 @@ export class Messaging implements IMessaging { this.sender.stop(); } - public send( - codec: ICodec, - message: IMessage - ): Promise { - return this.sender.send(codec, message); + public send(wakuLikeMessage: WakuLikeMessage): Promise { + return this.sender.send(wakuLikeMessage); } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index db772e2591..ea7d150658 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -1,25 +1,28 @@ +import { createDecoder, createEncoder } from "@waku/core"; import { - ICodec, IDecodedMessage, ILightPush, - IMessage, - IProtoMessage + IProtoMessage, + NetworkConfig } from "@waku/interfaces"; +import { createRoutingInfo } from "@waku/utils"; import { AckManager } from "./ack_manager.js"; import type { MessageStore } from "./message_store.js"; -import type { RequestId } from "./utils.js"; +import type { RequestId, WakuLikeMessage } from "./utils.js"; type SenderConstructorParams = { messageStore: MessageStore; lightPush: ILightPush; ackManager: AckManager; + networkConfig: NetworkConfig; }; export class Sender { private readonly messageStore: MessageStore; private readonly lightPush: ILightPush; private readonly ackManager: AckManager; + private readonly networkConfig: NetworkConfig; private sendInterval: ReturnType | null = null; @@ -27,6 +30,7 @@ export class Sender { this.messageStore = params.messageStore; this.lightPush = params.lightPush; this.ackManager = params.ackManager; + this.networkConfig = params.networkConfig; } public start(): void { @@ -40,20 +44,36 @@ export class Sender { } } - public async send( - codec: ICodec, - message: IMessage - ): Promise { - const requestId = await this.messageStore.queue(codec, message); + public async send(wakuLikeMessage: WakuLikeMessage): Promise { + const requestId = await this.messageStore.queue(wakuLikeMessage); - await this.ackManager.subscribe(codec); + await this.ackManager.subscribe(wakuLikeMessage.contentTopic); - const response = await this.lightPush.send(codec, message); // todo: add to light push return of proto message or decoded message + const encoder = createEncoder({ + contentTopic: wakuLikeMessage.contentTopic, + routingInfo: createRoutingInfo(this.networkConfig, { + contentTopic: wakuLikeMessage.contentTopic + }), + ephemeral: wakuLikeMessage.ephemeral + }); + + const decoder = createDecoder( + wakuLikeMessage.contentTopic, + createRoutingInfo(this.networkConfig, { + contentTopic: wakuLikeMessage.contentTopic + }) + ); + + const response = await this.lightPush.send(encoder, { + payload: wakuLikeMessage.payload + }); // todo: add to light push return of proto message or decoded message if (response.successes.length > 0) { - const protoObj = await codec.toProtoObj(message); - const decodedMessage = await codec.fromProtoObj( - codec.pubsubTopic, + const protoObj = await encoder.toProtoObj({ + payload: wakuLikeMessage.payload + }); + const decodedMessage = await decoder.fromProtoObj( + decoder.pubsubTopic, protoObj as IProtoMessage ); @@ -66,13 +86,32 @@ export class Sender { private async backgroundSend(): Promise { const pendingRequests = this.messageStore.getMessagesToSend(); - for (const { requestId, codec, message } of pendingRequests) { - const response = await this.lightPush.send(codec, message); + for (const { requestId, message } of pendingRequests) { + const encoder = createEncoder({ + contentTopic: message.contentTopic, + routingInfo: createRoutingInfo(this.networkConfig, { + contentTopic: message.contentTopic + }), + ephemeral: message.ephemeral + }); + + const decoder = createDecoder( + message.contentTopic, + createRoutingInfo(this.networkConfig, { + contentTopic: message.contentTopic + }) + ); + + const response = await this.lightPush.send(encoder, { + payload: message.payload + }); if (response.successes.length > 0) { - const protoObj = await codec.toProtoObj(message); - const decodedMessage = await codec.fromProtoObj( - codec.pubsubTopic, + const protoObj = await encoder.toProtoObj({ + payload: message.payload + }); + const decodedMessage = await decoder.fromProtoObj( + decoder.pubsubTopic, protoObj as IProtoMessage ); diff --git a/packages/sdk/src/messaging/utils.ts b/packages/sdk/src/messaging/utils.ts index f76cb7920f..3382be0fe7 100644 --- a/packages/sdk/src/messaging/utils.ts +++ b/packages/sdk/src/messaging/utils.ts @@ -1,9 +1,15 @@ -import { ICodec, IDecodedMessage } from "@waku/interfaces"; - export type RequestId = string; +// todo: make it IMessage type +export type WakuLikeMessage = { + contentTopic: string; + payload: Uint8Array; + ephemeral?: boolean; + rateLimitProof?: boolean; +}; + export interface IAckManager { start(): void; stop(): void; - subscribe(codec: ICodec): Promise; + subscribe(contentTopic: string): Promise; } diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 720e196883..9cb2d29770 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -22,7 +22,6 @@ import type { IEncoder, IFilter, ILightPush, - IMessage, IRelay, IRoutingInfo, IStore, @@ -42,7 +41,7 @@ import { Filter } from "../filter/index.js"; import { HealthIndicator } from "../health_indicator/index.js"; import { LightPush } from "../light_push/index.js"; import { Messaging } from "../messaging/index.js"; -import type { RequestId } from "../messaging/index.js"; +import type { RequestId, WakuLikeMessage } from "../messaging/index.js"; import { PeerManager } from "../peer_manager/index.js"; import { Store } from "../store/index.js"; @@ -141,7 +140,8 @@ export class WakuNode implements IWaku { this.messaging = new Messaging({ lightPush: this.lightPush, filter: this.filter, - store: this.store + store: this.store, + networkConfig: this.networkConfig }); } @@ -303,15 +303,12 @@ export class WakuNode implements IWaku { }); } - public send( - codec: ICodec, - message: IMessage - ): Promise { + public send(wakuLikeMessage: WakuLikeMessage): Promise { if (!this.messaging) { throw new Error("Messaging not initialized"); } - return this.messaging.send(codec, message); + return this.messaging.send(wakuLikeMessage); } public createCodec(params: CreateCodecParams): ICodec { From da1147819388976b6bea78df4da36ee701eed9de Mon Sep 17 00:00:00 2001 From: Sasha Date: Tue, 7 Oct 2025 00:00:27 +0200 Subject: [PATCH 12/16] implement working version of sendind --- .../core/src/lib/light_push/light_push.ts | 26 ++++-- .../src/lib/light_push/protocol_handler.ts | 15 +++- packages/interfaces/src/protocols.ts | 18 +++- packages/interfaces/src/sender.ts | 6 ++ packages/sdk/src/light_push/light_push.ts | 24 +++-- packages/sdk/src/messaging/ack_manager.ts | 18 +++- packages/sdk/src/messaging/message_store.ts | 35 +++++--- packages/sdk/src/messaging/sender.ts | 87 ++++++++----------- 8 files changed, 141 insertions(+), 88 deletions(-) diff --git a/packages/core/src/lib/light_push/light_push.ts b/packages/core/src/lib/light_push/light_push.ts index eb3b517eeb..6206a9c89f 100644 --- a/packages/core/src/lib/light_push/light_push.ts +++ b/packages/core/src/lib/light_push/light_push.ts @@ -55,11 +55,11 @@ export class LightPushCore { }; } - const { rpc, error: prepError } = await ProtocolHandler.preparePushMessage( - encoder, - message, - protocol - ); + const { + rpc, + error: prepError, + message: protoMessage + } = await ProtocolHandler.preparePushMessage(encoder, message, protocol); if (prepError) { return { @@ -117,7 +117,21 @@ export class LightPushCore { }; } - return ProtocolHandler.handleResponse(bytes, protocol, peerId); + const processedResponse = ProtocolHandler.handleResponse( + bytes, + protocol, + peerId + ); + + if (processedResponse.success) { + return { + success: processedResponse.success, + failure: null, + message: protoMessage + }; + } + + return processedResponse; } private async getProtocol( diff --git a/packages/core/src/lib/light_push/protocol_handler.ts b/packages/core/src/lib/light_push/protocol_handler.ts index 429664f32d..0cf2835a96 100644 --- a/packages/core/src/lib/light_push/protocol_handler.ts +++ b/packages/core/src/lib/light_push/protocol_handler.ts @@ -1,5 +1,10 @@ import type { PeerId } from "@libp2p/interface"; -import type { IEncoder, IMessage, LightPushCoreResult } from "@waku/interfaces"; +import type { + IEncoder, + IMessage, + IProtoMessage, + LightPushCoreResult +} from "@waku/interfaces"; import { LightPushError, LightPushStatusCode } from "@waku/interfaces"; import { PushResponse, WakuMessage } from "@waku/proto"; import { isMessageSizeUnderCap, Logger } from "@waku/utils"; @@ -15,8 +20,8 @@ type VersionedPushRpc = | ({ version: "v3" } & PushRpc); type PreparePushMessageResult = - | { rpc: VersionedPushRpc; error: null } - | { rpc: null; error: LightPushError }; + | { rpc: VersionedPushRpc; error: null; message?: IProtoMessage } + | { rpc: null; error: LightPushError; message?: IProtoMessage }; const log = new Logger("light-push:protocol-handler"); @@ -47,13 +52,15 @@ export class ProtocolHandler { log.info("Creating v3 RPC message"); return { rpc: ProtocolHandler.createV3Rpc(protoMessage, encoder.pubsubTopic), - error: null + error: null, + message: protoMessage }; } log.info("Creating v2 RPC message"); return { rpc: ProtocolHandler.createV2Rpc(protoMessage, encoder.pubsubTopic), + message: protoMessage, error: null }; } catch (err) { diff --git a/packages/interfaces/src/protocols.ts b/packages/interfaces/src/protocols.ts index 0fb60c182f..66e2ca09db 100644 --- a/packages/interfaces/src/protocols.ts +++ b/packages/interfaces/src/protocols.ts @@ -5,7 +5,7 @@ import type { DiscoveryOptions, PeerCache } from "./discovery.js"; import type { FilterProtocolOptions } from "./filter.js"; import type { CreateLibp2pOptions } from "./libp2p.js"; import type { LightPushProtocolOptions } from "./light_push.js"; -import type { IDecodedMessage } from "./message.js"; +import type { IDecodedMessage, IProtoMessage } from "./message.js"; import type { ThisAndThat, ThisOrThat } from "./misc.js"; import { NetworkConfig } from "./sharding.js"; import type { StoreProtocolOptions } from "./store.js"; @@ -195,7 +195,13 @@ export type LightPushCoreResult = ThisOrThat< PeerId, "failure", LightPushFailure ->; +> & { + /** + * The proto object of the message. + * Present only if the message was successfully pushed to the network. + */ + message?: IProtoMessage; +}; export type FilterCoreResult = ThisOrThat< "success", @@ -209,7 +215,13 @@ export type LightPushSDKResult = ThisAndThat< PeerId[], "failures", LightPushFailure[] ->; +> & { + /** + * The proto objects of the messages. + * Present only if the messages were successfully pushed to the network. + */ + messages?: IProtoMessage[]; +}; export type FilterSDKResult = ThisAndThat< "successes", diff --git a/packages/interfaces/src/sender.ts b/packages/interfaces/src/sender.ts index da4fc5f003..8cfee2ebfc 100644 --- a/packages/interfaces/src/sender.ts +++ b/packages/interfaces/src/sender.ts @@ -20,6 +20,12 @@ export type ISendOptions = { * @default false */ useLegacy?: boolean; + + /** + * Amount of peers to send message to. + * Overrides `numPeersToUse` in {@link @waku/interfaces!CreateNodeOptions}. + */ + numPeersToUse?: number; }; export interface ISender { diff --git a/packages/sdk/src/light_push/light_push.ts b/packages/sdk/src/light_push/light_push.ts index 669c77e38c..05a94aaf91 100644 --- a/packages/sdk/src/light_push/light_push.ts +++ b/packages/sdk/src/light_push/light_push.ts @@ -4,6 +4,7 @@ import { type IEncoder, ILightPush, type IMessage, + IProtoMessage, type ISendOptions, type Libp2p, LightPushCoreResult, @@ -82,10 +83,11 @@ export class LightPush implements ILightPush { log.info("send: attempting to send a message to pubsubTopic:", pubsubTopic); - const peerIds = await this.peerManager.getPeers({ + let peerIds = await this.peerManager.getPeers({ protocol: options.useLegacy ? "light-push-v2" : Protocols.LightPush, pubsubTopic: encoder.pubsubTopic }); + peerIds = peerIds.slice(0, options.numPeersToUse); const coreResults = peerIds?.length > 0 @@ -93,12 +95,15 @@ export class LightPush implements ILightPush { peerIds.map((peerId) => this.protocol .send(encoder, message, peerId, options.useLegacy) - .catch((_e) => ({ - success: null, - failure: { - error: LightPushError.GENERIC_FAIL - } - })) + .catch( + (_e) => + ({ + success: null, + failure: { + error: LightPushError.GENERIC_FAIL + } + }) as LightPushCoreResult + ) ) ) : []; @@ -110,7 +115,10 @@ export class LightPush implements ILightPush { .map((v) => v.success) as PeerId[], failures: coreResults .filter((v) => v.failure) - .map((v) => v.failure) as LightPushFailure[] + .map((v) => v.failure) as LightPushFailure[], + messages: coreResults + .filter((v) => v.message) + .map((v) => v.message) as IProtoMessage[] } : { successes: [], diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts index 21d856073f..8551cbc4b3 100644 --- a/packages/sdk/src/messaging/ack_manager.ts +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -24,6 +24,8 @@ export class AckManager implements IAckManager { private readonly storeAckManager: StoreAckManager; private readonly networkConfig: NetworkConfig; + private readonly subscribedContentTopics: Set = new Set(); + public constructor(params: AckManagerConstructorParams) { this.messageStore = params.messageStore; this.networkConfig = params.networkConfig; @@ -44,9 +46,15 @@ export class AckManager implements IAckManager { public async stop(): Promise { await this.filterAckManager.stop(); this.storeAckManager.stop(); + this.subscribedContentTopics.clear(); } public async subscribe(contentTopic: string): Promise { + if (this.subscribedContentTopics.has(contentTopic)) { + return true; + } + + this.subscribedContentTopics.add(contentTopic); const decoder = createDecoder( contentTopic, createRoutingInfo(this.networkConfig, { @@ -55,9 +63,11 @@ export class AckManager implements IAckManager { ); return ( - (await this.filterAckManager.subscribe(decoder)) || - (await this.storeAckManager.subscribe(decoder)) - ); + await Promise.all([ + this.filterAckManager.subscribe(decoder), + this.storeAckManager.subscribe(decoder) + ]) + ).some((success) => success); } } @@ -118,7 +128,7 @@ class StoreAckManager { this.interval = setInterval(() => { void this.query(); - }, 1000); + }, 5000); } public stop(): void { diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 5e9da4f2b6..7996beff08 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -32,15 +32,15 @@ export class MessageStore { private readonly resendIntervalMs: number; public constructor(options: MessageStoreOptions = {}) { - this.resendIntervalMs = options.resendIntervalMs ?? 2000; + this.resendIntervalMs = options.resendIntervalMs ?? 5000; } public has(hashStr: string): boolean { - return this.messages.has(hashStr); + return this.messages.has(hashStr) || this.pendingMessages.has(hashStr); } public add(message: IDecodedMessage, options: AddMessageOptions = {}): void { - if (!this.messages.has(message.hashStr)) { + if (!this.has(message.hashStr)) { this.messages.set(message.hashStr, { filterAck: options.filterAck ?? false, storeAck: options.storeAck ?? false, @@ -59,10 +59,7 @@ export class MessageStore { this.replacePendingWithMessage(hashStr); } - public async markSent( - requestId: RequestId, - sentMessage: IDecodedMessage - ): Promise { + public markSent(requestId: RequestId, sentMessage: IDecodedMessage): void { const entry = this.pendingRequests.get(requestId); if (!entry || !entry.messageRequest) { @@ -71,6 +68,8 @@ export class MessageStore { entry.lastSentAt = Number(sentMessage.timestamp); this.pendingMessages.set(sentMessage.hashStr, requestId); + + this.replacePendingWithMessage(sentMessage.hashStr); } public async queue(message: WakuLikeMessage): Promise { @@ -102,13 +101,11 @@ export class MessageStore { continue; } - const notSent = !entry.lastSentAt; - const notAcknowledged = - entry.lastSentAt && - Date.now() - entry.lastSentAt >= this.resendIntervalMs && - !isAcknowledged; + const sentAt = entry.lastSentAt || entry.createdAt; + const notTooRecent = Date.now() - sentAt >= this.resendIntervalMs; + const notAcknowledged = !isAcknowledged; - if (notSent || notAcknowledged) { + if (notTooRecent && notAcknowledged) { res.push({ requestId, message: entry.messageRequest @@ -154,12 +151,22 @@ export class MessageStore { return; } - const entry = this.pendingRequests.get(requestId); + let entry = this.pendingRequests.get(requestId); if (!entry) { return; } + // merge with message entry if possible + // this can happen if message we sent got received before we could add it to the message store + const messageEntry = this.messages.get(hashStr); + entry = { + ...entry, + ...messageEntry, + filterAck: messageEntry?.filterAck ?? entry.filterAck, + storeAck: messageEntry?.storeAck ?? entry.storeAck + }; + this.pendingRequests.delete(requestId); this.pendingMessages.delete(hashStr); diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index ea7d150658..35bfd38e94 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -1,10 +1,5 @@ import { createDecoder, createEncoder } from "@waku/core"; -import { - IDecodedMessage, - ILightPush, - IProtoMessage, - NetworkConfig -} from "@waku/interfaces"; +import { ILightPush, NetworkConfig } from "@waku/interfaces"; import { createRoutingInfo } from "@waku/utils"; import { AckManager } from "./ack_manager.js"; @@ -24,6 +19,8 @@ export class Sender { private readonly ackManager: AckManager; private readonly networkConfig: NetworkConfig; + private readonly processingRequests: Set = new Set(); + private sendInterval: ReturnType | null = null; public constructor(params: SenderConstructorParams) { @@ -48,37 +45,7 @@ export class Sender { const requestId = await this.messageStore.queue(wakuLikeMessage); await this.ackManager.subscribe(wakuLikeMessage.contentTopic); - - const encoder = createEncoder({ - contentTopic: wakuLikeMessage.contentTopic, - routingInfo: createRoutingInfo(this.networkConfig, { - contentTopic: wakuLikeMessage.contentTopic - }), - ephemeral: wakuLikeMessage.ephemeral - }); - - const decoder = createDecoder( - wakuLikeMessage.contentTopic, - createRoutingInfo(this.networkConfig, { - contentTopic: wakuLikeMessage.contentTopic - }) - ); - - const response = await this.lightPush.send(encoder, { - payload: wakuLikeMessage.payload - }); // todo: add to light push return of proto message or decoded message - - if (response.successes.length > 0) { - const protoObj = await encoder.toProtoObj({ - payload: wakuLikeMessage.payload - }); - const decodedMessage = await decoder.fromProtoObj( - decoder.pubsubTopic, - protoObj as IProtoMessage - ); - - await this.messageStore.markSent(requestId, decodedMessage!); - } + await this.sendMessage(requestId, wakuLikeMessage); return requestId; } @@ -87,6 +54,21 @@ export class Sender { const pendingRequests = this.messageStore.getMessagesToSend(); for (const { requestId, message } of pendingRequests) { + await this.sendMessage(requestId, message); + } + } + + private async sendMessage( + requestId: RequestId, + message: WakuLikeMessage + ): Promise { + try { + if (this.processingRequests.has(requestId)) { + return; + } + + this.processingRequests.add(requestId); + const encoder = createEncoder({ contentTopic: message.contentTopic, routingInfo: createRoutingInfo(this.networkConfig, { @@ -102,24 +84,31 @@ export class Sender { }) ); - const response = await this.lightPush.send(encoder, { - payload: message.payload - }); - - if (response.successes.length > 0) { - const protoObj = await encoder.toProtoObj({ + const response = await this.lightPush.send( + encoder, + { payload: message.payload - }); + }, + { + // force no retry as we have retry implemented in the sender + autoRetry: false, + // send to only one peer as we will retry on failure and need to ensure only one message is in the network + numPeersToUse: 1 + } + ); + + if (response?.messages && response.messages.length > 0) { const decodedMessage = await decoder.fromProtoObj( decoder.pubsubTopic, - protoObj as IProtoMessage + response.messages[0] ); - await this.messageStore.markSent( - requestId, - decodedMessage as IDecodedMessage - ); + this.messageStore.markSent(requestId, decodedMessage!); + } else { + // do nothing on failure, will retry } + } finally { + this.processingRequests.delete(requestId); } } } From 80cef4bc2a123ad6077b69c3cdabd53b211fe159 Mon Sep 17 00:00:00 2001 From: Sasha Date: Tue, 7 Oct 2025 00:16:38 +0200 Subject: [PATCH 13/16] remove Codec, update types --- packages/core/src/index.ts | 2 - packages/core/src/lib/message/codec.ts | 76 --------------------- packages/core/src/lib/message/index.ts | 1 - packages/interfaces/src/message.ts | 17 ++++- packages/interfaces/src/waku.ts | 51 +++----------- packages/sdk/src/messaging/index.ts | 2 - packages/sdk/src/messaging/message_store.ts | 13 ++-- packages/sdk/src/messaging/messaging.ts | 14 ++-- packages/sdk/src/messaging/sender.ts | 18 +++-- packages/sdk/src/messaging/utils.ts | 10 --- packages/sdk/src/waku/waku.ts | 31 ++------- packages/utils/src/common/mock_node.ts | 14 ++-- 12 files changed, 61 insertions(+), 188 deletions(-) delete mode 100644 packages/core/src/lib/message/codec.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5688e5e937..8021ac0a91 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,11 +1,9 @@ export { createEncoder, createDecoder } from "./lib/message/version_0.js"; -export { createCodec } from "./lib/message/index.js"; export type { Encoder, Decoder, DecodedMessage } from "./lib/message/version_0.js"; -export type { Codec } from "./lib/message/index.js"; export * as message from "./lib/message/index.js"; export * as waku_filter from "./lib/filter/index.js"; diff --git a/packages/core/src/lib/message/codec.ts b/packages/core/src/lib/message/codec.ts deleted file mode 100644 index c0212331bf..0000000000 --- a/packages/core/src/lib/message/codec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { - ICodec, - IDecodedMessage, - IDecoder, - IEncoder, - IMessage, - IMetaSetter, - IProtoMessage, - IRoutingInfo, - PubsubTopic -} from "@waku/interfaces"; - -import { Decoder, Encoder } from "./version_0.js"; - -export class Codec implements ICodec { - private encoder: IEncoder; - private decoder: IDecoder; - - public constructor( - public contentTopic: string, - public ephemeral: boolean = false, - public routingInfo: IRoutingInfo, - public metaSetter?: IMetaSetter - ) { - this.encoder = new Encoder( - contentTopic, - ephemeral, - routingInfo, - metaSetter - ); - this.decoder = new Decoder(contentTopic, routingInfo); - } - - public get pubsubTopic(): PubsubTopic { - return this.routingInfo.pubsubTopic; - } - - public async toWire(message: IMessage): Promise { - return this.encoder.toWire(message); - } - - public async toProtoObj( - message: IMessage - ): Promise { - return this.encoder.toProtoObj(message); - } - - public fromWireToProtoObj( - bytes: Uint8Array - ): Promise { - return this.decoder.fromWireToProtoObj(bytes); - } - - public async fromProtoObj( - pubsubTopic: string, - proto: IProtoMessage - ): Promise { - return this.decoder.fromProtoObj(pubsubTopic, proto); - } -} - -type CodecParams = { - contentTopic: string; - ephemeral: boolean; - routingInfo: IRoutingInfo; - metaSetter?: IMetaSetter; -}; - -export function createCodec(params: CodecParams): Codec { - return new Codec( - params.contentTopic, - params.ephemeral, - params.routingInfo, - params.metaSetter - ); -} diff --git a/packages/core/src/lib/message/index.ts b/packages/core/src/lib/message/index.ts index 8add5fba21..e5fbf51df3 100644 --- a/packages/core/src/lib/message/index.ts +++ b/packages/core/src/lib/message/index.ts @@ -1,3 +1,2 @@ export * as version_0 from "./version_0.js"; -export { Codec, createCodec } from "./codec.js"; export { OneMillion, Version } from "./constants.js"; diff --git a/packages/interfaces/src/message.ts b/packages/interfaces/src/message.ts index e97a5d0304..bdf08ebcf3 100644 --- a/packages/interfaces/src/message.ts +++ b/packages/interfaces/src/message.ts @@ -69,6 +69,21 @@ export interface IMessage { rateLimitProof?: IRateLimitProof; } +/** + * Send message data structure used in {@link IWaku.send}. + */ +export interface ISendMessage { + contentTopic: string; + payload: Uint8Array; + ephemeral?: boolean; + rateLimitProof?: boolean; +} + +/** + * Request ID of attempt to send a message. + */ +export type RequestId = string; + export interface IMetaSetter { (message: IProtoMessage & { meta: undefined }): Uint8Array; } @@ -111,5 +126,3 @@ export interface IDecoder { proto: IProtoMessage ) => Promise; } - -export type ICodec = IEncoder & IDecoder; diff --git a/packages/interfaces/src/waku.ts b/packages/interfaces/src/waku.ts index be754bc255..960d9352d2 100644 --- a/packages/interfaces/src/waku.ts +++ b/packages/interfaces/src/waku.ts @@ -10,7 +10,13 @@ import type { IFilter } from "./filter.js"; import type { HealthStatus } from "./health_status.js"; import type { Libp2p } from "./libp2p.js"; import type { ILightPush } from "./light_push.js"; -import { ICodec, IDecodedMessage, IDecoder, IEncoder } from "./message.js"; +import { + IDecodedMessage, + IDecoder, + IEncoder, + ISendMessage, + RequestId +} from "./message.js"; import type { Protocols } from "./protocols.js"; import type { IRelay } from "./relay.js"; import type { ShardId } from "./sharding.js"; @@ -25,8 +31,6 @@ export type CreateEncoderParams = CreateDecoderParams & { ephemeral?: boolean; }; -export type CreateCodecParams = CreateDecoderParams & CreateEncoderParams; - export enum WakuEvent { Connection = "waku:connection", Health = "waku:health" @@ -208,8 +212,6 @@ export interface IWaku { waitForPeers(protocols?: Protocols[], timeoutMs?: number): Promise; /** - * @deprecated Use {@link createCodec} instead - * * Creates a decoder for Waku messages on a specific content topic. * * A decoder is used to decode messages from the Waku network format. @@ -239,8 +241,6 @@ export interface IWaku { createDecoder(params: CreateDecoderParams): IDecoder; /** - * @deprecated Use {@link createCodec} instead - * * Creates an encoder for Waku messages on a specific content topic. * * An encoder is used to encode messages into the Waku network format. @@ -270,44 +270,13 @@ export interface IWaku { */ createEncoder(params: CreateEncoderParams): IEncoder; - /** - * Creates a codec for Waku messages on a specific content topic. - * - * A codec is used to encode and decode messages from the Waku network format. - * The codec automatically handles shard configuration based on the Waku node's network settings. - * - * @param {CreateCodecParams} params - Configuration for the codec including content topic and optionally shard information and ephemeral flag - * @returns {ICodec} A codec instance configured for the specified content topic - * @throws {Error} If the shard configuration is incompatible with the node's network settings - * - * @example - * ```typescript - * // Create a codec with default network shard settings - * const codec = waku.createCodec({ - * contentTopic: "/my-app/1/chat/proto" - * }); - * - * // Create a codec with custom shard settings - * const customCodec = waku.createCodec({ - * contentTopic: "/my-app/1/chat/proto", - * ephemeral: true, - * shardInfo: { - * clusterId: 1, - * shard: 5 - * } - * }); - * ``` - */ - createCodec(params: CreateCodecParams): ICodec; - /** * Sends a message to the Waku network. * - * @param {ICodec} codec - The codec to use for encoding the message - * @param {IMessage} message - The message to send - * @returns {Promise} A promise that resolves to the request ID + * @param {ISendMessage} message - The message to send. + * @returns {Promise} A promise that resolves to the request ID */ - // send(codec: ICodec, message: IMessage): Promise; + send(message: ISendMessage): Promise; /** * @returns {boolean} `true` if the node was started and `false` otherwise diff --git a/packages/sdk/src/messaging/index.ts b/packages/sdk/src/messaging/index.ts index 9dcca8b113..0035e4eb2f 100644 --- a/packages/sdk/src/messaging/index.ts +++ b/packages/sdk/src/messaging/index.ts @@ -1,3 +1 @@ export { Messaging } from "./messaging.js"; -// todo: do not export this -export type { RequestId, WakuLikeMessage } from "./utils.js"; diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index 7996beff08..f065b75651 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -1,10 +1,8 @@ -import { IDecodedMessage } from "@waku/interfaces"; +import { IDecodedMessage, ISendMessage, RequestId } from "@waku/interfaces"; import { v4 as uuidv4 } from "uuid"; -import { WakuLikeMessage } from "./utils.js"; - type QueuedMessage = { - messageRequest?: WakuLikeMessage; + messageRequest?: ISendMessage; filterAck: boolean; storeAck: boolean; lastSentAt?: number; @@ -20,7 +18,6 @@ type MessageStoreOptions = { resendIntervalMs?: number; }; -type RequestId = string; type MessageHashStr = string; export class MessageStore { @@ -72,7 +69,7 @@ export class MessageStore { this.replacePendingWithMessage(sentMessage.hashStr); } - public async queue(message: WakuLikeMessage): Promise { + public async queue(message: ISendMessage): Promise { const requestId = uuidv4(); this.pendingRequests.set(requestId.toString(), { @@ -87,11 +84,11 @@ export class MessageStore { public getMessagesToSend(): Array<{ requestId: string; - message: WakuLikeMessage; + message: ISendMessage; }> { const res: Array<{ requestId: string; - message: WakuLikeMessage; + message: ISendMessage; }> = []; for (const [requestId, entry] of this.pendingRequests.entries()) { diff --git a/packages/sdk/src/messaging/messaging.ts b/packages/sdk/src/messaging/messaging.ts index 05909a764a..28a17dbade 100644 --- a/packages/sdk/src/messaging/messaging.ts +++ b/packages/sdk/src/messaging/messaging.ts @@ -1,12 +1,18 @@ -import { IFilter, ILightPush, IStore, NetworkConfig } from "@waku/interfaces"; +import { + IFilter, + ILightPush, + ISendMessage, + IStore, + NetworkConfig, + RequestId +} from "@waku/interfaces"; import { AckManager } from "./ack_manager.js"; import { MessageStore } from "./message_store.js"; import { Sender } from "./sender.js"; -import type { RequestId, WakuLikeMessage } from "./utils.js"; interface IMessaging { - send(wakuLikeMessage: WakuLikeMessage): Promise; + send(wakuLikeMessage: ISendMessage): Promise; } type MessagingConstructorParams = { @@ -49,7 +55,7 @@ export class Messaging implements IMessaging { this.sender.stop(); } - public send(wakuLikeMessage: WakuLikeMessage): Promise { + public send(wakuLikeMessage: ISendMessage): Promise { return this.sender.send(wakuLikeMessage); } } diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index 35bfd38e94..b32978d8b8 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -1,10 +1,14 @@ import { createDecoder, createEncoder } from "@waku/core"; -import { ILightPush, NetworkConfig } from "@waku/interfaces"; +import { + ILightPush, + ISendMessage, + NetworkConfig, + RequestId +} from "@waku/interfaces"; import { createRoutingInfo } from "@waku/utils"; import { AckManager } from "./ack_manager.js"; import type { MessageStore } from "./message_store.js"; -import type { RequestId, WakuLikeMessage } from "./utils.js"; type SenderConstructorParams = { messageStore: MessageStore; @@ -41,11 +45,11 @@ export class Sender { } } - public async send(wakuLikeMessage: WakuLikeMessage): Promise { - const requestId = await this.messageStore.queue(wakuLikeMessage); + public async send(message: ISendMessage): Promise { + const requestId = await this.messageStore.queue(message); - await this.ackManager.subscribe(wakuLikeMessage.contentTopic); - await this.sendMessage(requestId, wakuLikeMessage); + await this.ackManager.subscribe(message.contentTopic); + await this.sendMessage(requestId, message); return requestId; } @@ -60,7 +64,7 @@ export class Sender { private async sendMessage( requestId: RequestId, - message: WakuLikeMessage + message: ISendMessage ): Promise { try { if (this.processingRequests.has(requestId)) { diff --git a/packages/sdk/src/messaging/utils.ts b/packages/sdk/src/messaging/utils.ts index 3382be0fe7..3ce045e68c 100644 --- a/packages/sdk/src/messaging/utils.ts +++ b/packages/sdk/src/messaging/utils.ts @@ -1,13 +1,3 @@ -export type RequestId = string; - -// todo: make it IMessage type -export type WakuLikeMessage = { - contentTopic: string; - payload: Uint8Array; - ephemeral?: boolean; - rateLimitProof?: boolean; -}; - export interface IAckManager { start(): void; stop(): void; diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 9cb2d29770..4e8c07d7fb 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -5,18 +5,11 @@ import { TypedEventEmitter } from "@libp2p/interface"; import type { MultiaddrInput } from "@multiformats/multiaddr"; -import { - ConnectionManager, - createCodec, - createDecoder, - createEncoder -} from "@waku/core"; +import { ConnectionManager, createDecoder, createEncoder } from "@waku/core"; import type { - CreateCodecParams, CreateDecoderParams, CreateEncoderParams, CreateNodeOptions, - ICodec, IDecodedMessage, IDecoder, IEncoder, @@ -33,7 +26,9 @@ import type { import { DefaultNetworkConfig, HealthStatus, - Protocols + ISendMessage, + Protocols, + RequestId } from "@waku/interfaces"; import { createRoutingInfo, Logger } from "@waku/utils"; @@ -41,7 +36,6 @@ import { Filter } from "../filter/index.js"; import { HealthIndicator } from "../health_indicator/index.js"; import { LightPush } from "../light_push/index.js"; import { Messaging } from "../messaging/index.js"; -import type { RequestId, WakuLikeMessage } from "../messaging/index.js"; import { PeerManager } from "../peer_manager/index.js"; import { Store } from "../store/index.js"; @@ -303,25 +297,12 @@ export class WakuNode implements IWaku { }); } - public send(wakuLikeMessage: WakuLikeMessage): Promise { + public send(message: ISendMessage): Promise { if (!this.messaging) { throw new Error("Messaging not initialized"); } - return this.messaging.send(wakuLikeMessage); - } - - public createCodec(params: CreateCodecParams): ICodec { - const routingInfo = this.createRoutingInfo( - params.contentTopic, - params.shardId - ); - - return createCodec({ - contentTopic: params.contentTopic, - ephemeral: params.ephemeral ?? false, - routingInfo: routingInfo - }); + return this.messaging.send(message); } private createRoutingInfo( diff --git a/packages/utils/src/common/mock_node.ts b/packages/utils/src/common/mock_node.ts index d33d83e88e..5d0dfd08c2 100644 --- a/packages/utils/src/common/mock_node.ts +++ b/packages/utils/src/common/mock_node.ts @@ -2,11 +2,9 @@ import { Peer, PeerId, Stream, TypedEventEmitter } from "@libp2p/interface"; import { MultiaddrInput } from "@multiformats/multiaddr"; import { Callback, - CreateCodecParams, CreateDecoderParams, CreateEncoderParams, HealthStatus, - ICodec, IDecodedMessage, IDecoder, IEncoder, @@ -14,13 +12,15 @@ import { ILightPush, type IMessage, IRelay, + ISendMessage, ISendOptions, IStore, IWaku, IWakuEventEmitter, Libp2p, LightPushSDKResult, - Protocols + Protocols, + RequestId } from "@waku/interfaces"; export type MockWakuEvents = { @@ -156,13 +156,7 @@ export class MockWakuNode implements IWaku { public createEncoder(_params: CreateEncoderParams): IEncoder { throw new Error("Method not implemented."); } - public createCodec(_params: CreateCodecParams): ICodec { - throw new Error("Method not implemented."); - } - public send( - _codec: ICodec, - _message: IMessage - ): Promise { + public send(_message: ISendMessage): Promise { throw new Error("Method not implemented."); } public isStarted(): boolean { From 969330d5cbb349d11d19e81f0b8884afcc34da5f Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 8 Oct 2025 00:29:59 +0200 Subject: [PATCH 14/16] add unit tests --- .../sdk/src/messaging/ack_manager.spec.ts | 309 ++++++++++++++++ .../sdk/src/messaging/message_store.spec.ts | 349 ++++++++++++++++++ packages/sdk/src/messaging/message_store.ts | 2 +- packages/sdk/src/messaging/messaging.spec.ts | 137 ++----- packages/sdk/src/messaging/sender.spec.ts | 144 ++++++++ 5 files changed, 830 insertions(+), 111 deletions(-) create mode 100644 packages/sdk/src/messaging/ack_manager.spec.ts create mode 100644 packages/sdk/src/messaging/message_store.spec.ts create mode 100644 packages/sdk/src/messaging/sender.spec.ts diff --git a/packages/sdk/src/messaging/ack_manager.spec.ts b/packages/sdk/src/messaging/ack_manager.spec.ts new file mode 100644 index 0000000000..37cfcaefa2 --- /dev/null +++ b/packages/sdk/src/messaging/ack_manager.spec.ts @@ -0,0 +1,309 @@ +import type { + IDecodedMessage, + IFilter, + IStore, + NetworkConfig +} from "@waku/interfaces"; +import { expect } from "chai"; +import { afterEach, beforeEach, describe, it } from "mocha"; +import sinon from "sinon"; + +import { AckManager } from "./ack_manager.js"; +import { MessageStore } from "./message_store.js"; + +const mockMessage: IDecodedMessage = { + version: 1, + payload: new Uint8Array([1, 2, 3]), + contentTopic: "/test/1/topic/proto", + pubsubTopic: "test-pubsub", + timestamp: new Date(), + rateLimitProof: undefined, + ephemeral: false, + meta: undefined, + hash: new Uint8Array([4, 5, 6]), + hashStr: "test-hash-123" +}; + +const mockNetworkConfig: NetworkConfig = { + clusterId: 1, + numShardsInCluster: 8 +}; + +describe("AckManager", () => { + let messageStore: MessageStore; + let mockFilter: IFilter; + let mockStore: IStore; + let ackManager: AckManager; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + messageStore = new MessageStore(); + + mockFilter = { + subscribe: sinon.stub().resolves(true), + unsubscribe: sinon.stub().resolves(true) + } as unknown as IFilter; + + mockStore = { + queryWithOrderedCallback: sinon.stub().resolves(undefined) + } as unknown as IStore; + + ackManager = new AckManager({ + messageStore, + filter: mockFilter, + store: mockStore, + networkConfig: mockNetworkConfig + }); + + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + sinon.restore(); + }); + + describe("constructor", () => { + it("should initialize with provided parameters", () => { + expect(ackManager).to.be.instanceOf(AckManager); + }); + }); + + describe("start", () => { + it("should start filter and store ack managers", () => { + ackManager.start(); + + expect(clock.countTimers()).to.equal(1); + }); + + it("should be idempotent", () => { + ackManager.start(); + ackManager.start(); + + expect(clock.countTimers()).to.equal(1); + }); + }); + + describe("stop", () => { + it("should stop filter and store ack managers", async () => { + ackManager.start(); + await ackManager.stop(); + + expect(clock.countTimers()).to.equal(0); + }); + + it("should clear subscribed content topics", async () => { + await ackManager.subscribe("/test/1/clear/proto"); + await ackManager.stop(); + + const result = await ackManager.subscribe("/test/1/clear/proto"); + expect(result).to.be.true; + }); + + it("should handle stop without start", async () => { + await ackManager.stop(); + }); + }); + + describe("subscribe", () => { + it("should subscribe to new content topic", async () => { + const result = await ackManager.subscribe("/test/1/new/proto"); + + expect(result).to.be.true; + expect( + (mockFilter.subscribe as sinon.SinonStub).calledWith( + sinon.match.object, + sinon.match.func + ) + ).to.be.true; + }); + + it("should return true for already subscribed topic", async () => { + await ackManager.subscribe("/test/1/existing/proto"); + const result = await ackManager.subscribe("/test/1/existing/proto"); + + expect(result).to.be.true; + expect((mockFilter.subscribe as sinon.SinonStub).calledOnce).to.be.true; + }); + + it("should return true if at least one subscription succeeds", async () => { + (mockFilter.subscribe as sinon.SinonStub).resolves(false); + + const result = await ackManager.subscribe("/test/1/topic/proto"); + + expect(result).to.be.true; + }); + + it("should return true when filter fails but store succeeds", async () => { + (mockFilter.subscribe as sinon.SinonStub).resolves(false); + + const result = await ackManager.subscribe("/test/1/topic/proto"); + + expect(result).to.be.true; + }); + }); + + describe("FilterAckManager", () => { + beforeEach(() => { + ackManager.start(); + }); + + it("should handle message reception and acknowledgment", async () => { + await ackManager.subscribe("/test/1/topic/proto"); + const onMessageCallback = ( + mockFilter.subscribe as sinon.SinonStub + ).getCall(0).args[1]; + + await onMessageCallback(mockMessage); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should not add duplicate messages", async () => { + messageStore.add(mockMessage, { filterAck: false }); + await ackManager.subscribe("/test/1/topic/proto"); + + const onMessageCallback = ( + mockFilter.subscribe as sinon.SinonStub + ).getCall(0).args[1]; + await onMessageCallback(mockMessage); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should unsubscribe all decoders on stop", async () => { + await ackManager.subscribe("/test/1/topic1/proto"); + await ackManager.subscribe("/test/1/topic2/proto"); + + await ackManager.stop(); + + expect((mockFilter.unsubscribe as sinon.SinonStub).calledTwice).to.be + .true; + }); + }); + + describe("StoreAckManager", () => { + beforeEach(() => { + ackManager.start(); + }); + + it("should query store periodically", async () => { + await ackManager.subscribe("/test/1/topic/proto"); + + await clock.tickAsync(5000); + + expect( + (mockStore.queryWithOrderedCallback as sinon.SinonStub).calledWith( + sinon.match.array, + sinon.match.func, + sinon.match.object + ) + ).to.be.true; + }); + + it("should handle store query callback", async () => { + await ackManager.subscribe("/test/1/topic/proto"); + + await clock.tickAsync(5000); + + const callback = ( + mockStore.queryWithOrderedCallback as sinon.SinonStub + ).getCall(0).args[1]; + callback(mockMessage); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should not add duplicate messages from store", async () => { + messageStore.add(mockMessage, { storeAck: false }); + + await ackManager.subscribe("/test/1/topic/proto"); + await clock.tickAsync(5000); + + const callback = ( + mockStore.queryWithOrderedCallback as sinon.SinonStub + ).getCall(0).args[1]; + callback(mockMessage); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should stop interval on stop", async () => { + ackManager.start(); + await ackManager.stop(); + + expect(clock.countTimers()).to.equal(0); + }); + }); + + describe("integration scenarios", () => { + it("should handle complete lifecycle", async () => { + ackManager.start(); + + const result1 = await ackManager.subscribe("/test/1/topic1/proto"); + const result2 = await ackManager.subscribe("/test/1/topic2/proto"); + + expect(result1).to.be.true; + expect(result2).to.be.true; + + await ackManager.stop(); + + expect(clock.countTimers()).to.equal(0); + }); + + it("should handle multiple subscriptions to same topic", async () => { + ackManager.start(); + + const result1 = await ackManager.subscribe("/test/1/same/proto"); + const result2 = await ackManager.subscribe("/test/1/same/proto"); + + expect(result1).to.be.true; + expect(result2).to.be.true; + expect((mockFilter.subscribe as sinon.SinonStub).calledOnce).to.be.true; + }); + + it("should handle subscription after stop", async () => { + ackManager.start(); + await ackManager.stop(); + + const result = await ackManager.subscribe("/test/1/after-stop/proto"); + expect(result).to.be.true; + }); + }); + + describe("error handling", () => { + it("should handle filter subscription errors gracefully", async () => { + (mockFilter.subscribe as sinon.SinonStub).resolves(false); + + const result = await ackManager.subscribe("/test/1/error/proto"); + + expect(result).to.be.true; + }); + + it("should handle store query errors gracefully", async () => { + (mockStore.queryWithOrderedCallback as sinon.SinonStub).rejects( + new Error("Store query error") + ); + + ackManager.start(); + await ackManager.subscribe("/test/1/error/proto"); + + await clock.tickAsync(5000); + }); + + it("should handle unsubscribe errors gracefully", async () => { + ackManager.start(); + await ackManager.subscribe("/test/1/error/proto"); + + (mockFilter.unsubscribe as sinon.SinonStub).rejects( + new Error("Unsubscribe error") + ); + + try { + await ackManager.stop(); + } catch { + // Expected to throw + } + }); + }); +}); diff --git a/packages/sdk/src/messaging/message_store.spec.ts b/packages/sdk/src/messaging/message_store.spec.ts new file mode 100644 index 0000000000..ae5cf5f5c4 --- /dev/null +++ b/packages/sdk/src/messaging/message_store.spec.ts @@ -0,0 +1,349 @@ +import type { IDecodedMessage, ISendMessage } from "@waku/interfaces"; +import { expect } from "chai"; +import { beforeEach, describe, it } from "mocha"; + +import { MessageStore } from "./message_store.js"; + +describe("MessageStore", () => { + let messageStore: MessageStore; + let mockMessage: IDecodedMessage; + let mockSendMessage: ISendMessage; + + beforeEach(() => { + messageStore = new MessageStore(); + mockMessage = { + version: 1, + payload: new Uint8Array([1, 2, 3]), + contentTopic: "test-topic", + pubsubTopic: "test-pubsub", + timestamp: new Date(1000), + rateLimitProof: undefined, + ephemeral: false, + meta: undefined, + hash: new Uint8Array([4, 5, 6]), + hashStr: "test-hash-123" + }; + mockSendMessage = { + contentTopic: "test-topic", + payload: new Uint8Array([7, 8, 9]), + ephemeral: false + }; + }); + + describe("constructor", () => { + it("should create instance with default options", () => { + const store = new MessageStore(); + expect(store).to.be.instanceOf(MessageStore); + }); + + it("should create instance with custom resend interval", () => { + const customInterval = 10000; + const store = new MessageStore({ resendIntervalMs: customInterval }); + expect(store).to.be.instanceOf(MessageStore); + }); + }); + + describe("has", () => { + it("should return false for non-existent message", () => { + expect(messageStore.has("non-existent")).to.be.false; + }); + + it("should return true for added message", () => { + messageStore.add(mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should return true for pending message", async () => { + await messageStore.queue(mockSendMessage); + expect(messageStore.has("pending-hash")).to.be.false; + }); + }); + + describe("add", () => { + it("should add new message with default options", () => { + messageStore.add(mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should add message with custom options", () => { + messageStore.add(mockMessage, { filterAck: true, storeAck: false }); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should not add duplicate message", () => { + messageStore.add(mockMessage); + messageStore.add(mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should not add message if already exists", () => { + messageStore.add(mockMessage); + messageStore.add(mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + }); + + describe("queue", () => { + it("should queue message and return request ID", async () => { + const requestId = await messageStore.queue(mockSendMessage); + expect(typeof requestId).to.equal("string"); + expect(requestId.length).to.be.greaterThan(0); + }); + + it("should queue multiple messages with different request IDs", async () => { + const requestId1 = await messageStore.queue(mockSendMessage); + const requestId2 = await messageStore.queue(mockSendMessage); + expect(requestId1).to.not.equal(requestId2); + }); + }); + + describe("markFilterAck", () => { + it("should mark filter acknowledgment for existing message", () => { + messageStore.add(mockMessage); + messageStore.markFilterAck(mockMessage.hashStr); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should handle filter ack for non-existent message", () => { + expect(() => { + messageStore.markFilterAck("non-existent"); + }).to.not.throw(); + }); + + it("should handle filter ack for pending message", async () => { + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + messageStore.markFilterAck(mockMessage.hashStr); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + }); + + describe("markStoreAck", () => { + it("should mark store acknowledgment for existing message", () => { + messageStore.add(mockMessage); + messageStore.markStoreAck(mockMessage.hashStr); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should handle store ack for non-existent message", () => { + expect(() => { + messageStore.markStoreAck("non-existent"); + }).to.not.throw(); + }); + + it("should handle store ack for pending message", async () => { + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + messageStore.markStoreAck(mockMessage.hashStr); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + }); + + describe("markSent", () => { + it("should mark message as sent with valid request ID", async () => { + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should handle markSent with invalid request ID", () => { + expect(() => { + messageStore.markSent("invalid-request-id", mockMessage); + }).to.not.throw(); + }); + + it("should handle markSent with request ID without message", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const entry = (messageStore as any).pendingRequests.get(requestId); + if (entry) { + entry.messageRequest = undefined; + } + expect(() => { + messageStore.markSent(requestId, mockMessage); + }).to.not.throw(); + }); + + it("should set lastSentAt timestamp", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const sentMessage = { ...mockMessage, timestamp: new Date(2000) }; + messageStore.markSent(requestId, sentMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + }); + + describe("getMessagesToSend", () => { + it("should return empty array when no messages queued", () => { + const messages = messageStore.getMessagesToSend(); + expect(messages).to.deep.equal([]); + }); + + it("should return queued messages that need sending", async () => { + const customStore = new MessageStore({ resendIntervalMs: 0 }); + const requestId = await customStore.queue(mockSendMessage); + const messages = customStore.getMessagesToSend(); + expect(messages).to.have.length(1); + expect(messages[0].requestId).to.equal(requestId); + expect(messages[0].message).to.equal(mockSendMessage); + }); + + it("should not return acknowledged messages", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const entry = (messageStore as any).pendingRequests.get(requestId); + if (entry) { + entry.filterAck = true; + } + const messages = messageStore.getMessagesToSend(); + expect(messages).to.have.length(0); + }); + + it("should not return store acknowledged messages", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const entry = (messageStore as any).pendingRequests.get(requestId); + if (entry) { + entry.storeAck = true; + } + const messages = messageStore.getMessagesToSend(); + expect(messages).to.have.length(0); + }); + + it("should respect resend interval", async () => { + const customStore = new MessageStore({ resendIntervalMs: 10000 }); + const requestId = await customStore.queue(mockSendMessage); + + const entry = (customStore as any).pendingRequests.get(requestId); + if (entry) { + entry.lastSentAt = Date.now() - 5000; + } + + const messagesAfterShortTime = customStore.getMessagesToSend(); + expect(messagesAfterShortTime).to.have.length(0); + + if (entry) { + entry.lastSentAt = Date.now() - 15000; + } + + const messagesAfterLongTime = customStore.getMessagesToSend(); + expect(messagesAfterLongTime).to.have.length(1); + }); + + it("should return messages after resend interval", async () => { + const customStore = new MessageStore({ resendIntervalMs: 1000 }); + const requestId = await customStore.queue(mockSendMessage); + + const entry = (customStore as any).pendingRequests.get(requestId); + if (entry) { + entry.lastSentAt = Date.now() - 2000; + } + + const messages = customStore.getMessagesToSend(); + expect(messages).to.have.length(1); + }); + + it("should not return messages without messageRequest", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const entry = (messageStore as any).pendingRequests.get(requestId); + if (entry) { + entry.messageRequest = undefined; + } + const messages = messageStore.getMessagesToSend(); + expect(messages).to.have.length(0); + }); + }); + + describe("edge cases", () => { + it("should handle multiple acknowledgments for same message", () => { + messageStore.add(mockMessage); + messageStore.markFilterAck(mockMessage.hashStr); + messageStore.markStoreAck(mockMessage.hashStr); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should handle message received before sent", async () => { + messageStore.add(mockMessage); + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should handle empty message hash", () => { + const emptyHashMessage = { ...mockMessage, hashStr: "" }; + messageStore.add(emptyHashMessage); + expect(messageStore.has("")).to.be.true; + }); + + it("should handle very long message hash", () => { + const longHash = "a".repeat(1000); + const longHashMessage = { ...mockMessage, hashStr: longHash }; + messageStore.add(longHashMessage); + expect(messageStore.has(longHash)).to.be.true; + }); + + it("should handle special characters in hash", () => { + const specialHash = "test-hash-!@#$%^&*()_+-=[]{}|;':\",./<>?"; + const specialHashMessage = { ...mockMessage, hashStr: specialHash }; + messageStore.add(specialHashMessage); + expect(messageStore.has(specialHash)).to.be.true; + }); + }); + + describe("state transitions", () => { + it("should move message from pending to stored on ack", async () => { + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + messageStore.markFilterAck(mockMessage.hashStr); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + const pendingMessages = (messageStore as any).pendingMessages; + expect(pendingMessages.has(mockMessage.hashStr)).to.be.false; + }); + + it("should merge pending and stored message data", async () => { + messageStore.add(mockMessage, { filterAck: true }); + const requestId = await messageStore.queue(mockSendMessage); + messageStore.markSent(requestId, mockMessage); + messageStore.markStoreAck(mockMessage.hashStr); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + + it("should preserve acknowledgment state during transition", async () => { + const requestId = await messageStore.queue(mockSendMessage); + const entry = (messageStore as any).pendingRequests.get(requestId); + if (entry) { + entry.filterAck = true; + } + messageStore.markSent(requestId, mockMessage); + messageStore.markStoreAck(mockMessage.hashStr); + + expect(messageStore.has(mockMessage.hashStr)).to.be.true; + }); + }); + + describe("timing edge cases", () => { + it("should handle zero timestamp", async () => { + const zeroTimeMessage = { ...mockMessage, timestamp: new Date(0) }; + const requestId = await messageStore.queue(mockSendMessage); + expect(() => { + messageStore.markSent(requestId, zeroTimeMessage); + }).to.not.throw(); + }); + + it("should handle future timestamp", async () => { + const futureTime = new Date(Date.now() + 86400000); + const futureMessage = { ...mockMessage, timestamp: futureTime }; + const requestId = await messageStore.queue(mockSendMessage); + expect(() => { + messageStore.markSent(requestId, futureMessage); + }).to.not.throw(); + }); + + it("should handle very old timestamp", async () => { + const oldTime = new Date(0); + const oldMessage = { ...mockMessage, timestamp: oldTime }; + const requestId = await messageStore.queue(mockSendMessage); + expect(() => { + messageStore.markSent(requestId, oldMessage); + }).to.not.throw(); + }); + }); +}); diff --git a/packages/sdk/src/messaging/message_store.ts b/packages/sdk/src/messaging/message_store.ts index f065b75651..3d57482252 100644 --- a/packages/sdk/src/messaging/message_store.ts +++ b/packages/sdk/src/messaging/message_store.ts @@ -70,7 +70,7 @@ export class MessageStore { } public async queue(message: ISendMessage): Promise { - const requestId = uuidv4(); + const requestId = uuidv4(); // cspell:ignore uuidv4 this.pendingRequests.set(requestId.toString(), { messageRequest: message, diff --git a/packages/sdk/src/messaging/messaging.spec.ts b/packages/sdk/src/messaging/messaging.spec.ts index 209336634c..667110a300 100644 --- a/packages/sdk/src/messaging/messaging.spec.ts +++ b/packages/sdk/src/messaging/messaging.spec.ts @@ -1,48 +1,40 @@ -import { createDecoder, createEncoder } from "@waku/core"; import type { - IDecodedMessage, - IDecoder, - IEncoder, IFilter, ILightPush, - IMessage, + ISendMessage, IStore } from "@waku/interfaces"; -import { createRoutingInfo } from "@waku/utils"; import { utf8ToBytes } from "@waku/utils/bytes"; import { expect } from "chai"; import sinon from "sinon"; -import { - FilterAckManager, - MessageStore, - Messaging, - StoreAckManager -} from "./messaging.js"; +import { MessageStore } from "./message_store.js"; +import { Messaging } from "./messaging.js"; const testContentTopic = "/test/1/waku-messaging/utf8"; const testNetworkconfig = { clusterId: 0, numShardsInCluster: 9 }; -const testRoutingInfo = createRoutingInfo(testNetworkconfig, { - contentTopic: testContentTopic -}); describe("MessageStore", () => { it("queues, marks sent and acks", async () => { - const encoder = createEncoder({ - contentTopic: testContentTopic, - routingInfo: testRoutingInfo - }); const store = new MessageStore({ resendIntervalMs: 1 }); - const msg: IMessage = { payload: utf8ToBytes("hello") }; + const msg: ISendMessage = { + contentTopic: testContentTopic, + payload: utf8ToBytes("hello") + }; - const hash = await store.queue(encoder as IEncoder, msg); + const hash = await store.queue(msg); expect(hash).to.be.a("string"); if (!hash) return; - expect(store.has(hash)).to.be.true; - store.markSent(hash); + + const mockDecodedMessage = { + hashStr: hash, + timestamp: new Date() + } as any; + + store.markSent(hash, mockDecodedMessage); store.markFilterAck(hash); store.markStoreAck(hash); @@ -50,94 +42,9 @@ describe("MessageStore", () => { expect(toSend.length).to.eq(0); }); }); -describe("FilterAckManager", () => { - it("subscribes and marks filter ack on messages", async () => { - const store = new MessageStore(); - const filter: IFilter = { - multicodec: "filter", - start: sinon.stub().resolves(), - stop: sinon.stub().resolves(), - subscribe: sinon.stub().callsFake(async (_dec, cb: any) => { - const decoder = createDecoder(testContentTopic, testRoutingInfo); - const proto = await decoder.fromProtoObj(decoder.pubsubTopic, { - payload: utf8ToBytes("x"), - contentTopic: testContentTopic, - version: 0, - timestamp: BigInt(Date.now()), - meta: undefined, - rateLimitProof: undefined, - ephemeral: false - } as any); - if (proto) { - await cb({ ...proto, hashStr: "hash" } as IDecodedMessage); - } - return true; - }), - unsubscribe: sinon.stub().resolves(true), - unsubscribeAll: sinon.stub() - } as unknown as IFilter; - - const mgr = new FilterAckManager(store, filter); - const encoder = createEncoder({ - contentTopic: testContentTopic, - routingInfo: testRoutingInfo - }); - - const subscribed = await mgr.subscribe({ - ...encoder, - fromWireToProtoObj: (b: Uint8Array) => - createDecoder(testContentTopic, testRoutingInfo).fromWireToProtoObj(b), - fromProtoObj: (pubsub: string, p: any) => - createDecoder(testContentTopic, testRoutingInfo).fromProtoObj(pubsub, p) - } as unknown as IDecoder & IEncoder); - expect(subscribed).to.be.true; - }); -}); - -describe("StoreAckManager", () => { - it("queries and marks store ack", async () => { - const store = new MessageStore(); - const decoder = createDecoder(testContentTopic, testRoutingInfo); - const d = decoder as IDecoder & IEncoder; - - const mockStore: IStore = { - multicodec: "store", - createCursor: sinon.stub() as any, - queryGenerator: sinon.stub() as any, - queryWithOrderedCallback: sinon - .stub() - .callsFake(async (_decs: any, cb: any) => { - const proto = await decoder.fromProtoObj(decoder.pubsubTopic, { - payload: utf8ToBytes("x"), - contentTopic: testContentTopic, - version: 0, - timestamp: BigInt(Date.now()), - meta: undefined, - rateLimitProof: undefined, - ephemeral: false - } as any); - if (proto) { - await cb({ ...proto, hashStr: "hash2" }); - } - }), - queryWithPromiseCallback: sinon.stub() as any - } as unknown as IStore; - - const mgr = new StoreAckManager(store, mockStore); - await mgr.subscribe(d); - mgr.start(); - await new Promise((r) => setTimeout(r, 5)); - mgr.stop(); - }); -}); describe("Messaging", () => { it("queues and sends via light push, marks sent", async () => { - const encoder = createEncoder({ - contentTopic: testContentTopic, - routingInfo: testRoutingInfo - }); - const lightPush: ILightPush = { multicodec: "lightpush", start: () => {}, @@ -162,9 +69,19 @@ describe("Messaging", () => { queryWithPromiseCallback: sinon.stub().resolves() } as unknown as IStore; - const messaging = new Messaging({ lightPush, filter, store }); + const messaging = new Messaging({ + lightPush, + filter, + store, + networkConfig: testNetworkconfig + }); + + const message: ISendMessage = { + contentTopic: testContentTopic, + payload: utf8ToBytes("hello") + }; - await messaging.send(encoder, { payload: utf8ToBytes("hello") }); + await messaging.send(message); expect((lightPush.send as any).calledOnce).to.be.true; }); }); diff --git a/packages/sdk/src/messaging/sender.spec.ts b/packages/sdk/src/messaging/sender.spec.ts new file mode 100644 index 0000000000..4f9082d019 --- /dev/null +++ b/packages/sdk/src/messaging/sender.spec.ts @@ -0,0 +1,144 @@ +import type { ILightPush, ISendMessage, NetworkConfig } from "@waku/interfaces"; +import { expect } from "chai"; +import { afterEach, beforeEach, describe, it } from "mocha"; +import sinon from "sinon"; + +import type { AckManager } from "./ack_manager.js"; +import type { MessageStore } from "./message_store.js"; +import { Sender } from "./sender.js"; + +describe("Sender", () => { + let sender: Sender; + let mockMessageStore: MessageStore; + let mockLightPush: ILightPush; + let mockAckManager: AckManager; + let mockNetworkConfig: NetworkConfig; + + beforeEach(() => { + mockMessageStore = { + queue: sinon.stub(), + getMessagesToSend: sinon.stub(), + markSent: sinon.stub() + } as any; + + mockLightPush = { + send: sinon.stub() + } as any; + + mockAckManager = { + subscribe: sinon.stub() + } as any; + + mockNetworkConfig = { + clusterId: 1, + shardId: 0 + } as any; + + sender = new Sender({ + messageStore: mockMessageStore, + lightPush: mockLightPush, + ackManager: mockAckManager, + networkConfig: mockNetworkConfig + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("constructor", () => { + it("should initialize with provided parameters", () => { + expect(sender).to.be.instanceOf(Sender); + }); + }); + + describe("start", () => { + it("should set up background sending interval", () => { + const setIntervalSpy = sinon.spy(global, "setInterval"); + + sender.start(); + + expect(setIntervalSpy.calledWith(sinon.match.func, 1000)).to.be.true; + }); + + it("should create multiple intervals when called multiple times", () => { + const setIntervalSpy = sinon.spy(global, "setInterval"); + + sender.start(); + sender.start(); + + expect(setIntervalSpy.calledTwice).to.be.true; + }); + }); + + describe("stop", () => { + it("should clear interval when called", () => { + const clearIntervalSpy = sinon.spy(global, "clearInterval"); + + sender.start(); + sender.stop(); + + expect(clearIntervalSpy.called).to.be.true; + }); + + it("should handle multiple stop calls gracefully", () => { + const clearIntervalSpy = sinon.spy(global, "clearInterval"); + + sender.start(); + sender.stop(); + sender.stop(); + + expect(clearIntervalSpy.calledOnce).to.be.true; + }); + + it("should handle stop without start", () => { + expect(() => sender.stop()).to.not.throw(); + }); + }); + + describe("send", () => { + const mockMessage: ISendMessage = { + contentTopic: "test-topic", + payload: new Uint8Array([1, 2, 3]), + ephemeral: false + }; + + const mockRequestId = "test-request-id"; + + it("should handle messageStore.queue failure", async () => { + const error = new Error("Queue failed"); + (mockMessageStore.queue as sinon.SinonStub).rejects(error); + + try { + await sender.send(mockMessage); + expect.fail("Expected error to be thrown"); + } catch (e: any) { + expect(e).to.equal(error); + } + }); + + it("should handle ackManager.subscribe failure", async () => { + const error = new Error("Subscribe failed"); + (mockAckManager.subscribe as sinon.SinonStub).rejects(error); + (mockMessageStore.queue as sinon.SinonStub).resolves(mockRequestId); + + try { + await sender.send(mockMessage); + expect.fail("Expected error to be thrown"); + } catch (e: any) { + expect(e).to.equal(error); + } + }); + }); + + describe("backgroundSend", () => { + it("should handle empty pending messages", async () => { + (mockMessageStore.getMessagesToSend as sinon.SinonStub).returns([]); + + await sender["backgroundSend"](); + + expect((mockMessageStore.getMessagesToSend as sinon.SinonStub).called).to + .be.true; + }); + }); +}); From e39c6c94bf71aabee942b3c94214c03288745afe Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 8 Oct 2025 23:40:49 +0200 Subject: [PATCH 15/16] address review --- packages/sdk/src/light_push/light_push.ts | 2 +- packages/sdk/src/messaging/ack_manager.ts | 30 +++++++++++++++-------- packages/sdk/src/messaging/sender.spec.ts | 4 +-- packages/sdk/src/messaging/sender.ts | 7 +++++- packages/sdk/src/waku/waku.ts | 2 +- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/sdk/src/light_push/light_push.ts b/packages/sdk/src/light_push/light_push.ts index 05a94aaf91..972fdcdc16 100644 --- a/packages/sdk/src/light_push/light_push.ts +++ b/packages/sdk/src/light_push/light_push.ts @@ -87,7 +87,7 @@ export class LightPush implements ILightPush { protocol: options.useLegacy ? "light-push-v2" : Protocols.LightPush, pubsubTopic: encoder.pubsubTopic }); - peerIds = peerIds.slice(0, options.numPeersToUse); + peerIds = peerIds?.slice(0, options.numPeersToUse); const coreResults = peerIds?.length > 0 diff --git a/packages/sdk/src/messaging/ack_manager.ts b/packages/sdk/src/messaging/ack_manager.ts index 8551cbc4b3..004f798cb1 100644 --- a/packages/sdk/src/messaging/ack_manager.ts +++ b/packages/sdk/src/messaging/ack_manager.ts @@ -18,6 +18,9 @@ type AckManagerConstructorParams = { networkConfig: NetworkConfig; }; +const DEFAULT_QUERY_INTERVAL = 5000; +const QUERY_TIME_WINDOW_MS = 60 * 60 * 1000; + export class AckManager implements IAckManager { private readonly messageStore: MessageStore; private readonly filterAckManager: FilterAckManager; @@ -25,6 +28,7 @@ export class AckManager implements IAckManager { private readonly networkConfig: NetworkConfig; private readonly subscribedContentTopics: Set = new Set(); + private readonly subscribingAttempts: Set = new Set(); public constructor(params: AckManagerConstructorParams) { this.messageStore = params.messageStore; @@ -50,11 +54,15 @@ export class AckManager implements IAckManager { } public async subscribe(contentTopic: string): Promise { - if (this.subscribedContentTopics.has(contentTopic)) { + if ( + this.subscribedContentTopics.has(contentTopic) || + this.subscribingAttempts.has(contentTopic) + ) { return true; } - this.subscribedContentTopics.add(contentTopic); + this.subscribingAttempts.add(contentTopic); + const decoder = createDecoder( contentTopic, createRoutingInfo(this.networkConfig, { @@ -62,12 +70,14 @@ export class AckManager implements IAckManager { }) ); - return ( - await Promise.all([ - this.filterAckManager.subscribe(decoder), - this.storeAckManager.subscribe(decoder) - ]) - ).some((success) => success); + const promises = await Promise.all([ + this.filterAckManager.subscribe(decoder), + this.storeAckManager.subscribe(decoder) + ]); + + this.subscribedContentTopics.add(contentTopic); + this.subscribingAttempts.delete(contentTopic); + return promises.some((success) => success); } } @@ -128,7 +138,7 @@ class StoreAckManager { this.interval = setInterval(() => { void this.query(); - }, 5000); + }, DEFAULT_QUERY_INTERVAL); } public stop(): void { @@ -157,7 +167,7 @@ class StoreAckManager { this.messageStore.markStoreAck(message.hashStr); }, { - timeStart: new Date(Date.now() - 60 * 60 * 1000), + timeStart: new Date(Date.now() - QUERY_TIME_WINDOW_MS), timeEnd: new Date() } ); diff --git a/packages/sdk/src/messaging/sender.spec.ts b/packages/sdk/src/messaging/sender.spec.ts index 4f9082d019..83306e95b4 100644 --- a/packages/sdk/src/messaging/sender.spec.ts +++ b/packages/sdk/src/messaging/sender.spec.ts @@ -61,13 +61,13 @@ describe("Sender", () => { expect(setIntervalSpy.calledWith(sinon.match.func, 1000)).to.be.true; }); - it("should create multiple intervals when called multiple times", () => { + it("should not create multiple intervals when called multiple times", () => { const setIntervalSpy = sinon.spy(global, "setInterval"); sender.start(); sender.start(); - expect(setIntervalSpy.calledTwice).to.be.true; + expect(setIntervalSpy.calledOnce).to.be.true; }); }); diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index b32978d8b8..8dbb1c4e8e 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -17,6 +17,8 @@ type SenderConstructorParams = { networkConfig: NetworkConfig; }; +const DEFAULT_SEND_INTERVAL = 1000; + export class Sender { private readonly messageStore: MessageStore; private readonly lightPush: ILightPush; @@ -35,7 +37,10 @@ export class Sender { } public start(): void { - this.sendInterval = setInterval(() => void this.backgroundSend(), 1000); + this.sendInterval = setInterval( + () => void this.backgroundSend(), + DEFAULT_SEND_INTERVAL + ); } public stop(): void { diff --git a/packages/sdk/src/waku/waku.ts b/packages/sdk/src/waku/waku.ts index 4e8c07d7fb..c4a4c27b42 100644 --- a/packages/sdk/src/waku/waku.ts +++ b/packages/sdk/src/waku/waku.ts @@ -67,7 +67,7 @@ export class WakuNode implements IWaku { private readonly connectionManager: ConnectionManager; private readonly peerManager: PeerManager; private readonly healthIndicator: HealthIndicator; - private readonly messaging: Messaging | null = null; + private messaging: Messaging | null = null; public constructor( options: CreateNodeOptions, From 974fb21f0b7ea45d74c87644947e61ac20344372 Mon Sep 17 00:00:00 2001 From: Sasha Date: Wed, 8 Oct 2025 23:41:53 +0200 Subject: [PATCH 16/16] fix start --- packages/sdk/src/messaging/sender.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk/src/messaging/sender.ts b/packages/sdk/src/messaging/sender.ts index 8dbb1c4e8e..6d093d8bc2 100644 --- a/packages/sdk/src/messaging/sender.ts +++ b/packages/sdk/src/messaging/sender.ts @@ -37,6 +37,10 @@ export class Sender { } public start(): void { + if (this.sendInterval) { + return; + } + this.sendInterval = setInterval( () => void this.backgroundSend(), DEFAULT_SEND_INTERVAL