mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-02-09 08:23:06 +00:00
* feat(sds): messages with lost deps are delivered This is to re-enable participation in the SDS protocol. Meaning the received message with missing dependencies becomes part of the causal history, re-enabling acknowledgements. * fix(sds): avoid overflow in message history storage * feat(reliable-channel): Emit a "Synced" Status with message counts Return a "synced" or "syncing" status on `ReliableChannel.status` that let the developer know whether messages are missing, and if so, how many. * fix: clean up subscriptions, intervals and timeouts when stopping # Conflicts: # packages/sdk/src/reliable_channel/reliable_channel.ts * chore: extract random timeout * fix rebase * revert listener changes * typo * Ensuring no inconsistency on missing message * test: streamline, stop channels * clear sync status sets when stopping channel * prevent sync status event spam * test: improve naming * try/catch for callback * encapsulate/simplify reliable channel API * sanity checks * test: ensure sync status cleanup
329 lines
8.4 KiB
TypeScript
329 lines
8.4 KiB
TypeScript
import { Peer, PeerId } from "@libp2p/interface";
|
|
import {
|
|
createEncoder,
|
|
Encoder,
|
|
LightPushCodec,
|
|
LightPushCodecV2
|
|
} from "@waku/core";
|
|
import { Libp2p, LightPushError, LightPushStatusCode } from "@waku/interfaces";
|
|
import { createRoutingInfo } from "@waku/utils";
|
|
import { utf8ToBytes } from "@waku/utils/bytes";
|
|
import { expect } from "chai";
|
|
import { afterEach } from "mocha";
|
|
import sinon, { SinonSpy } from "sinon";
|
|
|
|
import { PeerManager } from "../peer_manager/index.js";
|
|
|
|
import { LightPush } from "./light_push.js";
|
|
|
|
const testContentTopic = "/test/1/waku-light-push/utf8";
|
|
const testRoutingInfo = createRoutingInfo(
|
|
{
|
|
clusterId: 0,
|
|
numShardsInCluster: 7
|
|
},
|
|
{ contentTopic: testContentTopic }
|
|
);
|
|
|
|
describe("LightPush SDK", () => {
|
|
let libp2p: Libp2p;
|
|
let encoder: Encoder;
|
|
let lightPush: LightPush;
|
|
|
|
beforeEach(() => {
|
|
libp2p = mockLibp2p();
|
|
encoder = createEncoder({
|
|
contentTopic: testContentTopic,
|
|
routingInfo: testRoutingInfo
|
|
});
|
|
lightPush = mockLightPush({ libp2p });
|
|
});
|
|
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
it("should fail to send if no connected peers found", async () => {
|
|
const result = await lightPush.send(encoder, {
|
|
payload: utf8ToBytes("test")
|
|
});
|
|
const failures = result.failures ?? [];
|
|
|
|
expect(failures.length).to.be.eq(1);
|
|
expect(failures.some((v) => v.error === LightPushError.NO_PEER_AVAILABLE))
|
|
.to.be.true;
|
|
});
|
|
|
|
it("should send to specified number of peers of used peers", async () => {
|
|
libp2p = mockLibp2p({
|
|
peers: [mockPeer("1"), mockPeer("2"), mockPeer("3"), mockPeer("4")]
|
|
});
|
|
|
|
lightPush = mockLightPush({ libp2p, numPeersToUse: 2 });
|
|
let sendSpy = sinon.spy(
|
|
(_encoder: any, _message: any, peerId: PeerId) =>
|
|
Promise.resolve({ success: peerId }) as any
|
|
);
|
|
lightPush["protocol"].send = sendSpy;
|
|
|
|
let result = await lightPush.send(encoder, {
|
|
payload: utf8ToBytes("test")
|
|
});
|
|
|
|
expect(sendSpy.calledTwice, "1").to.be.true;
|
|
expect(result.successes?.length, "2").to.be.eq(2);
|
|
|
|
// check if setting another value works
|
|
lightPush = mockLightPush({ libp2p, numPeersToUse: 3 });
|
|
sendSpy = sinon.spy(
|
|
(_encoder: any, _message: any, peerId: PeerId) =>
|
|
Promise.resolve({ success: peerId }) as any
|
|
);
|
|
lightPush["protocol"].send = sendSpy;
|
|
|
|
result = await lightPush.send(encoder, { payload: utf8ToBytes("test") });
|
|
|
|
expect(sendSpy.calledThrice, "3").to.be.true;
|
|
expect(result.successes?.length, "4").to.be.eq(3);
|
|
});
|
|
|
|
it("should retry on complete failure if specified", async () => {
|
|
libp2p = mockLibp2p({
|
|
peers: [mockPeer("1"), mockPeer("2")]
|
|
});
|
|
|
|
lightPush = mockLightPush({ libp2p });
|
|
const sendSpy = sinon.spy((_encoder: any, _message: any, _peerId: PeerId) =>
|
|
Promise.resolve({ failure: { error: "problem" } })
|
|
);
|
|
lightPush["protocol"].send = sendSpy as any;
|
|
|
|
const retryPushSpy = (lightPush as any)["retryManager"].push as SinonSpy;
|
|
const result = await lightPush.send(
|
|
encoder,
|
|
{ payload: utf8ToBytes("test") },
|
|
{ autoRetry: true }
|
|
);
|
|
|
|
expect(retryPushSpy.callCount).to.be.eq(1);
|
|
expect(result.failures?.length).to.be.eq(2);
|
|
});
|
|
|
|
it("should not retry if at least one success", async () => {
|
|
libp2p = mockLibp2p({
|
|
peers: [mockPeer("1"), mockPeer("2")]
|
|
});
|
|
|
|
lightPush = mockLightPush({ libp2p });
|
|
const sendSpy = sinon.spy(
|
|
(_encoder: any, _message: any, peerId: PeerId) => {
|
|
if (peerId.toString() === "1") {
|
|
return Promise.resolve({ success: peerId });
|
|
}
|
|
|
|
return Promise.resolve({ failure: { error: "problem" } });
|
|
}
|
|
);
|
|
lightPush["protocol"].send = sendSpy as any;
|
|
const retryPushSpy = (lightPush as any)["retryManager"].push as SinonSpy;
|
|
|
|
const result = await lightPush.send(
|
|
encoder,
|
|
{ payload: utf8ToBytes("test") },
|
|
{ autoRetry: true }
|
|
);
|
|
|
|
expect(retryPushSpy.callCount).to.be.eq(0);
|
|
expect(result.successes?.length).to.be.eq(1);
|
|
expect(result.failures?.length).to.be.eq(1);
|
|
});
|
|
|
|
describe("v3 protocol support", () => {
|
|
it("should work with v3 peers", async () => {
|
|
libp2p = mockLibp2p({
|
|
peers: [mockV3Peer("1"), mockV3Peer("2")]
|
|
});
|
|
});
|
|
|
|
it("should work with mixed v2 and v3 peers", async () => {
|
|
libp2p = mockLibp2p({
|
|
peers: [mockV2AndV3Peer("1"), mockPeer("2"), mockV3Peer("3")]
|
|
});
|
|
|
|
// Mock responses for different protocol versions
|
|
const v3Response = mockV3SuccessResponse(5);
|
|
const v2Response = mockV2SuccessResponse();
|
|
const v3ErrorResponse = mockV3ErrorResponse(
|
|
LightPushStatusCode.PAYLOAD_TOO_LARGE
|
|
);
|
|
const v2ErrorResponse = mockV2ErrorResponse("Message too large");
|
|
|
|
expect(v3Response.statusCode).to.eq(LightPushStatusCode.SUCCESS);
|
|
expect(v3Response.relayPeerCount).to.eq(5);
|
|
expect(v2Response.isSuccess).to.be.true;
|
|
expect(v3ErrorResponse.statusCode).to.eq(
|
|
LightPushStatusCode.PAYLOAD_TOO_LARGE
|
|
);
|
|
expect(v2ErrorResponse.isSuccess).to.be.false;
|
|
});
|
|
|
|
it("should handle v3 RLN errors", async () => {
|
|
const v3RLNError = mockV3RLNErrorResponse();
|
|
const v2RLNError = mockV2RLNErrorResponse();
|
|
|
|
expect(v3RLNError.statusCode).to.eq(LightPushStatusCode.NO_RLN_PROOF);
|
|
expect(v3RLNError.statusDesc).to.include("RLN proof generation failed");
|
|
expect(v2RLNError.info).to.include("RLN proof generation failed");
|
|
});
|
|
});
|
|
});
|
|
|
|
type MockLibp2pOptions = {
|
|
peers?: Peer[];
|
|
};
|
|
|
|
function mockLibp2p(options?: MockLibp2pOptions): Libp2p {
|
|
const peers = options?.peers || [];
|
|
const peerStore = {
|
|
get: (id: any) => {
|
|
const peer = peers.find((p) => p.id === id);
|
|
if (peer) {
|
|
return Promise.resolve({
|
|
...peer,
|
|
protocols: peer.protocols || [LightPushCodec]
|
|
});
|
|
}
|
|
return Promise.resolve(undefined);
|
|
}
|
|
};
|
|
|
|
return {
|
|
peerStore,
|
|
getPeers: () => peers.map((p) => p.id),
|
|
components: {
|
|
events: new EventTarget(),
|
|
connectionManager: {
|
|
getConnections: () => []
|
|
} as any,
|
|
peerStore
|
|
}
|
|
} as unknown as Libp2p;
|
|
}
|
|
|
|
type MockLightPushOptions = {
|
|
libp2p: Libp2p;
|
|
pubsubTopics?: string[];
|
|
numPeersToUse?: number;
|
|
};
|
|
|
|
function mockLightPush(options: MockLightPushOptions): LightPush {
|
|
const lightPush = new LightPush({
|
|
peerManager: {
|
|
getPeers: () =>
|
|
options.libp2p
|
|
.getPeers()
|
|
.slice(0, options.numPeersToUse || options.libp2p.getPeers().length)
|
|
} as unknown as PeerManager,
|
|
libp2p: options.libp2p,
|
|
options: {
|
|
numPeersToUse: options.numPeersToUse
|
|
}
|
|
});
|
|
|
|
(lightPush as any)["retryManager"] = {
|
|
push: sinon.spy()
|
|
};
|
|
|
|
return lightPush;
|
|
}
|
|
|
|
function mockPeer(id: string, protocols: string[] = [LightPushCodec]): Peer {
|
|
return {
|
|
id: { toString: () => id } as PeerId,
|
|
protocols: protocols,
|
|
metadata: new Map(),
|
|
addresses: [],
|
|
tags: new Map()
|
|
};
|
|
}
|
|
|
|
// V3-specific mock functions
|
|
function mockV3Peer(id: string): Peer {
|
|
return mockPeer(id, [LightPushCodec]);
|
|
}
|
|
|
|
function mockV2AndV3Peer(id: string): Peer {
|
|
return mockPeer(id, [LightPushCodec, LightPushCodecV2]);
|
|
}
|
|
|
|
function mockV3SuccessResponse(relayPeerCount?: number): {
|
|
statusCode: LightPushStatusCode;
|
|
statusDesc: string;
|
|
relayPeerCount?: number;
|
|
isSuccess: boolean;
|
|
} {
|
|
return {
|
|
statusCode: LightPushStatusCode.SUCCESS,
|
|
statusDesc: "Message sent successfully",
|
|
relayPeerCount,
|
|
isSuccess: true
|
|
};
|
|
}
|
|
|
|
function mockV3ErrorResponse(
|
|
statusCode: LightPushStatusCode,
|
|
statusDesc?: string
|
|
): {
|
|
statusCode: LightPushStatusCode;
|
|
statusDesc: string;
|
|
isSuccess: boolean;
|
|
} {
|
|
return {
|
|
statusCode,
|
|
statusDesc: statusDesc || "Error occurred",
|
|
isSuccess: false
|
|
};
|
|
}
|
|
|
|
function mockV2SuccessResponse(): {
|
|
isSuccess: boolean;
|
|
info: string;
|
|
} {
|
|
return {
|
|
isSuccess: true,
|
|
info: "Message sent successfully"
|
|
};
|
|
}
|
|
|
|
function mockV2ErrorResponse(info?: string): {
|
|
isSuccess: boolean;
|
|
info: string;
|
|
} {
|
|
return {
|
|
isSuccess: false,
|
|
info: info || "Error occurred"
|
|
};
|
|
}
|
|
|
|
function mockV3RLNErrorResponse(): {
|
|
statusCode: LightPushStatusCode;
|
|
statusDesc: string;
|
|
isSuccess: boolean;
|
|
} {
|
|
return {
|
|
statusCode: LightPushStatusCode.NO_RLN_PROOF,
|
|
statusDesc: "RLN proof generation failed",
|
|
isSuccess: false
|
|
};
|
|
}
|
|
|
|
function mockV2RLNErrorResponse(): {
|
|
isSuccess: boolean;
|
|
info: string;
|
|
} {
|
|
return {
|
|
isSuccess: false,
|
|
info: "RLN proof generation failed"
|
|
};
|
|
}
|