mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-04 23:03:07 +00:00
feat!: re-architect connection manager (#2445)
* remove public pubsub field and redundant util * add hangUp and improve dial operations, improve keepAliveManager and remove unused method, move utils and add tests * improve public dial method to start keep alive checks * move dial method * implement discovery dialer * implement discovery dialer with queue with tests * add discovery dialer e2e tests, change local discovery log tag, update other tests * remove comment * add issue link, remove only * implement shard reader component * create evetns module, remove unused connection manager events and related tests * implement network indicator * implement connection limiter, change public API of connection manager, implement recovery strategy * decouple keep alive maanger * add connection manager js-doc * refactor keep alive manager, cover with tests * add tests for connection manager main facade * add tests for connection limiter * add e2e tests for connection manager modules pass js-waku config during test node init remove dns discovery for js-waku * restructure dialing tests * address last e2e tests * address review * add logging for main methods * decouple pure dialer class, update network monitor with specific metrics * remove console.log * remove usage of protocols * update sdk package tests * add connect await promise * add debug for e2e tests * enable only packages tests * use only one file * revert debugging * up interface for netwrok manager * add logs * add more logs * add more logs * add another logs * remove .only * remove log statements * skip the test with follow up
This commit is contained in:
parent
bfda249aa6
commit
c7682ea67c
1068
packages/core/src/lib/connection_manager/connection_limiter.spec.ts
Normal file
1068
packages/core/src/lib/connection_manager/connection_limiter.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
201
packages/core/src/lib/connection_manager/connection_limiter.ts
Normal file
201
packages/core/src/lib/connection_manager/connection_limiter.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Peer, PeerId } from "@libp2p/interface";
|
||||
import {
|
||||
ConnectionManagerOptions,
|
||||
IWakuEventEmitter,
|
||||
Libp2p,
|
||||
Tags
|
||||
} from "@waku/interfaces";
|
||||
import { Logger } from "@waku/utils";
|
||||
|
||||
import { Dialer } from "./dialer.js";
|
||||
import { NetworkMonitor } from "./network_monitor.js";
|
||||
|
||||
const log = new Logger("connection-limiter");
|
||||
|
||||
type Libp2pEventHandler<T> = (e: CustomEvent<T>) => void;
|
||||
|
||||
type ConnectionLimiterConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
events: IWakuEventEmitter;
|
||||
dialer: Dialer;
|
||||
networkMonitor: NetworkMonitor;
|
||||
options: ConnectionManagerOptions;
|
||||
};
|
||||
|
||||
interface IConnectionLimiter {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is responsible for limiting the number of connections to peers.
|
||||
* It also dials all known peers because libp2p might have emitted `peer:discovery` before initialization
|
||||
* and listen to `peer:connect` and `peer:disconnect` events to manage connections.
|
||||
*/
|
||||
export class ConnectionLimiter implements IConnectionLimiter {
|
||||
private readonly libp2p: Libp2p;
|
||||
private readonly events: IWakuEventEmitter;
|
||||
private readonly networkMonitor: NetworkMonitor;
|
||||
private readonly dialer: Dialer;
|
||||
|
||||
private readonly options: ConnectionManagerOptions;
|
||||
|
||||
public constructor(options: ConnectionLimiterConstructorOptions) {
|
||||
this.libp2p = options.libp2p;
|
||||
this.events = options.events;
|
||||
this.networkMonitor = options.networkMonitor;
|
||||
this.dialer = options.dialer;
|
||||
|
||||
this.options = options.options;
|
||||
|
||||
this.onWakuConnectionEvent = this.onWakuConnectionEvent.bind(this);
|
||||
this.onConnectedEvent = this.onConnectedEvent.bind(this);
|
||||
this.onDisconnectedEvent = this.onDisconnectedEvent.bind(this);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// dial all known peers because libp2p might have emitted `peer:discovery` before initialization
|
||||
void this.dialPeersFromStore();
|
||||
|
||||
this.events.addEventListener("waku:connection", this.onWakuConnectionEvent);
|
||||
|
||||
this.libp2p.addEventListener(
|
||||
"peer:connect",
|
||||
this.onConnectedEvent as Libp2pEventHandler<PeerId>
|
||||
);
|
||||
|
||||
/**
|
||||
* NOTE: Event is not being emitted on closing nor losing a connection.
|
||||
* @see https://github.com/libp2p/js-libp2p/issues/939
|
||||
* @see https://github.com/status-im/js-waku/issues/252
|
||||
*
|
||||
* >This event will be triggered anytime we are disconnected from another peer,
|
||||
* >regardless of the circumstances of that disconnection.
|
||||
* >If we happen to have multiple connections to a peer,
|
||||
* >this event will **only** be triggered when the last connection is closed.
|
||||
* @see https://github.com/libp2p/js-libp2p/blob/bad9e8c0ff58d60a78314077720c82ae331cc55b/doc/API.md?plain=1#L2100
|
||||
*/
|
||||
this.libp2p.addEventListener(
|
||||
"peer:disconnect",
|
||||
this.onDisconnectedEvent as Libp2pEventHandler<PeerId>
|
||||
);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.events.removeEventListener(
|
||||
"waku:connection",
|
||||
this.onWakuConnectionEvent
|
||||
);
|
||||
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:connect",
|
||||
this.onConnectedEvent as Libp2pEventHandler<PeerId>
|
||||
);
|
||||
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:disconnect",
|
||||
this.onDisconnectedEvent as Libp2pEventHandler<PeerId>
|
||||
);
|
||||
}
|
||||
|
||||
private onWakuConnectionEvent(): void {
|
||||
if (this.networkMonitor.isBrowserConnected()) {
|
||||
void this.dialPeersFromStore();
|
||||
}
|
||||
}
|
||||
|
||||
private async onConnectedEvent(evt: CustomEvent<PeerId>): Promise<void> {
|
||||
log.info(`Connected to peer ${evt.detail.toString()}`);
|
||||
|
||||
const peerId = evt.detail;
|
||||
|
||||
const tags = await this.getTagsForPeer(peerId);
|
||||
const isBootstrap = tags.includes(Tags.BOOTSTRAP);
|
||||
|
||||
if (!isBootstrap) {
|
||||
log.info(
|
||||
`Connected to peer ${peerId.toString()} is not a bootstrap peer`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.hasMoreThanMaxBootstrapConnections()) {
|
||||
log.info(
|
||||
`Connected to peer ${peerId.toString()} and node has more than max bootstrap connections ${this.options.maxBootstrapPeers}. Dropping connection.`
|
||||
);
|
||||
await this.libp2p.hangUp(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
private async onDisconnectedEvent(): Promise<void> {
|
||||
if (this.libp2p.getConnections().length === 0) {
|
||||
log.info(`No connections, dialing peers from store`);
|
||||
await this.dialPeersFromStore();
|
||||
}
|
||||
}
|
||||
|
||||
private async dialPeersFromStore(): Promise<void> {
|
||||
log.info(`Dialing peers from store`);
|
||||
|
||||
const allPeers = await this.libp2p.peerStore.all();
|
||||
const allConnections = this.libp2p.getConnections();
|
||||
|
||||
log.info(
|
||||
`Found ${allPeers.length} peers in store, and found ${allConnections.length} connections`
|
||||
);
|
||||
|
||||
const promises = allPeers
|
||||
.filter((p) => !allConnections.some((c) => c.remotePeer.equals(p.id)))
|
||||
.map((p) => this.dialer.dial(p.id));
|
||||
|
||||
try {
|
||||
log.info(`Dialing ${promises.length} peers from store`);
|
||||
await Promise.all(promises);
|
||||
log.info(`Dialed ${promises.length} peers from store`);
|
||||
} catch (error) {
|
||||
log.error(`Unexpected error while dialing peer store peers`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async hasMoreThanMaxBootstrapConnections(): Promise<boolean> {
|
||||
try {
|
||||
const peers = await Promise.all(
|
||||
this.libp2p
|
||||
.getConnections()
|
||||
.map((conn) => conn.remotePeer)
|
||||
.map((id) => this.getPeer(id))
|
||||
);
|
||||
|
||||
const bootstrapPeers = peers.filter(
|
||||
(peer) => peer && peer.tags.has(Tags.BOOTSTRAP)
|
||||
);
|
||||
|
||||
return bootstrapPeers.length > this.options.maxBootstrapPeers;
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Unexpected error while checking for bootstrap connections`,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async getPeer(peerId: PeerId): Promise<Peer | null> {
|
||||
try {
|
||||
return await this.libp2p.peerStore.get(peerId);
|
||||
} catch (error) {
|
||||
log.error(`Failed to get peer ${peerId}, error: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getTagsForPeer(peerId: PeerId): Promise<string[]> {
|
||||
try {
|
||||
const peer = await this.libp2p.peerStore.get(peerId);
|
||||
return Array.from(peer.tags.keys());
|
||||
} catch (error) {
|
||||
log.error(`Failed to get peer ${peerId}, error: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,606 @@
|
||||
import { type Peer, type PeerId, type Stream } from "@libp2p/interface";
|
||||
import { peerIdFromString } from "@libp2p/peer-id";
|
||||
import { multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import {
|
||||
IWakuEventEmitter,
|
||||
Libp2p,
|
||||
NetworkConfig,
|
||||
PubsubTopic
|
||||
} from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ConnectionLimiter } from "./connection_limiter.js";
|
||||
import { ConnectionManager } from "./connection_manager.js";
|
||||
import { DiscoveryDialer } from "./discovery_dialer.js";
|
||||
import { KeepAliveManager } from "./keep_alive_manager.js";
|
||||
import { NetworkMonitor } from "./network_monitor.js";
|
||||
import { ShardReader } from "./shard_reader.js";
|
||||
|
||||
describe("ConnectionManager", () => {
|
||||
let libp2p: Libp2p;
|
||||
let events: IWakuEventEmitter;
|
||||
let networkConfig: NetworkConfig;
|
||||
let pubsubTopics: PubsubTopic[];
|
||||
let relay: any;
|
||||
let connectionManager: ConnectionManager;
|
||||
let mockPeerId: PeerId;
|
||||
let mockMultiaddr: MultiaddrInput;
|
||||
let mockStream: Stream;
|
||||
// Mock internal components
|
||||
let mockKeepAliveManager: sinon.SinonStubbedInstance<KeepAliveManager>;
|
||||
let mockDiscoveryDialer: sinon.SinonStubbedInstance<DiscoveryDialer>;
|
||||
let mockShardReader: sinon.SinonStubbedInstance<ShardReader>;
|
||||
let mockNetworkMonitor: sinon.SinonStubbedInstance<NetworkMonitor>;
|
||||
let mockConnectionLimiter: sinon.SinonStubbedInstance<ConnectionLimiter>;
|
||||
|
||||
const createMockPeer = (
|
||||
id: string,
|
||||
protocols: string[] = [],
|
||||
ping = 100
|
||||
): Peer =>
|
||||
({
|
||||
id: peerIdFromString(id),
|
||||
protocols,
|
||||
metadata: new Map([["ping", new TextEncoder().encode(ping.toString())]]),
|
||||
toString: () => id
|
||||
}) as Peer;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock dependencies
|
||||
libp2p = {
|
||||
dialProtocol: sinon.stub().resolves({} as Stream),
|
||||
hangUp: sinon.stub().resolves(),
|
||||
getPeers: sinon.stub().returns([]),
|
||||
peerStore: {
|
||||
get: sinon.stub().resolves(null),
|
||||
merge: sinon.stub().resolves()
|
||||
}
|
||||
} as unknown as Libp2p;
|
||||
|
||||
events = {
|
||||
dispatchEvent: sinon.stub()
|
||||
} as unknown as IWakuEventEmitter;
|
||||
|
||||
networkConfig = {
|
||||
clusterId: 1,
|
||||
shards: [0, 1]
|
||||
} as NetworkConfig;
|
||||
|
||||
pubsubTopics = ["/waku/2/rs/1/0", "/waku/2/rs/1/1"];
|
||||
|
||||
relay = {
|
||||
pubsubTopics,
|
||||
getMeshPeers: sinon.stub().returns([])
|
||||
};
|
||||
|
||||
// Create mock internal components
|
||||
mockKeepAliveManager = {
|
||||
start: sinon.stub(),
|
||||
stop: sinon.stub()
|
||||
} as unknown as sinon.SinonStubbedInstance<KeepAliveManager>;
|
||||
|
||||
mockDiscoveryDialer = {
|
||||
start: sinon.stub(),
|
||||
stop: sinon.stub()
|
||||
} as unknown as sinon.SinonStubbedInstance<DiscoveryDialer>;
|
||||
|
||||
mockShardReader = {
|
||||
isPeerOnTopic: sinon.stub().resolves(true)
|
||||
} as unknown as sinon.SinonStubbedInstance<ShardReader>;
|
||||
|
||||
mockNetworkMonitor = {
|
||||
start: sinon.stub(),
|
||||
stop: sinon.stub(),
|
||||
isConnected: sinon.stub().returns(true)
|
||||
} as unknown as sinon.SinonStubbedInstance<NetworkMonitor>;
|
||||
|
||||
mockConnectionLimiter = {
|
||||
start: sinon.stub(),
|
||||
stop: sinon.stub()
|
||||
} as unknown as sinon.SinonStubbedInstance<ConnectionLimiter>;
|
||||
|
||||
// Create test data
|
||||
mockPeerId = peerIdFromString(
|
||||
"12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE"
|
||||
);
|
||||
mockMultiaddr = multiaddr(
|
||||
"/ip4/127.0.0.1/tcp/60000/p2p/12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE"
|
||||
);
|
||||
mockStream = {} as Stream;
|
||||
|
||||
// Mock the internal component prototype methods
|
||||
sinon
|
||||
.stub(KeepAliveManager.prototype, "start")
|
||||
.callsFake(() => mockKeepAliveManager.start());
|
||||
sinon
|
||||
.stub(KeepAliveManager.prototype, "stop")
|
||||
.callsFake(() => mockKeepAliveManager.stop());
|
||||
|
||||
sinon
|
||||
.stub(DiscoveryDialer.prototype, "start")
|
||||
.callsFake(() => mockDiscoveryDialer.start());
|
||||
sinon
|
||||
.stub(DiscoveryDialer.prototype, "stop")
|
||||
.callsFake(() => mockDiscoveryDialer.stop());
|
||||
|
||||
sinon
|
||||
.stub(ShardReader.prototype, "isPeerOnTopic")
|
||||
.callsFake((peerId: PeerId, topic: string) =>
|
||||
mockShardReader.isPeerOnTopic(peerId, topic)
|
||||
);
|
||||
|
||||
sinon
|
||||
.stub(NetworkMonitor.prototype, "start")
|
||||
.callsFake(() => mockNetworkMonitor.start());
|
||||
sinon
|
||||
.stub(NetworkMonitor.prototype, "stop")
|
||||
.callsFake(() => mockNetworkMonitor.stop());
|
||||
sinon
|
||||
.stub(NetworkMonitor.prototype, "isConnected")
|
||||
.callsFake(() => mockNetworkMonitor.isConnected());
|
||||
|
||||
sinon
|
||||
.stub(ConnectionLimiter.prototype, "start")
|
||||
.callsFake(() => mockConnectionLimiter.start());
|
||||
sinon
|
||||
.stub(ConnectionLimiter.prototype, "stop")
|
||||
.callsFake(() => mockConnectionLimiter.stop());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create ConnectionManager with required options", () => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
|
||||
expect(connectionManager).to.be.instanceOf(ConnectionManager);
|
||||
});
|
||||
|
||||
it("should create ConnectionManager with relay", () => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig,
|
||||
relay
|
||||
});
|
||||
|
||||
expect(connectionManager).to.be.instanceOf(ConnectionManager);
|
||||
});
|
||||
|
||||
it("should set default options when no config provided", () => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
|
||||
expect(connectionManager).to.be.instanceOf(ConnectionManager);
|
||||
// Default options are set internally and tested through behavior
|
||||
});
|
||||
|
||||
it("should merge provided config with defaults", () => {
|
||||
const customConfig = {
|
||||
maxBootstrapPeers: 5,
|
||||
pingKeepAlive: 120
|
||||
};
|
||||
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig,
|
||||
config: customConfig
|
||||
});
|
||||
|
||||
expect(connectionManager).to.be.instanceOf(ConnectionManager);
|
||||
});
|
||||
|
||||
it("should create all internal components", () => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig,
|
||||
relay
|
||||
});
|
||||
|
||||
expect(connectionManager).to.be.instanceOf(ConnectionManager);
|
||||
// Internal components are created and tested through their behavior
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig,
|
||||
relay
|
||||
});
|
||||
});
|
||||
|
||||
it("should start all internal components", () => {
|
||||
connectionManager.start();
|
||||
|
||||
expect(mockNetworkMonitor.start.calledOnce).to.be.true;
|
||||
expect(mockDiscoveryDialer.start.calledOnce).to.be.true;
|
||||
expect(mockKeepAliveManager.start.calledOnce).to.be.true;
|
||||
expect(mockConnectionLimiter.start.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
connectionManager.start();
|
||||
connectionManager.start();
|
||||
|
||||
expect(mockNetworkMonitor.start.calledTwice).to.be.true;
|
||||
expect(mockDiscoveryDialer.start.calledTwice).to.be.true;
|
||||
expect(mockKeepAliveManager.start.calledTwice).to.be.true;
|
||||
expect(mockConnectionLimiter.start.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig,
|
||||
relay
|
||||
});
|
||||
connectionManager.start();
|
||||
});
|
||||
|
||||
it("should stop all internal components", () => {
|
||||
connectionManager.stop();
|
||||
|
||||
expect(mockNetworkMonitor.stop.calledOnce).to.be.true;
|
||||
expect(mockDiscoveryDialer.stop.calledOnce).to.be.true;
|
||||
expect(mockKeepAliveManager.stop.calledOnce).to.be.true;
|
||||
expect(mockConnectionLimiter.stop.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
connectionManager.stop();
|
||||
connectionManager.stop();
|
||||
|
||||
expect(mockNetworkMonitor.stop.calledTwice).to.be.true;
|
||||
expect(mockDiscoveryDialer.stop.calledTwice).to.be.true;
|
||||
expect(mockKeepAliveManager.stop.calledTwice).to.be.true;
|
||||
expect(mockConnectionLimiter.stop.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConnected", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should delegate to networkMonitor.isConnected()", () => {
|
||||
mockNetworkMonitor.isConnected.returns(true);
|
||||
|
||||
const result = connectionManager.isConnected();
|
||||
|
||||
expect(mockNetworkMonitor.isConnected.calledOnce).to.be.true;
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false when network is not connected", () => {
|
||||
mockNetworkMonitor.isConnected.returns(false);
|
||||
|
||||
const result = connectionManager.isConnected();
|
||||
|
||||
expect(mockNetworkMonitor.isConnected.calledOnce).to.be.true;
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("dial", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should dial with PeerId and return stream", async () => {
|
||||
const protocolCodecs = ["/waku/2/store/1.0.0"];
|
||||
const libp2pStub = libp2p.dialProtocol as sinon.SinonStub;
|
||||
libp2pStub.resolves(mockStream);
|
||||
|
||||
const result = await connectionManager.dial(mockPeerId, protocolCodecs);
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(libp2pStub.calledWith(mockPeerId, protocolCodecs)).to.be.true;
|
||||
expect(result).to.equal(mockStream);
|
||||
});
|
||||
|
||||
it("should dial with multiaddr and return stream", async () => {
|
||||
const protocolCodecs = ["/waku/2/store/1.0.0"];
|
||||
const libp2pStub = libp2p.dialProtocol as sinon.SinonStub;
|
||||
libp2pStub.resolves(mockStream);
|
||||
|
||||
const result = await connectionManager.dial(
|
||||
mockMultiaddr,
|
||||
protocolCodecs
|
||||
);
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(result).to.equal(mockStream);
|
||||
});
|
||||
|
||||
it("should handle dial errors", async () => {
|
||||
const protocolCodecs = ["/waku/2/store/1.0.0"];
|
||||
const libp2pStub = libp2p.dialProtocol as sinon.SinonStub;
|
||||
const error = new Error("Dial failed");
|
||||
libp2pStub.rejects(error);
|
||||
|
||||
try {
|
||||
await connectionManager.dial(mockPeerId, protocolCodecs);
|
||||
expect.fail("Should have thrown error");
|
||||
} catch (e) {
|
||||
expect(e).to.equal(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("hangUp", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should hang up with PeerId and return true on success", async () => {
|
||||
const libp2pStub = libp2p.hangUp as sinon.SinonStub;
|
||||
libp2pStub.resolves();
|
||||
|
||||
const result = await connectionManager.hangUp(mockPeerId);
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(libp2pStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should hang up with multiaddr and return true on success", async () => {
|
||||
const libp2pStub = libp2p.hangUp as sinon.SinonStub;
|
||||
libp2pStub.resolves();
|
||||
|
||||
const result = await connectionManager.hangUp(mockMultiaddr);
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false and handle errors gracefully", async () => {
|
||||
const libp2pStub = libp2p.hangUp as sinon.SinonStub;
|
||||
libp2pStub.rejects(new Error("Hang up failed"));
|
||||
|
||||
const result = await connectionManager.hangUp(mockPeerId);
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConnectedPeers", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when no peers connected", async () => {
|
||||
const libp2pStub = libp2p.getPeers as sinon.SinonStub;
|
||||
libp2pStub.returns([]);
|
||||
|
||||
const result = await connectionManager.getConnectedPeers();
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(result).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it("should return all connected peers without codec filter", async () => {
|
||||
const peer1Id = "12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE";
|
||||
const peer2Id = "12D3KooWNFmTNRsVfUJqGrRMzQiULd4fL2iRKGj4PpNm4F5BhvCw";
|
||||
const mockPeerIds = [
|
||||
peerIdFromString(peer1Id),
|
||||
peerIdFromString(peer2Id)
|
||||
];
|
||||
const mockPeers = [
|
||||
createMockPeer(peer1Id, ["/waku/2/relay/1.0.0"], 50),
|
||||
createMockPeer(peer2Id, ["/waku/2/store/1.0.0"], 100)
|
||||
];
|
||||
|
||||
const libp2pStub = libp2p.getPeers as sinon.SinonStub;
|
||||
libp2pStub.returns(mockPeerIds);
|
||||
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.onCall(0).resolves(mockPeers[0]);
|
||||
peerStoreStub.onCall(1).resolves(mockPeers[1]);
|
||||
|
||||
const result = await connectionManager.getConnectedPeers();
|
||||
|
||||
expect(libp2pStub.calledOnce).to.be.true;
|
||||
expect(peerStoreStub.calledTwice).to.be.true;
|
||||
expect(result).to.have.length(2);
|
||||
// Should be sorted by ping (peer1 has lower ping)
|
||||
expect(result[0].id.toString()).to.equal(peer1Id);
|
||||
expect(result[1].id.toString()).to.equal(peer2Id);
|
||||
});
|
||||
|
||||
it("should filter peers by codec", async () => {
|
||||
const peer1Id = "12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE";
|
||||
const peer2Id = "12D3KooWNFmTNRsVfUJqGrRMzQiULd4fL2iRKGj4PpNm4F5BhvCw";
|
||||
const mockPeerIds = [
|
||||
peerIdFromString(peer1Id),
|
||||
peerIdFromString(peer2Id)
|
||||
];
|
||||
const mockPeers = [
|
||||
createMockPeer(peer1Id, ["/waku/2/relay/1.0.0"], 50),
|
||||
createMockPeer(peer2Id, ["/waku/2/store/1.0.0"], 100)
|
||||
];
|
||||
|
||||
const libp2pStub = libp2p.getPeers as sinon.SinonStub;
|
||||
libp2pStub.returns(mockPeerIds);
|
||||
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.onCall(0).resolves(mockPeers[0]);
|
||||
peerStoreStub.onCall(1).resolves(mockPeers[1]);
|
||||
|
||||
const result = await connectionManager.getConnectedPeers(
|
||||
"/waku/2/relay/1.0.0"
|
||||
);
|
||||
|
||||
expect(result).to.have.length(1);
|
||||
expect(result[0].id.toString()).to.equal(peer1Id);
|
||||
});
|
||||
|
||||
it("should handle peerStore errors gracefully", async () => {
|
||||
const peer1Id = "12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE";
|
||||
const peer2Id = "12D3KooWNFmTNRsVfUJqGrRMzQiULd4fL2iRKGj4PpNm4F5BhvCw";
|
||||
const mockPeerIds = [
|
||||
peerIdFromString(peer1Id),
|
||||
peerIdFromString(peer2Id)
|
||||
];
|
||||
const mockPeer = createMockPeer(peer2Id, ["/waku/2/store/1.0.0"], 100);
|
||||
|
||||
const libp2pStub = libp2p.getPeers as sinon.SinonStub;
|
||||
libp2pStub.returns(mockPeerIds);
|
||||
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.onCall(0).rejects(new Error("Peer not found"));
|
||||
peerStoreStub.onCall(1).resolves(mockPeer);
|
||||
|
||||
const result = await connectionManager.getConnectedPeers();
|
||||
|
||||
expect(result).to.have.length(1);
|
||||
expect(result[0].id.toString()).to.equal(peer2Id);
|
||||
});
|
||||
|
||||
it("should sort peers by ping value", async () => {
|
||||
const peer1Id = "12D3KooWPjceQuRaNMhcrLF6BaW69PdCXB95h6TBpFf9nAmcL8hE";
|
||||
const peer2Id = "12D3KooWNFmTNRsVfUJqGrRMzQiULd4fL2iRKGj4PpNm4F5BhvCw";
|
||||
const peer3Id = "12D3KooWMvU9HGhiEHDWYgJDnLj2Z4JHBQMdxFPgWTNKXjHDYKUW";
|
||||
const mockPeerIds = [
|
||||
peerIdFromString(peer1Id),
|
||||
peerIdFromString(peer2Id),
|
||||
peerIdFromString(peer3Id)
|
||||
];
|
||||
const mockPeers = [
|
||||
createMockPeer(peer1Id, ["/waku/2/relay/1.0.0"], 200),
|
||||
createMockPeer(peer2Id, ["/waku/2/store/1.0.0"], 50),
|
||||
createMockPeer(peer3Id, ["/waku/2/filter/1.0.0"], 150)
|
||||
];
|
||||
|
||||
const libp2pStub = libp2p.getPeers as sinon.SinonStub;
|
||||
libp2pStub.returns(mockPeerIds);
|
||||
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.onCall(0).resolves(mockPeers[0]);
|
||||
peerStoreStub.onCall(1).resolves(mockPeers[1]);
|
||||
peerStoreStub.onCall(2).resolves(mockPeers[2]);
|
||||
|
||||
const result = await connectionManager.getConnectedPeers();
|
||||
|
||||
expect(result).to.have.length(3);
|
||||
// Should be sorted by ping: peer2 (50), peer3 (150), peer1 (200)
|
||||
expect(result[0].id.toString()).to.equal(peer2Id);
|
||||
expect(result[1].id.toString()).to.equal(peer3Id);
|
||||
expect(result[2].id.toString()).to.equal(peer1Id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTopicConfigured", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true when topic is configured", () => {
|
||||
const result = connectionManager.isTopicConfigured("/waku/2/rs/1/0");
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false when topic is not configured", () => {
|
||||
const result = connectionManager.isTopicConfigured("/waku/2/rs/1/99");
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPeerOnTopic", () => {
|
||||
beforeEach(() => {
|
||||
connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
events,
|
||||
pubsubTopics,
|
||||
networkConfig
|
||||
});
|
||||
});
|
||||
|
||||
it("should delegate to shardReader.isPeerOnTopic()", async () => {
|
||||
const topic = "/waku/2/rs/1/0";
|
||||
mockShardReader.isPeerOnTopic.resolves(true);
|
||||
|
||||
const result = await connectionManager.isPeerOnTopic(mockPeerId, topic);
|
||||
|
||||
expect(mockShardReader.isPeerOnTopic.calledOnce).to.be.true;
|
||||
expect(mockShardReader.isPeerOnTopic.calledWith(mockPeerId, topic)).to.be
|
||||
.true;
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false when peer is not on topic", async () => {
|
||||
const topic = "/waku/2/rs/1/0";
|
||||
mockShardReader.isPeerOnTopic.resolves(false);
|
||||
|
||||
const result = await connectionManager.isPeerOnTopic(mockPeerId, topic);
|
||||
|
||||
expect(mockShardReader.isPeerOnTopic.calledOnce).to.be.true;
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle shardReader errors", async () => {
|
||||
const topic = "/waku/2/rs/1/0";
|
||||
const error = new Error("Shard reader error");
|
||||
mockShardReader.isPeerOnTopic.rejects(error);
|
||||
|
||||
try {
|
||||
await connectionManager.isPeerOnTopic(mockPeerId, topic);
|
||||
expect.fail("Should have thrown error");
|
||||
} catch (e) {
|
||||
expect(e).to.equal(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,166 +1,58 @@
|
||||
import {
|
||||
type Connection,
|
||||
isPeerId,
|
||||
type Peer,
|
||||
type PeerId,
|
||||
type PeerInfo,
|
||||
type Stream,
|
||||
TypedEventEmitter
|
||||
} from "@libp2p/interface";
|
||||
import { Multiaddr, multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import { type Peer, type PeerId, type Stream } from "@libp2p/interface";
|
||||
import { MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import {
|
||||
ConnectionManagerOptions,
|
||||
DiscoveryTrigger,
|
||||
DNS_DISCOVERY_TAG,
|
||||
EConnectionStateEvents,
|
||||
EPeersByDiscoveryEvents,
|
||||
IConnectionManager,
|
||||
IConnectionStateEvents,
|
||||
IPeersByDiscoveryEvents,
|
||||
IRelay,
|
||||
PeersByDiscoveryResult,
|
||||
PubsubTopic,
|
||||
ShardInfo
|
||||
IWakuEventEmitter,
|
||||
NetworkConfig,
|
||||
PubsubTopic
|
||||
} from "@waku/interfaces";
|
||||
import { Libp2p, Tags } from "@waku/interfaces";
|
||||
import { decodeRelayShard, shardInfoToPubsubTopics } from "@waku/utils";
|
||||
import { Libp2p } from "@waku/interfaces";
|
||||
import { Logger } from "@waku/utils";
|
||||
|
||||
import { ConnectionLimiter } from "./connection_limiter.js";
|
||||
import { Dialer } from "./dialer.js";
|
||||
import { DiscoveryDialer } from "./discovery_dialer.js";
|
||||
import { KeepAliveManager } from "./keep_alive_manager.js";
|
||||
import { getPeerPing } from "./utils.js";
|
||||
import { NetworkMonitor } from "./network_monitor.js";
|
||||
import { ShardReader } from "./shard_reader.js";
|
||||
import { getPeerPing, mapToPeerId, mapToPeerIdOrMultiaddr } from "./utils.js";
|
||||
|
||||
const log = new Logger("connection-manager");
|
||||
|
||||
const DEFAULT_MAX_BOOTSTRAP_PEERS_ALLOWED = 1;
|
||||
const DEFAULT_MAX_DIAL_ATTEMPTS_FOR_PEER = 3;
|
||||
const DEFAULT_MAX_PARALLEL_DIALS = 3;
|
||||
|
||||
const DEFAULT_PING_KEEP_ALIVE_SEC = 5 * 60;
|
||||
const DEFAULT_RELAY_KEEP_ALIVE_SEC = 5 * 60;
|
||||
|
||||
type ConnectionManagerConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
events: IWakuEventEmitter;
|
||||
pubsubTopics: PubsubTopic[];
|
||||
networkConfig: NetworkConfig;
|
||||
relay?: IRelay;
|
||||
config?: Partial<ConnectionManagerOptions>;
|
||||
};
|
||||
|
||||
export class ConnectionManager
|
||||
extends TypedEventEmitter<IPeersByDiscoveryEvents & IConnectionStateEvents>
|
||||
implements IConnectionManager
|
||||
{
|
||||
// TODO(weboko): make it private
|
||||
public readonly pubsubTopics: PubsubTopic[];
|
||||
export class ConnectionManager implements IConnectionManager {
|
||||
private readonly pubsubTopics: PubsubTopic[];
|
||||
|
||||
private readonly keepAliveManager: KeepAliveManager;
|
||||
private readonly discoveryDialer: DiscoveryDialer;
|
||||
private readonly dialer: Dialer;
|
||||
private readonly shardReader: ShardReader;
|
||||
private readonly networkMonitor: NetworkMonitor;
|
||||
private readonly connectionLimiter: ConnectionLimiter;
|
||||
|
||||
private keepAliveManager: KeepAliveManager;
|
||||
private options: ConnectionManagerOptions;
|
||||
private libp2p: Libp2p;
|
||||
private dialAttemptsForPeer: Map<string, number> = new Map();
|
||||
private dialErrorsForPeer: Map<string, any> = new Map();
|
||||
|
||||
private currentActiveParallelDialCount = 0;
|
||||
private pendingPeerDialQueue: Array<PeerId> = [];
|
||||
|
||||
private isP2PNetworkConnected: boolean = false;
|
||||
|
||||
public isConnected(): boolean {
|
||||
if (globalThis?.navigator && !globalThis?.navigator?.onLine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isP2PNetworkConnected;
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.keepAliveManager.stopAll();
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:connect",
|
||||
this.onEventHandlers["peer:connect"]
|
||||
);
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:disconnect",
|
||||
this.onEventHandlers["peer:disconnect"]
|
||||
);
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:discovery",
|
||||
this.onEventHandlers["peer:discovery"]
|
||||
);
|
||||
this.stopNetworkStatusListener();
|
||||
}
|
||||
|
||||
public async dropConnection(peerId: PeerId): Promise<void> {
|
||||
try {
|
||||
this.keepAliveManager.stop(peerId);
|
||||
await this.libp2p.hangUp(peerId);
|
||||
log.info(`Dropped connection with peer ${peerId.toString()}`);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error dropping connection with peer ${peerId.toString()} - ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getPeersByDiscovery(): Promise<PeersByDiscoveryResult> {
|
||||
const peersDiscovered = await this.libp2p.peerStore.all();
|
||||
const peersConnected = this.libp2p
|
||||
.getConnections()
|
||||
.map((conn) => conn.remotePeer);
|
||||
|
||||
const peersDiscoveredByBootstrap: Peer[] = [];
|
||||
const peersDiscoveredByPeerExchange: Peer[] = [];
|
||||
const peersDiscoveredByLocal: Peer[] = [];
|
||||
|
||||
const peersConnectedByBootstrap: Peer[] = [];
|
||||
const peersConnectedByPeerExchange: Peer[] = [];
|
||||
const peersConnectedByLocal: Peer[] = [];
|
||||
|
||||
for (const peer of peersDiscovered) {
|
||||
const tags = await this.getTagNamesForPeer(peer.id);
|
||||
|
||||
if (tags.includes(Tags.BOOTSTRAP)) {
|
||||
peersDiscoveredByBootstrap.push(peer);
|
||||
} else if (tags.includes(Tags.PEER_EXCHANGE)) {
|
||||
peersDiscoveredByPeerExchange.push(peer);
|
||||
} else if (tags.includes(Tags.LOCAL)) {
|
||||
peersDiscoveredByLocal.push(peer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const peerId of peersConnected) {
|
||||
const peer = await this.libp2p.peerStore.get(peerId);
|
||||
const tags = await this.getTagNamesForPeer(peerId);
|
||||
|
||||
if (tags.includes(Tags.BOOTSTRAP)) {
|
||||
peersConnectedByBootstrap.push(peer);
|
||||
} else if (tags.includes(Tags.PEER_EXCHANGE)) {
|
||||
peersConnectedByPeerExchange.push(peer);
|
||||
} else if (tags.includes(Tags.LOCAL)) {
|
||||
peersConnectedByLocal.push(peer);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
DISCOVERED: {
|
||||
[Tags.BOOTSTRAP]: peersDiscoveredByBootstrap,
|
||||
[Tags.PEER_EXCHANGE]: peersDiscoveredByPeerExchange,
|
||||
[Tags.LOCAL]: peersDiscoveredByLocal
|
||||
},
|
||||
CONNECTED: {
|
||||
[Tags.BOOTSTRAP]: peersConnectedByBootstrap,
|
||||
[Tags.PEER_EXCHANGE]: peersConnectedByPeerExchange,
|
||||
[Tags.LOCAL]: peersConnectedByLocal
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public constructor(options: ConnectionManagerConstructorOptions) {
|
||||
super();
|
||||
this.libp2p = options.libp2p;
|
||||
this.pubsubTopics = options.pubsubTopics;
|
||||
|
||||
this.options = {
|
||||
maxDialAttemptsForPeer: DEFAULT_MAX_DIAL_ATTEMPTS_FOR_PEER,
|
||||
maxBootstrapPeersAllowed: DEFAULT_MAX_BOOTSTRAP_PEERS_ALLOWED,
|
||||
maxParallelDials: DEFAULT_MAX_PARALLEL_DIALS,
|
||||
maxBootstrapPeers: DEFAULT_MAX_BOOTSTRAP_PEERS_ALLOWED,
|
||||
pingKeepAlive: DEFAULT_PING_KEEP_ALIVE_SEC,
|
||||
relayKeepAlive: DEFAULT_RELAY_KEEP_ALIVE_SEC,
|
||||
...options.config
|
||||
@ -175,24 +67,92 @@ export class ConnectionManager
|
||||
}
|
||||
});
|
||||
|
||||
this.startEventListeners()
|
||||
.then(() => log.info(`Connection Manager is now running`))
|
||||
.catch((error) =>
|
||||
log.error(`Unexpected error while running service`, error)
|
||||
this.shardReader = new ShardReader({
|
||||
libp2p: options.libp2p,
|
||||
networkConfig: options.networkConfig
|
||||
});
|
||||
|
||||
this.dialer = new Dialer({
|
||||
libp2p: options.libp2p,
|
||||
shardReader: this.shardReader
|
||||
});
|
||||
|
||||
this.discoveryDialer = new DiscoveryDialer({
|
||||
libp2p: options.libp2p,
|
||||
dialer: this.dialer
|
||||
});
|
||||
|
||||
this.networkMonitor = new NetworkMonitor({
|
||||
libp2p: options.libp2p,
|
||||
events: options.events
|
||||
});
|
||||
|
||||
this.connectionLimiter = new ConnectionLimiter({
|
||||
libp2p: options.libp2p,
|
||||
events: options.events,
|
||||
networkMonitor: this.networkMonitor,
|
||||
dialer: this.dialer,
|
||||
options: this.options
|
||||
});
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.networkMonitor.start();
|
||||
this.discoveryDialer.start();
|
||||
this.keepAliveManager.start();
|
||||
this.connectionLimiter.start();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.networkMonitor.stop();
|
||||
this.discoveryDialer.stop();
|
||||
this.keepAliveManager.stop();
|
||||
this.connectionLimiter.stop();
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.networkMonitor.isConnected();
|
||||
}
|
||||
|
||||
public async dial(
|
||||
peer: PeerId | MultiaddrInput,
|
||||
protocolCodecs: string[]
|
||||
): Promise<Stream> {
|
||||
const ma = mapToPeerIdOrMultiaddr(peer);
|
||||
log.info(`Dialing peer ${ma.toString()} with protocols ${protocolCodecs}`);
|
||||
|
||||
// must use libp2p directly instead of dialer because we need to dial the peer right away
|
||||
const stream = await this.libp2p.dialProtocol(ma, protocolCodecs);
|
||||
log.info(`Dialed peer ${ma.toString()} with protocols ${protocolCodecs}`);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
public async hangUp(peer: PeerId | MultiaddrInput): Promise<boolean> {
|
||||
const peerId = mapToPeerId(peer);
|
||||
|
||||
try {
|
||||
log.info(`Dropping connection with peer ${peerId.toString()}`);
|
||||
await this.libp2p.hangUp(peerId);
|
||||
log.info(`Dropped connection with peer ${peerId.toString()}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error dropping connection with peer ${peerId.toString()} - ${error}`
|
||||
);
|
||||
|
||||
// libp2p emits `peer:discovery` events during its initialization
|
||||
// which means that before the ConnectionManager is initialized, some peers may have been discovered
|
||||
// we will dial the peers in peerStore ONCE before we start to listen to the `peer:discovery` events within the ConnectionManager
|
||||
this.dialPeerStorePeers().catch((error) =>
|
||||
log.error(`Unexpected error while dialing peer store peers`, error)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getConnectedPeers(codec?: string): Promise<Peer[]> {
|
||||
const peerIDs = this.libp2p.getPeers();
|
||||
|
||||
log.info(`Getting connected peers for codec ${codec}`);
|
||||
|
||||
if (peerIDs.length === 0) {
|
||||
log.info(`No connected peers`);
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -206,553 +166,28 @@ export class ConnectionManager
|
||||
})
|
||||
);
|
||||
|
||||
return peers
|
||||
const result = peers
|
||||
.filter((p) => !!p)
|
||||
.filter((p) => (codec ? (p as Peer).protocols.includes(codec) : true))
|
||||
.sort((left, right) => getPeerPing(left) - getPeerPing(right)) as Peer[];
|
||||
|
||||
log.info(`Found ${result.length} connected peers for codec ${codec}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async dialPeerStorePeers(): Promise<void> {
|
||||
const peerInfos = await this.libp2p.peerStore.all();
|
||||
const dialPromises = [];
|
||||
for (const peerInfo of peerInfos) {
|
||||
if (
|
||||
this.libp2p.getConnections().find((c) => c.remotePeer === peerInfo.id)
|
||||
)
|
||||
continue;
|
||||
|
||||
dialPromises.push(this.attemptDial(peerInfo.id));
|
||||
}
|
||||
try {
|
||||
await Promise.all(dialPromises);
|
||||
} catch (error) {
|
||||
log.error(`Unexpected error while dialing peer store peers`, error);
|
||||
}
|
||||
public isTopicConfigured(pubsubTopic: PubsubTopic): boolean {
|
||||
return this.pubsubTopics.includes(pubsubTopic);
|
||||
}
|
||||
|
||||
private async startEventListeners(): Promise<void> {
|
||||
this.startPeerDiscoveryListener();
|
||||
this.startPeerConnectionListener();
|
||||
this.startPeerDisconnectionListener();
|
||||
|
||||
this.startNetworkStatusListener();
|
||||
public async hasShardInfo(peerId: PeerId): Promise<boolean> {
|
||||
return this.shardReader.hasShardInfo(peerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to establish a connection with a peer and set up specified protocols.
|
||||
* The method handles both PeerId and Multiaddr inputs, manages connection attempts,
|
||||
* and maintains the connection state.
|
||||
*
|
||||
* The dialing process includes:
|
||||
* 1. Converting input to dialable peer info
|
||||
* 2. Managing parallel dial attempts
|
||||
* 3. Attempting to establish protocol-specific connections
|
||||
* 4. Handling connection failures and retries
|
||||
* 5. Updating the peer store and connection state
|
||||
*
|
||||
* @param {PeerId | MultiaddrInput} peer - The peer to connect to, either as a PeerId or multiaddr
|
||||
* @param {string[]} [protocolCodecs] - Optional array of protocol-specific codec strings to establish
|
||||
* (e.g., for LightPush, Filter, Store protocols)
|
||||
*
|
||||
* @throws {Error} If the multiaddr is missing a peer ID
|
||||
* @throws {Error} If the maximum dial attempts are reached and the peer cannot be dialed
|
||||
* @throws {Error} If there's an error deleting an undialable peer from the peer store
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Dial using PeerId
|
||||
* await connectionManager.dialPeer(peerId);
|
||||
*
|
||||
* // Dial using multiaddr with specific protocols
|
||||
* await connectionManager.dialPeer(multiaddr, [
|
||||
* "/vac/waku/relay/2.0.0",
|
||||
* "/vac/waku/lightpush/2.0.0-beta1"
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* - The method implements exponential backoff through multiple dial attempts
|
||||
* - Maintains a queue for parallel dial attempts (limited by maxParallelDials)
|
||||
* - Integrates with the KeepAliveManager for connection maintenance
|
||||
* - Updates the peer store and connection state after successful/failed attempts
|
||||
* - If all dial attempts fail, triggers DNS discovery as a fallback
|
||||
*/
|
||||
public async dialPeer(peer: PeerId | MultiaddrInput): Promise<Connection> {
|
||||
let connection: Connection | undefined;
|
||||
let peerId: PeerId | undefined;
|
||||
const peerDialInfo = this.getDialablePeerInfo(peer);
|
||||
const peerIdStr = isPeerId(peerDialInfo)
|
||||
? peerDialInfo.toString()
|
||||
: peerDialInfo.getPeerId()!;
|
||||
|
||||
this.currentActiveParallelDialCount += 1;
|
||||
let dialAttempt = 0;
|
||||
while (dialAttempt < this.options.maxDialAttemptsForPeer) {
|
||||
try {
|
||||
log.info(`Dialing peer ${peerDialInfo} on attempt ${dialAttempt + 1}`);
|
||||
connection = await this.libp2p.dial(peerDialInfo);
|
||||
peerId = connection.remotePeer;
|
||||
|
||||
const tags = await this.getTagNamesForPeer(peerId);
|
||||
// add tag to connection describing discovery mechanism
|
||||
// don't add duplicate tags
|
||||
this.libp2p.getConnections(peerId).forEach((conn) => {
|
||||
conn.tags = Array.from(new Set([...conn.tags, ...tags]));
|
||||
});
|
||||
|
||||
// instead of deleting the peer from the peer store, we set the dial attempt to -1
|
||||
// this helps us keep track of peers that have been dialed before
|
||||
this.dialAttemptsForPeer.set(peerId.toString(), -1);
|
||||
|
||||
// Dialing succeeded, break the loop
|
||||
this.keepAliveManager.start(peerId);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error instanceof AggregateError) {
|
||||
// Handle AggregateError
|
||||
log.error(`Error dialing peer ${peerIdStr} - ${error.errors}`);
|
||||
} else {
|
||||
// Handle generic error
|
||||
log.error(
|
||||
`Error dialing peer ${peerIdStr} - ${(error as any).message}`
|
||||
);
|
||||
}
|
||||
this.dialErrorsForPeer.set(peerIdStr, error);
|
||||
|
||||
dialAttempt++;
|
||||
this.dialAttemptsForPeer.set(peerIdStr, dialAttempt);
|
||||
}
|
||||
}
|
||||
|
||||
// Always decrease the active dial count and process the dial queue
|
||||
this.currentActiveParallelDialCount--;
|
||||
this.processDialQueue();
|
||||
|
||||
// If max dial attempts reached and dialing failed, delete the peer
|
||||
if (dialAttempt === this.options.maxDialAttemptsForPeer) {
|
||||
try {
|
||||
const error = this.dialErrorsForPeer.get(peerIdStr);
|
||||
|
||||
if (error) {
|
||||
let errorMessage;
|
||||
if (error instanceof AggregateError) {
|
||||
if (!error.errors) {
|
||||
log.warn(`No errors array found for AggregateError`);
|
||||
} else if (error.errors.length === 0) {
|
||||
log.warn(`Errors array is empty for AggregateError`);
|
||||
} else {
|
||||
errorMessage = JSON.stringify(error.errors[0]);
|
||||
}
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
log.info(
|
||||
`Deleting undialable peer ${peerIdStr} from peer store. Reason: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
this.dialErrorsForPeer.delete(peerIdStr);
|
||||
if (peerId) {
|
||||
await this.libp2p.peerStore.delete(peerId);
|
||||
}
|
||||
|
||||
// if it was last available peer - attempt DNS discovery
|
||||
await this.attemptDnsDiscovery();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error deleting undialable peer ${peerIdStr} from peer store - ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
throw new Error(`Failed to dial peer ${peerDialInfo}`);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dial a peer with specific protocols.
|
||||
* This method is a raw proxy to the libp2p dialProtocol method.
|
||||
* @param peer - The peer to connect to, either as a PeerId or multiaddr
|
||||
* @param protocolCodecs - Optional array of protocol-specific codec strings to establish
|
||||
* @returns A stream to the peer
|
||||
*/
|
||||
public async rawDialPeerWithProtocols(
|
||||
peer: PeerId | MultiaddrInput,
|
||||
protocolCodecs: string[]
|
||||
): Promise<Stream> {
|
||||
const peerDialInfo = this.getDialablePeerInfo(peer);
|
||||
return await this.libp2p.dialProtocol(peerDialInfo, protocolCodecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal utility to extract a PeerId or Multiaddr from a peer input.
|
||||
* This is used internally by the connection manager to handle different peer input formats.
|
||||
* @internal
|
||||
*/
|
||||
private getDialablePeerInfo(
|
||||
peer: PeerId | MultiaddrInput
|
||||
): PeerId | Multiaddr {
|
||||
if (isPeerId(peer)) {
|
||||
return peer;
|
||||
} else {
|
||||
// peer is of MultiaddrInput type
|
||||
const ma = multiaddr(peer);
|
||||
const peerIdStr = ma.getPeerId();
|
||||
if (!peerIdStr) {
|
||||
throw new Error("Failed to dial multiaddr: missing peer ID");
|
||||
}
|
||||
return ma;
|
||||
}
|
||||
}
|
||||
|
||||
private async attemptDnsDiscovery(): Promise<void> {
|
||||
if (this.libp2p.getConnections().length > 0) return;
|
||||
if ((await this.libp2p.peerStore.all()).length > 0) return;
|
||||
|
||||
log.info("Attempting to trigger DNS discovery.");
|
||||
|
||||
const dnsDiscovery = Object.values(this.libp2p.components.components).find(
|
||||
(v: unknown) => {
|
||||
if (v && v.toString) {
|
||||
return v.toString().includes(DNS_DISCOVERY_TAG);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
) as DiscoveryTrigger;
|
||||
|
||||
if (!dnsDiscovery) return;
|
||||
|
||||
await dnsDiscovery.findPeers();
|
||||
}
|
||||
|
||||
private processDialQueue(): void {
|
||||
if (
|
||||
this.pendingPeerDialQueue.length > 0 &&
|
||||
this.currentActiveParallelDialCount < this.options.maxParallelDials
|
||||
) {
|
||||
const peerId = this.pendingPeerDialQueue.shift();
|
||||
if (!peerId) return;
|
||||
this.attemptDial(peerId).catch((error) => {
|
||||
log.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private startPeerDiscoveryListener(): void {
|
||||
this.libp2p.addEventListener(
|
||||
"peer:discovery",
|
||||
this.onEventHandlers["peer:discovery"]
|
||||
);
|
||||
}
|
||||
|
||||
private startPeerConnectionListener(): void {
|
||||
this.libp2p.addEventListener(
|
||||
"peer:connect",
|
||||
this.onEventHandlers["peer:connect"]
|
||||
);
|
||||
}
|
||||
|
||||
private startPeerDisconnectionListener(): void {
|
||||
// TODO: ensure that these following issues are updated and confirmed
|
||||
/**
|
||||
* NOTE: Event is not being emitted on closing nor losing a connection.
|
||||
* @see https://github.com/libp2p/js-libp2p/issues/939
|
||||
* @see https://github.com/status-im/js-waku/issues/252
|
||||
*
|
||||
* >This event will be triggered anytime we are disconnected from another peer,
|
||||
* >regardless of the circumstances of that disconnection.
|
||||
* >If we happen to have multiple connections to a peer,
|
||||
* >this event will **only** be triggered when the last connection is closed.
|
||||
* @see https://github.com/libp2p/js-libp2p/blob/bad9e8c0ff58d60a78314077720c82ae331cc55b/doc/API.md?plain=1#L2100
|
||||
*/
|
||||
this.libp2p.addEventListener(
|
||||
"peer:disconnect",
|
||||
this.onEventHandlers["peer:disconnect"]
|
||||
);
|
||||
}
|
||||
|
||||
public async attemptDial(peerId: PeerId): Promise<void> {
|
||||
if (!(await this.shouldDialPeer(peerId))) return;
|
||||
|
||||
if (this.currentActiveParallelDialCount >= this.options.maxParallelDials) {
|
||||
this.pendingPeerDialQueue.push(peerId);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dialPeer(peerId);
|
||||
}
|
||||
|
||||
private onEventHandlers = {
|
||||
"peer:discovery": (evt: CustomEvent<PeerInfo>): void => {
|
||||
void (async () => {
|
||||
const { id: peerId } = evt.detail;
|
||||
|
||||
await this.dispatchDiscoveryEvent(peerId);
|
||||
|
||||
try {
|
||||
await this.attemptDial(peerId);
|
||||
} catch (error) {
|
||||
log.error(`Error dialing peer ${peerId.toString()} : ${error}`);
|
||||
}
|
||||
})();
|
||||
},
|
||||
"peer:connect": (evt: CustomEvent<PeerId>): void => {
|
||||
void (async () => {
|
||||
log.info(`Connected to peer ${evt.detail.toString()}`);
|
||||
|
||||
const peerId = evt.detail;
|
||||
|
||||
this.keepAliveManager.start(peerId);
|
||||
|
||||
const isBootstrap = (await this.getTagNamesForPeer(peerId)).includes(
|
||||
Tags.BOOTSTRAP
|
||||
);
|
||||
|
||||
if (isBootstrap) {
|
||||
const bootstrapConnections = this.libp2p
|
||||
.getConnections()
|
||||
.filter((conn) => conn.tags.includes(Tags.BOOTSTRAP));
|
||||
|
||||
// If we have too many bootstrap connections, drop one
|
||||
if (
|
||||
bootstrapConnections.length > this.options.maxBootstrapPeersAllowed
|
||||
) {
|
||||
await this.dropConnection(peerId);
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<PeerId>(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
{
|
||||
detail: peerId
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<PeerId>(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE,
|
||||
{
|
||||
detail: peerId
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.setP2PNetworkConnected();
|
||||
})();
|
||||
},
|
||||
"peer:disconnect": (evt: CustomEvent<PeerId>): void => {
|
||||
void (async () => {
|
||||
this.keepAliveManager.stop(evt.detail);
|
||||
this.setP2PNetworkDisconnected();
|
||||
})();
|
||||
},
|
||||
"browser:network": (): void => {
|
||||
this.dispatchWakuConnectionEvent();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the peer should be dialed based on the following conditions:
|
||||
* 1. If the peer is already connected, don't dial
|
||||
* 2. If the peer is not part of any of the configured pubsub topics, don't dial
|
||||
* 3. If the peer is not dialable based on bootstrap status, don't dial
|
||||
* 4. If the peer is already has an active dial attempt, or has been dialed before, don't dial it
|
||||
* @returns true if the peer should be dialed, false otherwise
|
||||
*/
|
||||
private async shouldDialPeer(peerId: PeerId): Promise<boolean> {
|
||||
const isConnected = this.libp2p.getConnections(peerId).length > 0;
|
||||
if (isConnected) {
|
||||
log.warn(`Already connected to peer ${peerId.toString()}. Not dialing.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isSameShard = await this.isPeerOnSameShard(peerId);
|
||||
if (!isSameShard) {
|
||||
const shardInfo = await this.getPeerShardInfo(peerId);
|
||||
|
||||
log.warn(
|
||||
`Discovered peer ${peerId.toString()} with ShardInfo ${shardInfo} is not part of any of the configured pubsub topics (${
|
||||
this.pubsubTopics
|
||||
}).
|
||||
Not dialing.`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPreferredBasedOnBootstrap =
|
||||
await this.isPeerDialableBasedOnBootstrapStatus(peerId);
|
||||
if (!isPreferredBasedOnBootstrap) {
|
||||
log.warn(
|
||||
`Peer ${peerId.toString()} is not dialable based on bootstrap status. Not dialing.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasBeenDialed = this.dialAttemptsForPeer.has(peerId.toString());
|
||||
if (hasBeenDialed) {
|
||||
log.warn(
|
||||
`Peer ${peerId.toString()} has already been attempted dial before, or already has a dial attempt in progress, skipping dial`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the peer is dialable based on the following conditions:
|
||||
* 1. If the peer is a bootstrap peer, it is only dialable if the number of current bootstrap connections is less than the max allowed.
|
||||
* 2. If the peer is not a bootstrap peer
|
||||
*/
|
||||
private async isPeerDialableBasedOnBootstrapStatus(
|
||||
peerId: PeerId
|
||||
): Promise<boolean> {
|
||||
const tagNames = await this.getTagNamesForPeer(peerId);
|
||||
|
||||
const isBootstrap = tagNames.some((tagName) => tagName === Tags.BOOTSTRAP);
|
||||
|
||||
if (!isBootstrap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentBootstrapConnections = this.libp2p
|
||||
.getConnections()
|
||||
.filter((conn) => {
|
||||
return conn.tags.find((name) => name === Tags.BOOTSTRAP);
|
||||
}).length;
|
||||
|
||||
return currentBootstrapConnections < this.options.maxBootstrapPeersAllowed;
|
||||
}
|
||||
|
||||
private async dispatchDiscoveryEvent(peerId: PeerId): Promise<void> {
|
||||
const isBootstrap = (await this.getTagNamesForPeer(peerId)).includes(
|
||||
Tags.BOOTSTRAP
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<PeerId>(
|
||||
isBootstrap
|
||||
? EPeersByDiscoveryEvents.PEER_DISCOVERY_BOOTSTRAP
|
||||
: EPeersByDiscoveryEvents.PEER_DISCOVERY_PEER_EXCHANGE,
|
||||
{
|
||||
detail: peerId
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the tag names for a given peer
|
||||
*/
|
||||
private async getTagNamesForPeer(peerId: PeerId): Promise<string[]> {
|
||||
try {
|
||||
const peer = await this.libp2p.peerStore.get(peerId);
|
||||
return Array.from(peer.tags.keys());
|
||||
} catch (error) {
|
||||
log.error(`Failed to get peer ${peerId}, error: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async isPeerOnSameShard(peerId: PeerId): Promise<boolean> {
|
||||
const shardInfo = await this.getPeerShardInfo(peerId);
|
||||
|
||||
if (!shardInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pubsubTopics = shardInfoToPubsubTopics(shardInfo);
|
||||
|
||||
const isTopicConfigured = pubsubTopics.some((topic) =>
|
||||
this.pubsubTopics.includes(topic)
|
||||
);
|
||||
|
||||
return isTopicConfigured;
|
||||
}
|
||||
|
||||
public async isPeerOnPubsubTopic(
|
||||
public async isPeerOnTopic(
|
||||
peerId: PeerId,
|
||||
pubsubTopic: string
|
||||
): Promise<boolean> {
|
||||
const shardInfo = await this.getPeerShardInfo(peerId);
|
||||
|
||||
if (!shardInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pubsubTopics = shardInfoToPubsubTopics(shardInfo);
|
||||
return pubsubTopics.some((t) => t === pubsubTopic);
|
||||
}
|
||||
|
||||
private async getPeerShardInfo(
|
||||
peerId: PeerId
|
||||
): Promise<ShardInfo | undefined> {
|
||||
const peer = await this.libp2p.peerStore.get(peerId);
|
||||
const shardInfoBytes = peer.metadata.get("shardInfo");
|
||||
if (!shardInfoBytes) return undefined;
|
||||
return decodeRelayShard(shardInfoBytes);
|
||||
}
|
||||
|
||||
private startNetworkStatusListener(): void {
|
||||
try {
|
||||
globalThis.addEventListener(
|
||||
"online",
|
||||
this.onEventHandlers["browser:network"]
|
||||
);
|
||||
globalThis.addEventListener(
|
||||
"offline",
|
||||
this.onEventHandlers["browser:network"]
|
||||
);
|
||||
} catch (err) {
|
||||
log.error(`Failed to start network listener: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private stopNetworkStatusListener(): void {
|
||||
try {
|
||||
globalThis.removeEventListener(
|
||||
"online",
|
||||
this.onEventHandlers["browser:network"]
|
||||
);
|
||||
globalThis.removeEventListener(
|
||||
"offline",
|
||||
this.onEventHandlers["browser:network"]
|
||||
);
|
||||
} catch (err) {
|
||||
log.error(`Failed to stop network listener: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setP2PNetworkConnected(): void {
|
||||
if (!this.isP2PNetworkConnected) {
|
||||
this.isP2PNetworkConnected = true;
|
||||
this.dispatchWakuConnectionEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private setP2PNetworkDisconnected(): void {
|
||||
if (
|
||||
this.isP2PNetworkConnected &&
|
||||
this.libp2p.getConnections().length === 0
|
||||
) {
|
||||
this.isP2PNetworkConnected = false;
|
||||
this.dispatchWakuConnectionEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchWakuConnectionEvent(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<boolean>(EConnectionStateEvents.CONNECTION_STATUS, {
|
||||
detail: this.isConnected()
|
||||
})
|
||||
);
|
||||
return this.shardReader.isPeerOnTopic(peerId, pubsubTopic);
|
||||
}
|
||||
}
|
||||
|
||||
411
packages/core/src/lib/connection_manager/dialer.spec.ts
Normal file
411
packages/core/src/lib/connection_manager/dialer.spec.ts
Normal file
@ -0,0 +1,411 @@
|
||||
import { PeerId } from "@libp2p/interface";
|
||||
import { Libp2p } from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { Dialer } from "./dialer.js";
|
||||
import { ShardReader } from "./shard_reader.js";
|
||||
|
||||
describe("Dialer", () => {
|
||||
let libp2p: Libp2p;
|
||||
let dialer: Dialer;
|
||||
let mockShardReader: sinon.SinonStubbedInstance<ShardReader>;
|
||||
let mockPeerId: PeerId;
|
||||
let mockPeerId2: PeerId;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
|
||||
const createMockPeerId = (id: string): PeerId =>
|
||||
({
|
||||
toString: () => id,
|
||||
equals: (other: PeerId) => other.toString() === id
|
||||
}) as PeerId;
|
||||
|
||||
beforeEach(() => {
|
||||
libp2p = {
|
||||
dial: sinon.stub().resolves(),
|
||||
getPeers: sinon.stub().returns([])
|
||||
} as unknown as Libp2p;
|
||||
|
||||
mockShardReader = {
|
||||
hasShardInfo: sinon.stub().resolves(false),
|
||||
isPeerOnNetwork: sinon.stub().resolves(true)
|
||||
} as unknown as sinon.SinonStubbedInstance<ShardReader>;
|
||||
|
||||
mockPeerId = createMockPeerId("12D3KooWTest1");
|
||||
mockPeerId2 = createMockPeerId("12D3KooWTest2");
|
||||
|
||||
clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (dialer) {
|
||||
dialer.stop();
|
||||
}
|
||||
clock.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create dialer with libp2p and shardReader", () => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
|
||||
expect(dialer).to.be.instanceOf(Dialer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
});
|
||||
|
||||
it("should start the dialing interval", () => {
|
||||
dialer.start();
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("should clear dial history on start", () => {
|
||||
dialer.start();
|
||||
|
||||
void dialer.dial(mockPeerId);
|
||||
|
||||
dialer.stop();
|
||||
dialer.start();
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resetHistory();
|
||||
|
||||
void dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should not create multiple intervals when called multiple times", () => {
|
||||
dialer.start();
|
||||
dialer.start();
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(() => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
});
|
||||
|
||||
it("should clear the dialing interval", () => {
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
|
||||
dialer.stop();
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should clear dial history on stop", () => {
|
||||
dialer.stop();
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resetHistory();
|
||||
|
||||
dialer.start();
|
||||
void dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
dialer.stop();
|
||||
dialer.stop();
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dial", () => {
|
||||
beforeEach(() => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
});
|
||||
|
||||
it("should dial peer immediately when queue is empty", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should add peer to queue when queue is not empty", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
void dialer.dial(mockPeerId);
|
||||
|
||||
dialStub.resetHistory();
|
||||
await dialer.dial(mockPeerId2);
|
||||
|
||||
expect(dialStub.called).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId2)).to.be.true;
|
||||
});
|
||||
|
||||
it("should skip peer when already connected", async () => {
|
||||
const getPeersStub = libp2p.getPeers as sinon.SinonStub;
|
||||
getPeersStub.returns([mockPeerId]);
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should skip peer when dialed recently", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
|
||||
dialStub.resetHistory();
|
||||
|
||||
clock.tick(5000);
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.true;
|
||||
});
|
||||
|
||||
it("should allow redial after cooldown period", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
|
||||
clock.tick(10001);
|
||||
await dialer.dial(mockPeerId);
|
||||
expect(dialStub.calledTwice).to.be.true;
|
||||
});
|
||||
|
||||
it("should skip peer when not on same shard", async () => {
|
||||
mockShardReader.hasShardInfo.resolves(true);
|
||||
mockShardReader.isPeerOnNetwork.resolves(false);
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
expect(mockShardReader.hasShardInfo.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.isPeerOnNetwork.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should dial peer when on same shard", async () => {
|
||||
mockShardReader.hasShardInfo.resolves(true);
|
||||
mockShardReader.isPeerOnNetwork.resolves(true);
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.hasShardInfo.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.isPeerOnNetwork.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should dial peer when no shard info available", async () => {
|
||||
mockShardReader.hasShardInfo.resolves(false);
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.hasShardInfo.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.isPeerOnNetwork.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle dial errors gracefully", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.rejects(new Error("Dial failed"));
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("queue processing", () => {
|
||||
beforeEach(() => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
});
|
||||
|
||||
it("should process queue every 500ms", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
|
||||
dialStub.resetHistory();
|
||||
await dialer.dial(mockPeerId2);
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId2)).to.be.true;
|
||||
});
|
||||
|
||||
it("should process up to 3 peers at once", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
const mockPeerId3 = createMockPeerId("12D3KooWTest3");
|
||||
const mockPeerId4 = createMockPeerId("12D3KooWTest4");
|
||||
const mockPeerId5 = createMockPeerId("12D3KooWTest5");
|
||||
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
await dialer.dial(mockPeerId2);
|
||||
await dialer.dial(mockPeerId3);
|
||||
await dialer.dial(mockPeerId4);
|
||||
await dialer.dial(mockPeerId5);
|
||||
|
||||
expect(dialStub.callCount).to.equal(5);
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId2)).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId3)).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId4)).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId5)).to.be.true;
|
||||
});
|
||||
|
||||
it("should not process empty queue", () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
clock.tick(500);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle queue processing errors gracefully", async () => {
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
let resolveFirstDial: () => void;
|
||||
const firstDialPromise = new Promise<void>((resolve) => {
|
||||
resolveFirstDial = resolve;
|
||||
});
|
||||
dialStub.onFirstCall().returns(firstDialPromise);
|
||||
dialStub.onSecondCall().rejects(new Error("Queue dial failed"));
|
||||
|
||||
const firstDialPromise2 = dialer.dial(mockPeerId);
|
||||
await dialer.dial(mockPeerId2);
|
||||
|
||||
resolveFirstDial!();
|
||||
await firstDialPromise2;
|
||||
|
||||
clock.tick(500);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(dialStub.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("shard reader integration", () => {
|
||||
beforeEach(() => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
});
|
||||
|
||||
it("should handle shard reader errors gracefully", async () => {
|
||||
mockShardReader.hasShardInfo.rejects(new Error("Shard reader error"));
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
expect(mockShardReader.hasShardInfo.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle network check errors gracefully", async () => {
|
||||
mockShardReader.hasShardInfo.resolves(true);
|
||||
mockShardReader.isPeerOnNetwork.rejects(new Error("Network check error"));
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.called).to.be.false;
|
||||
expect(mockShardReader.hasShardInfo.calledWith(mockPeerId)).to.be.true;
|
||||
expect(mockShardReader.isPeerOnNetwork.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
it("should handle complete dial lifecycle", async () => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
|
||||
expect(dialStub.calledOnce).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
|
||||
dialer.stop();
|
||||
});
|
||||
|
||||
it("should handle multiple peers with different shard configurations", async () => {
|
||||
dialer = new Dialer({
|
||||
libp2p,
|
||||
shardReader: mockShardReader
|
||||
});
|
||||
dialer.start();
|
||||
|
||||
const dialStub = libp2p.dial as sinon.SinonStub;
|
||||
dialStub.resolves();
|
||||
|
||||
mockShardReader.hasShardInfo.withArgs(mockPeerId).resolves(true);
|
||||
mockShardReader.isPeerOnNetwork.withArgs(mockPeerId).resolves(true);
|
||||
|
||||
mockShardReader.hasShardInfo.withArgs(mockPeerId2).resolves(false);
|
||||
|
||||
await dialer.dial(mockPeerId);
|
||||
await dialer.dial(mockPeerId2);
|
||||
|
||||
expect(dialStub.calledTwice).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(dialStub.calledWith(mockPeerId2)).to.be.true;
|
||||
|
||||
dialer.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
139
packages/core/src/lib/connection_manager/dialer.ts
Normal file
139
packages/core/src/lib/connection_manager/dialer.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { Libp2p } from "@waku/interfaces";
|
||||
import { Logger } from "@waku/utils";
|
||||
|
||||
import { ShardReader } from "./shard_reader.js";
|
||||
|
||||
const log = new Logger("dialer");
|
||||
|
||||
type DialerConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
shardReader: ShardReader;
|
||||
};
|
||||
|
||||
interface IDialer {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
dial(peerId: PeerId): Promise<void>;
|
||||
}
|
||||
|
||||
export class Dialer implements IDialer {
|
||||
private readonly libp2p: Libp2p;
|
||||
private readonly shardReader: ShardReader;
|
||||
|
||||
private dialingQueue: PeerId[] = [];
|
||||
private dialHistory: Map<string, number> = new Map();
|
||||
private dialingInterval: NodeJS.Timeout | null = null;
|
||||
private isProcessing = false;
|
||||
|
||||
public constructor(options: DialerConstructorOptions) {
|
||||
this.libp2p = options.libp2p;
|
||||
this.shardReader = options.shardReader;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
if (!this.dialingInterval) {
|
||||
this.dialingInterval = setInterval(() => {
|
||||
void this.processQueue();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
this.dialHistory.clear();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.dialingInterval) {
|
||||
clearInterval(this.dialingInterval);
|
||||
this.dialingInterval = null;
|
||||
}
|
||||
|
||||
this.dialHistory.clear();
|
||||
}
|
||||
|
||||
public async dial(peerId: PeerId): Promise<void> {
|
||||
const shouldSkip = await this.shouldSkipPeer(peerId);
|
||||
|
||||
if (shouldSkip) {
|
||||
log.info(`Skipping peer: ${peerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If queue is empty and we're not currently processing, dial immediately
|
||||
if (this.dialingQueue.length === 0 && !this.isProcessing) {
|
||||
await this.dialPeer(peerId);
|
||||
} else {
|
||||
// Add to queue
|
||||
this.dialingQueue.push(peerId);
|
||||
log.info(
|
||||
`Added peer to dialing queue, queue size: ${this.dialingQueue.length}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processQueue(): Promise<void> {
|
||||
if (this.dialingQueue.length === 0 || this.isProcessing) return;
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
try {
|
||||
const peersToDial = this.dialingQueue.slice(0, 3);
|
||||
this.dialingQueue = this.dialingQueue.slice(peersToDial.length);
|
||||
|
||||
log.info(
|
||||
`Processing dial queue: dialing ${peersToDial.length} peers, ${this.dialingQueue.length} remaining in queue`
|
||||
);
|
||||
|
||||
await Promise.all(peersToDial.map((peerId) => this.dialPeer(peerId)));
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async dialPeer(peerId: PeerId): Promise<void> {
|
||||
try {
|
||||
log.info(`Dialing peer from queue: ${peerId}`);
|
||||
|
||||
await this.libp2p.dial(peerId);
|
||||
this.dialHistory.set(peerId.toString(), Date.now());
|
||||
|
||||
log.info(`Successfully dialed peer from queue: ${peerId}`);
|
||||
} catch (error) {
|
||||
log.error(`Error dialing peer ${peerId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldSkipPeer(peerId: PeerId): Promise<boolean> {
|
||||
const hasConnection = this.libp2p.getPeers().some((p) => p.equals(peerId));
|
||||
if (hasConnection) {
|
||||
log.info(`Skipping peer ${peerId} - already connected`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastDialed = this.dialHistory.get(peerId.toString());
|
||||
if (lastDialed && Date.now() - lastDialed < 10_000) {
|
||||
log.info(
|
||||
`Skipping peer ${peerId} - already dialed in the last 10 seconds`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const hasShardInfo = await this.shardReader.hasShardInfo(peerId);
|
||||
if (!hasShardInfo) {
|
||||
log.info(`Skipping peer ${peerId} - no shard info`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isOnSameShard = await this.shardReader.isPeerOnNetwork(peerId);
|
||||
if (!isOnSameShard) {
|
||||
log.info(`Skipping peer ${peerId} - not on same shard`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
log.error(`Error checking shard info for peer ${peerId}`, error);
|
||||
return true; // Skip peer when there's an error
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,304 @@
|
||||
import { PeerId, PeerInfo } from "@libp2p/interface";
|
||||
import { expect } from "chai";
|
||||
import { Libp2p } from "libp2p";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { Dialer } from "./dialer.js";
|
||||
import { DiscoveryDialer } from "./discovery_dialer.js";
|
||||
|
||||
describe("DiscoveryDialer", () => {
|
||||
let libp2p: Libp2p;
|
||||
let discoveryDialer: DiscoveryDialer;
|
||||
let dialer: sinon.SinonStubbedInstance<Dialer>;
|
||||
let mockPeerId: PeerId;
|
||||
let mockPeerInfo: PeerInfo;
|
||||
|
||||
beforeEach(() => {
|
||||
libp2p = {
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
peerStore: {
|
||||
get: sinon.stub().resolves(undefined),
|
||||
save: sinon.stub().resolves(),
|
||||
merge: sinon.stub().resolves()
|
||||
}
|
||||
} as unknown as Libp2p;
|
||||
|
||||
dialer = {
|
||||
start: sinon.stub(),
|
||||
stop: sinon.stub(),
|
||||
dial: sinon.stub().resolves()
|
||||
} as unknown as sinon.SinonStubbedInstance<Dialer>;
|
||||
|
||||
mockPeerId = {
|
||||
toString: () => "mock-peer-id",
|
||||
equals: (other: PeerId) => other.toString() === "mock-peer-id"
|
||||
} as PeerId;
|
||||
|
||||
mockPeerInfo = {
|
||||
id: mockPeerId,
|
||||
multiaddrs: []
|
||||
} as PeerInfo;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (discoveryDialer) {
|
||||
discoveryDialer.stop();
|
||||
}
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an instance with libp2p and dialer", () => {
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
expect(discoveryDialer).to.be.instanceOf(DiscoveryDialer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
});
|
||||
|
||||
it("should add event listener for peer:discovery", () => {
|
||||
discoveryDialer.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
expect(addEventListenerStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
addEventListenerStub.calledWith("peer:discovery", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
discoveryDialer.start();
|
||||
discoveryDialer.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
expect(addEventListenerStub.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(() => {
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
discoveryDialer.start();
|
||||
});
|
||||
|
||||
it("should remove event listener for peer:discovery", () => {
|
||||
discoveryDialer.stop();
|
||||
|
||||
const removeEventListenerStub =
|
||||
libp2p.removeEventListener as sinon.SinonStub;
|
||||
expect(removeEventListenerStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
removeEventListenerStub.calledWith("peer:discovery", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
discoveryDialer.stop();
|
||||
discoveryDialer.stop();
|
||||
|
||||
const removeEventListenerStub =
|
||||
libp2p.removeEventListener as sinon.SinonStub;
|
||||
expect(removeEventListenerStub.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("peer discovery handling", () => {
|
||||
let eventHandler: (event: CustomEvent<PeerInfo>) => Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
discoveryDialer.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
eventHandler = addEventListenerStub.getCall(0).args[1];
|
||||
});
|
||||
|
||||
it("should dial peer when peer is discovered", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(undefined);
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect(dialer.dial.calledOnce).to.be.true;
|
||||
expect(dialer.dial.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle dial errors gracefully", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(undefined);
|
||||
|
||||
dialer.dial.rejects(new Error("Dial failed"));
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect(dialer.dial.calledOnce).to.be.true;
|
||||
expect(dialer.dial.calledWith(mockPeerId)).to.be.true;
|
||||
});
|
||||
|
||||
it("should update peer store before dialing", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(undefined);
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect(peerStoreStub.calledWith(mockPeerId)).to.be.true;
|
||||
expect(dialer.dial.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle peer store errors gracefully", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.rejects(new Error("Peer store error"));
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect(dialer.dial.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePeerStore", () => {
|
||||
let eventHandler: (event: CustomEvent<PeerInfo>) => Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
discoveryDialer.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
eventHandler = addEventListenerStub.getCall(0).args[1];
|
||||
});
|
||||
|
||||
it("should save new peer to store", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(undefined);
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect((libp2p.peerStore.save as sinon.SinonStub).calledOnce).to.be.true;
|
||||
expect(
|
||||
(libp2p.peerStore.save as sinon.SinonStub).calledWith(mockPeerId, {
|
||||
multiaddrs: mockPeerInfo.multiaddrs
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should skip updating peer store if peer has same addresses", async () => {
|
||||
// Set up mockPeerInfo with actual multiaddrs for this test
|
||||
const mockMultiaddr = { equals: sinon.stub().returns(true) };
|
||||
const mockPeerInfoWithAddr = {
|
||||
id: mockPeerId,
|
||||
multiaddrs: [mockMultiaddr]
|
||||
} as unknown as PeerInfo;
|
||||
|
||||
const mockPeer = {
|
||||
addresses: [{ multiaddr: mockMultiaddr }]
|
||||
};
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(mockPeer);
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfoWithAddr
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect((libp2p.peerStore.save as sinon.SinonStub).called).to.be.false;
|
||||
expect((libp2p.peerStore.merge as sinon.SinonStub).called).to.be.false;
|
||||
});
|
||||
|
||||
it("should merge peer addresses if peer exists with different addresses", async () => {
|
||||
// Set up mockPeerInfo with actual multiaddrs for this test
|
||||
const mockMultiaddr = { equals: sinon.stub().returns(false) };
|
||||
const mockPeerInfoWithAddr = {
|
||||
id: mockPeerId,
|
||||
multiaddrs: [mockMultiaddr]
|
||||
} as unknown as PeerInfo;
|
||||
|
||||
const mockPeer = {
|
||||
addresses: []
|
||||
};
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(mockPeer);
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfoWithAddr
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect((libp2p.peerStore.merge as sinon.SinonStub).calledOnce).to.be.true;
|
||||
expect(
|
||||
(libp2p.peerStore.merge as sinon.SinonStub).calledWith(mockPeerId, {
|
||||
multiaddrs: mockPeerInfoWithAddr.multiaddrs
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
it("should handle complete discovery-to-dial flow", async () => {
|
||||
const peerStoreStub = libp2p.peerStore.get as sinon.SinonStub;
|
||||
peerStoreStub.resolves(undefined);
|
||||
|
||||
discoveryDialer = new DiscoveryDialer({
|
||||
libp2p,
|
||||
dialer
|
||||
});
|
||||
discoveryDialer.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
const eventHandler = addEventListenerStub.getCall(0).args[1];
|
||||
|
||||
const mockEvent = new CustomEvent("peer:discovery", {
|
||||
detail: mockPeerInfo
|
||||
});
|
||||
|
||||
await eventHandler(mockEvent);
|
||||
|
||||
expect(dialer.dial.calledOnce).to.be.true;
|
||||
expect(dialer.dial.calledWith(mockPeerId)).to.be.true;
|
||||
|
||||
discoveryDialer.stop();
|
||||
const removeEventListenerStub =
|
||||
libp2p.removeEventListener as sinon.SinonStub;
|
||||
expect(removeEventListenerStub.called).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
106
packages/core/src/lib/connection_manager/discovery_dialer.ts
Normal file
106
packages/core/src/lib/connection_manager/discovery_dialer.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { Peer, PeerId, PeerInfo } from "@libp2p/interface";
|
||||
import { Multiaddr } from "@multiformats/multiaddr";
|
||||
import { Logger } from "@waku/utils";
|
||||
import { Libp2p } from "libp2p";
|
||||
|
||||
import { Dialer } from "./dialer.js";
|
||||
|
||||
type Libp2pEventHandler<T> = (e: CustomEvent<T>) => void;
|
||||
|
||||
type DiscoveryDialerConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
dialer: Dialer;
|
||||
};
|
||||
|
||||
interface IDiscoveryDialer {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
const log = new Logger("discovery-dialer");
|
||||
|
||||
/**
|
||||
* This class is responsible for dialing peers that are discovered by the libp2p node.
|
||||
* Managing limits for the peers is out of scope for this class.
|
||||
* Dialing after discovery is needed to identify the peer and get all other information: metadata, protocols, etc.
|
||||
*/
|
||||
export class DiscoveryDialer implements IDiscoveryDialer {
|
||||
private readonly libp2p: Libp2p;
|
||||
private readonly dialer: Dialer;
|
||||
|
||||
public constructor(options: DiscoveryDialerConstructorOptions) {
|
||||
this.libp2p = options.libp2p;
|
||||
this.dialer = options.dialer;
|
||||
|
||||
this.onPeerDiscovery = this.onPeerDiscovery.bind(this);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.libp2p.addEventListener(
|
||||
"peer:discovery",
|
||||
this.onPeerDiscovery as Libp2pEventHandler<PeerInfo>
|
||||
);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:discovery",
|
||||
this.onPeerDiscovery as Libp2pEventHandler<PeerInfo>
|
||||
);
|
||||
}
|
||||
|
||||
private async onPeerDiscovery(event: CustomEvent<PeerInfo>): Promise<void> {
|
||||
const peerId = event.detail.id;
|
||||
log.info(`Discovered new peer: ${peerId}`);
|
||||
|
||||
try {
|
||||
await this.updatePeerStore(peerId, event.detail.multiaddrs);
|
||||
await this.dialer.dial(peerId);
|
||||
} catch (error) {
|
||||
log.error(`Error dialing peer ${peerId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async updatePeerStore(
|
||||
peerId: PeerId,
|
||||
multiaddrs: Multiaddr[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
log.info(`Updating peer store for ${peerId}`);
|
||||
const peer = await this.getPeer(peerId);
|
||||
|
||||
if (!peer) {
|
||||
log.info(`Peer ${peerId} not found in store, saving`);
|
||||
await this.libp2p.peerStore.save(peerId, {
|
||||
multiaddrs: multiaddrs
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSameAddr = multiaddrs.every((addr) =>
|
||||
peer.addresses.some((a) => a.multiaddr.equals(addr))
|
||||
);
|
||||
|
||||
if (hasSameAddr) {
|
||||
log.info(`Peer ${peerId} has same addresses in peer store, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Merging peer ${peerId} addresses in peer store`);
|
||||
await this.libp2p.peerStore.merge(peerId, {
|
||||
multiaddrs: multiaddrs
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error updating peer store for ${peerId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getPeer(peerId: PeerId): Promise<Peer | undefined> {
|
||||
try {
|
||||
return await this.libp2p.peerStore.get(peerId);
|
||||
} catch (error) {
|
||||
log.error(`Error getting peer info for ${peerId}`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,583 @@
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { expect } from "chai";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { KeepAliveManager } from "./keep_alive_manager.js";
|
||||
|
||||
describe("KeepAliveManager", () => {
|
||||
let libp2p: any;
|
||||
let relay: any;
|
||||
let keepAliveManager: KeepAliveManager;
|
||||
let mockPeerId: PeerId;
|
||||
let mockPeerId2: PeerId;
|
||||
let clock: sinon.SinonFakeTimers;
|
||||
|
||||
const createMockPeerId = (id: string): PeerId =>
|
||||
({
|
||||
toString: () => id,
|
||||
equals: (other: PeerId) => other.toString() === id
|
||||
}) as PeerId;
|
||||
|
||||
const defaultOptions = {
|
||||
pingKeepAlive: 30,
|
||||
relayKeepAlive: 60
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers();
|
||||
|
||||
mockPeerId = createMockPeerId("12D3KooWTest1");
|
||||
mockPeerId2 = createMockPeerId("12D3KooWTest2");
|
||||
|
||||
libp2p = {
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
services: {
|
||||
ping: {
|
||||
ping: sinon.stub().resolves(100)
|
||||
}
|
||||
},
|
||||
peerStore: {
|
||||
merge: sinon.stub().resolves()
|
||||
}
|
||||
};
|
||||
|
||||
relay = {
|
||||
pubsubTopics: ["/waku/2/rs/1/0", "/waku/2/rs/1/1"],
|
||||
getMeshPeers: sinon.stub().returns(["12D3KooWTest1"]),
|
||||
send: sinon.stub().resolves()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (keepAliveManager) {
|
||||
keepAliveManager.stop();
|
||||
}
|
||||
clock.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create KeepAliveManager with required options", () => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p
|
||||
});
|
||||
|
||||
expect(keepAliveManager).to.be.instanceOf(KeepAliveManager);
|
||||
});
|
||||
|
||||
it("should create KeepAliveManager with relay", () => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
|
||||
expect(keepAliveManager).to.be.instanceOf(KeepAliveManager);
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p
|
||||
});
|
||||
});
|
||||
|
||||
it("should add event listeners for peer connect and disconnect", () => {
|
||||
keepAliveManager.start();
|
||||
|
||||
expect(libp2p.addEventListener.calledTwice).to.be.true;
|
||||
expect(
|
||||
libp2p.addEventListener.calledWith("peer:connect", sinon.match.func)
|
||||
).to.be.true;
|
||||
expect(
|
||||
libp2p.addEventListener.calledWith("peer:disconnect", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
keepAliveManager.start();
|
||||
keepAliveManager.start();
|
||||
|
||||
expect(libp2p.addEventListener.callCount).to.equal(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should remove event listeners", () => {
|
||||
keepAliveManager.stop();
|
||||
|
||||
expect(libp2p.removeEventListener.calledTwice).to.be.true;
|
||||
expect(
|
||||
libp2p.removeEventListener.calledWith("peer:connect", sinon.match.func)
|
||||
).to.be.true;
|
||||
expect(
|
||||
libp2p.removeEventListener.calledWith(
|
||||
"peer:disconnect",
|
||||
sinon.match.func
|
||||
)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should clear all timers", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
const timersBeforeStop = clock.countTimers();
|
||||
expect(timersBeforeStop).to.be.greaterThan(0);
|
||||
|
||||
keepAliveManager.stop();
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should be safe to call multiple times", () => {
|
||||
keepAliveManager.stop();
|
||||
keepAliveManager.stop();
|
||||
|
||||
expect(libp2p.removeEventListener.callCount).to.equal(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("peer connect event handling", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should start ping timers on peer connect", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle multiple peer connections", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent1 = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
const connectEvent2 = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId2
|
||||
});
|
||||
|
||||
peerConnectHandler(connectEvent1);
|
||||
peerConnectHandler(connectEvent2);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("peer disconnect event handling", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should stop ping timers on peer disconnect", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const peerDisconnectHandler = libp2p.addEventListener.getCall(1).args[1];
|
||||
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
const timerCountAfterConnect = clock.countTimers();
|
||||
expect(timerCountAfterConnect).to.be.greaterThan(0);
|
||||
|
||||
const disconnectEvent = new CustomEvent("peer:disconnect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerDisconnectHandler(disconnectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.lessThan(timerCountAfterConnect);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ping timer management", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should create ping timers when pingKeepAlive > 0", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("should not create ping timers when pingKeepAlive = 0", () => {
|
||||
keepAliveManager.stop();
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: { pingKeepAlive: 0, relayKeepAlive: 0 },
|
||||
libp2p
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(2).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should perform ping and update peer store on timer", async () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
clock.tick(defaultOptions.pingKeepAlive * 1000);
|
||||
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.calledWith(libp2p.services.ping.ping, mockPeerId);
|
||||
sinon.assert.calledWith(
|
||||
libp2p.peerStore.merge,
|
||||
mockPeerId,
|
||||
sinon.match.object
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle ping failures gracefully", async () => {
|
||||
libp2p.services.ping.ping.rejects(new Error("Ping failed"));
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
clock.tick(defaultOptions.pingKeepAlive * 1000);
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.calledWith(libp2p.services.ping.ping, mockPeerId);
|
||||
sinon.assert.notCalled(libp2p.peerStore.merge);
|
||||
});
|
||||
|
||||
it("should handle peer store update failures gracefully", async () => {
|
||||
libp2p.peerStore.merge.rejects(new Error("Peer store update failed"));
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
clock.tick(defaultOptions.pingKeepAlive * 1000);
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.calledWith(libp2p.services.ping.ping, mockPeerId);
|
||||
sinon.assert.calledWith(
|
||||
libp2p.peerStore.merge,
|
||||
mockPeerId,
|
||||
sinon.match.object
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relay timer management", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should create relay timers when relay exists and relayKeepAlive > 0", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(1);
|
||||
});
|
||||
|
||||
it("should not create relay timers when relayKeepAlive = 0", () => {
|
||||
keepAliveManager.stop();
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: { pingKeepAlive: 30, relayKeepAlive: 0 },
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(2).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
|
||||
it("should not create relay timers when relay is not provided", () => {
|
||||
keepAliveManager.stop();
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(2).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
|
||||
it("should create timers for each pubsub topic where peer is in mesh", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(relay.pubsubTopics.length);
|
||||
});
|
||||
|
||||
it("should not create timers for topics where peer is not in mesh", () => {
|
||||
relay.getMeshPeers.returns([]);
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
|
||||
it("should send relay ping messages on timer", async () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
clock.tick(defaultOptions.relayKeepAlive * 1000);
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.called(relay.send);
|
||||
});
|
||||
|
||||
it("should handle relay send failures gracefully", async () => {
|
||||
relay.send.rejects(new Error("Relay send failed"));
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
clock.tick(defaultOptions.relayKeepAlive * 1000);
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.called(relay.send);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timer cleanup", () => {
|
||||
beforeEach(() => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
});
|
||||
|
||||
it("should clear timers for specific peer on disconnect", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const peerDisconnectHandler = libp2p.addEventListener.getCall(1).args[1];
|
||||
|
||||
const connectEvent1 = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
const connectEvent2 = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId2
|
||||
});
|
||||
peerConnectHandler(connectEvent1);
|
||||
peerConnectHandler(connectEvent2);
|
||||
|
||||
const timerCountAfterConnect = clock.countTimers();
|
||||
expect(timerCountAfterConnect).to.be.greaterThan(0);
|
||||
|
||||
const disconnectEvent = new CustomEvent("peer:disconnect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerDisconnectHandler(disconnectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.lessThan(timerCountAfterConnect);
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle disconnect when peer has no timers", () => {
|
||||
const peerDisconnectHandler = libp2p.addEventListener.getCall(1).args[1];
|
||||
const disconnectEvent = new CustomEvent("peer:disconnect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
|
||||
expect(() => peerDisconnectHandler(disconnectEvent)).to.not.throw();
|
||||
});
|
||||
|
||||
it("should clear existing timers before creating new ones", () => {
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
|
||||
peerConnectHandler(connectEvent);
|
||||
const timerCountAfterFirst = clock.countTimers();
|
||||
|
||||
peerConnectHandler(connectEvent);
|
||||
const timerCountAfterSecond = clock.countTimers();
|
||||
|
||||
expect(timerCountAfterSecond).to.equal(timerCountAfterFirst);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty pubsub topics", () => {
|
||||
const emptyRelay = {
|
||||
pubsubTopics: [],
|
||||
getMeshPeers: sinon.stub().returns([]),
|
||||
send: sinon.stub().resolves()
|
||||
} as any;
|
||||
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay: emptyRelay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
|
||||
it("should handle all zero keep alive options", () => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: { pingKeepAlive: 0, relayKeepAlive: 0 },
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should handle peer not in mesh for all topics", () => {
|
||||
relay.getMeshPeers.returns(["different-peer-id"]);
|
||||
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
it("should handle complete peer lifecycle", async () => {
|
||||
keepAliveManager = new KeepAliveManager({
|
||||
options: defaultOptions,
|
||||
libp2p,
|
||||
relay
|
||||
});
|
||||
keepAliveManager.start();
|
||||
|
||||
const peerConnectHandler = libp2p.addEventListener.getCall(0).args[1];
|
||||
const peerDisconnectHandler = libp2p.addEventListener.getCall(1).args[1];
|
||||
|
||||
const connectEvent = new CustomEvent("peer:connect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerConnectHandler(connectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.be.greaterThan(0);
|
||||
|
||||
clock.tick(
|
||||
Math.max(defaultOptions.pingKeepAlive, defaultOptions.relayKeepAlive) *
|
||||
1000
|
||||
);
|
||||
await clock.tickAsync(0);
|
||||
|
||||
sinon.assert.called(libp2p.services.ping.ping);
|
||||
sinon.assert.called(relay.send);
|
||||
|
||||
const disconnectEvent = new CustomEvent("peer:disconnect", {
|
||||
detail: mockPeerId
|
||||
});
|
||||
peerDisconnectHandler(disconnectEvent);
|
||||
|
||||
expect(clock.countTimers()).to.equal(0);
|
||||
|
||||
keepAliveManager.stop();
|
||||
|
||||
sinon.assert.called(libp2p.removeEventListener);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import type { IRelay, Libp2p, PeerIdStr } from "@waku/interfaces";
|
||||
import type { IEncoder, IRelay, Libp2p } from "@waku/interfaces";
|
||||
import { Logger, pubsubTopicToSingleShardInfo } from "@waku/utils";
|
||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
||||
|
||||
@ -19,7 +19,12 @@ type CreateKeepAliveManagerOptions = {
|
||||
relay?: IRelay;
|
||||
};
|
||||
|
||||
export class KeepAliveManager {
|
||||
interface IKeepAliveManager {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export class KeepAliveManager implements IKeepAliveManager {
|
||||
private readonly relay?: IRelay;
|
||||
private readonly libp2p: Libp2p;
|
||||
|
||||
@ -27,7 +32,7 @@ export class KeepAliveManager {
|
||||
|
||||
private pingKeepAliveTimers: Map<string, ReturnType<typeof setInterval>> =
|
||||
new Map();
|
||||
private relayKeepAliveTimers: Map<PeerId, ReturnType<typeof setInterval>[]> =
|
||||
private relayKeepAliveTimers: Map<string, ReturnType<typeof setInterval>[]> =
|
||||
new Map();
|
||||
|
||||
public constructor({
|
||||
@ -38,122 +43,184 @@ export class KeepAliveManager {
|
||||
this.options = options;
|
||||
this.relay = relay;
|
||||
this.libp2p = libp2p;
|
||||
|
||||
this.onPeerConnect = this.onPeerConnect.bind(this);
|
||||
this.onPeerDisconnect = this.onPeerDisconnect.bind(this);
|
||||
}
|
||||
|
||||
public start(peerId: PeerId): void {
|
||||
// Just in case a timer already exists for this peer
|
||||
this.stop(peerId);
|
||||
|
||||
const { pingKeepAlive: pingPeriodSecs, relayKeepAlive: relayPeriodSecs } =
|
||||
this.options;
|
||||
|
||||
const peerIdStr = peerId.toString();
|
||||
|
||||
// Ping the peer every pingPeriodSecs seconds
|
||||
// if pingPeriodSecs is 0, don't ping the peer
|
||||
if (pingPeriodSecs !== 0) {
|
||||
const interval = setInterval(() => {
|
||||
void (async () => {
|
||||
let ping: number;
|
||||
try {
|
||||
// ping the peer for keep alive
|
||||
// also update the peer store with the latency
|
||||
try {
|
||||
ping = await this.libp2p.services.ping.ping(peerId);
|
||||
log.info(`Ping succeeded (${peerIdStr})`, ping);
|
||||
} catch (error) {
|
||||
log.error(`Ping failed for peer (${peerIdStr}).
|
||||
Next ping will be attempted in ${pingPeriodSecs} seconds.
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.libp2p.peerStore.merge(peerId, {
|
||||
metadata: {
|
||||
ping: utf8ToBytes(ping.toString())
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
log.error("Failed to update ping", e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`Ping failed (${peerIdStr})`, e);
|
||||
}
|
||||
})();
|
||||
}, pingPeriodSecs * 1000);
|
||||
|
||||
this.pingKeepAliveTimers.set(peerIdStr, interval);
|
||||
}
|
||||
|
||||
const relay = this.relay;
|
||||
if (relay && relayPeriodSecs !== 0) {
|
||||
const intervals = this.scheduleRelayPings(
|
||||
relay,
|
||||
relayPeriodSecs,
|
||||
peerId.toString()
|
||||
);
|
||||
this.relayKeepAliveTimers.set(peerId, intervals);
|
||||
}
|
||||
public start(): void {
|
||||
this.libp2p.addEventListener("peer:connect", this.onPeerConnect);
|
||||
this.libp2p.addEventListener("peer:disconnect", this.onPeerDisconnect);
|
||||
}
|
||||
|
||||
public stop(peerId: PeerId): void {
|
||||
const peerIdStr = peerId.toString();
|
||||
public stop(): void {
|
||||
this.libp2p.removeEventListener("peer:connect", this.onPeerConnect);
|
||||
this.libp2p.removeEventListener("peer:disconnect", this.onPeerDisconnect);
|
||||
|
||||
if (this.pingKeepAliveTimers.has(peerIdStr)) {
|
||||
clearInterval(this.pingKeepAliveTimers.get(peerIdStr));
|
||||
this.pingKeepAliveTimers.delete(peerIdStr);
|
||||
}
|
||||
|
||||
if (this.relayKeepAliveTimers.has(peerId)) {
|
||||
this.relayKeepAliveTimers.get(peerId)?.map(clearInterval);
|
||||
this.relayKeepAliveTimers.delete(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
public stopAll(): void {
|
||||
for (const timer of [
|
||||
...Object.values(this.pingKeepAliveTimers),
|
||||
...Object.values(this.relayKeepAliveTimers)
|
||||
]) {
|
||||
for (const timer of this.pingKeepAliveTimers.values()) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
for (const timerArray of this.relayKeepAliveTimers.values()) {
|
||||
for (const timer of timerArray) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}
|
||||
|
||||
this.pingKeepAliveTimers.clear();
|
||||
this.relayKeepAliveTimers.clear();
|
||||
}
|
||||
|
||||
public connectionsExist(): boolean {
|
||||
return (
|
||||
this.pingKeepAliveTimers.size > 0 || this.relayKeepAliveTimers.size > 0
|
||||
);
|
||||
private onPeerConnect(evt: CustomEvent<PeerId>): void {
|
||||
const peerId = evt.detail;
|
||||
this.startPingForPeer(peerId);
|
||||
}
|
||||
|
||||
private scheduleRelayPings(
|
||||
relay: IRelay,
|
||||
relayPeriodSecs: number,
|
||||
peerIdStr: PeerIdStr
|
||||
): NodeJS.Timeout[] {
|
||||
// send a ping message to each PubsubTopic the peer is part of
|
||||
private onPeerDisconnect(evt: CustomEvent<PeerId>): void {
|
||||
const peerId = evt.detail;
|
||||
this.stopPingForPeer(peerId);
|
||||
}
|
||||
|
||||
private startPingForPeer(peerId: PeerId): void {
|
||||
// Just in case a timer already exists for this peer
|
||||
this.stopPingForPeer(peerId);
|
||||
|
||||
this.startLibp2pPing(peerId);
|
||||
this.startRelayPing(peerId);
|
||||
}
|
||||
|
||||
private stopPingForPeer(peerId: PeerId): void {
|
||||
this.stopLibp2pPing(peerId);
|
||||
this.stopRelayPing(peerId);
|
||||
}
|
||||
|
||||
private startLibp2pPing(peerId: PeerId): void {
|
||||
if (this.options.pingKeepAlive === 0) {
|
||||
log.warn(
|
||||
`Ping keep alive is disabled pingKeepAlive:${this.options.pingKeepAlive}, skipping start for libp2p ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const peerIdStr = peerId.toString();
|
||||
|
||||
if (this.pingKeepAliveTimers.has(peerIdStr)) {
|
||||
log.warn(
|
||||
`Ping already started for peer: ${peerIdStr}, skipping start for libp2p ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void this.pingLibp2p(peerId);
|
||||
}, this.options.pingKeepAlive * 1000);
|
||||
|
||||
this.pingKeepAliveTimers.set(peerIdStr, interval);
|
||||
}
|
||||
|
||||
private stopLibp2pPing(peerId: PeerId): void {
|
||||
const peerIdStr = peerId.toString();
|
||||
|
||||
if (!this.pingKeepAliveTimers.has(peerIdStr)) {
|
||||
log.warn(
|
||||
`Ping not started for peer: ${peerIdStr}, skipping stop for ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.pingKeepAliveTimers.get(peerIdStr));
|
||||
this.pingKeepAliveTimers.delete(peerIdStr);
|
||||
}
|
||||
|
||||
private startRelayPing(peerId: PeerId): void {
|
||||
if (!this.relay) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.relayKeepAlive === 0) {
|
||||
log.warn(
|
||||
`Relay keep alive is disabled relayKeepAlive:${this.options.relayKeepAlive}, skipping start for relay ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.relayKeepAliveTimers.has(peerId.toString())) {
|
||||
log.warn(
|
||||
`Relay ping already started for peer: ${peerId.toString()}, skipping start for relay ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const intervals: NodeJS.Timeout[] = [];
|
||||
for (const topic of relay.pubsubTopics) {
|
||||
const meshPeers = relay.getMeshPeers(topic);
|
||||
if (!meshPeers.includes(peerIdStr)) continue;
|
||||
|
||||
for (const topic of this.relay.pubsubTopics) {
|
||||
const meshPeers = this.relay.getMeshPeers(topic);
|
||||
|
||||
if (!meshPeers.includes(peerId.toString())) {
|
||||
log.warn(
|
||||
`Peer: ${peerId.toString()} is not in the mesh for topic: ${topic}, skipping start for relay ping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encoder = createEncoder({
|
||||
pubsubTopicShardInfo: pubsubTopicToSingleShardInfo(topic),
|
||||
contentTopic: RelayPingContentTopic,
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
const interval = setInterval(() => {
|
||||
log.info("Sending Waku Relay ping message");
|
||||
relay
|
||||
.send(encoder, { payload: new Uint8Array([1]) })
|
||||
.catch((e) => log.error("Failed to send relay ping", e));
|
||||
}, relayPeriodSecs * 1000);
|
||||
void this.pingRelay(encoder);
|
||||
}, this.options.relayKeepAlive * 1000);
|
||||
|
||||
intervals.push(interval);
|
||||
}
|
||||
|
||||
return intervals;
|
||||
this.relayKeepAliveTimers.set(peerId.toString(), intervals);
|
||||
}
|
||||
|
||||
private stopRelayPing(peerId: PeerId): void {
|
||||
if (!this.relay) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerIdStr = peerId.toString();
|
||||
|
||||
if (!this.relayKeepAliveTimers.has(peerIdStr)) {
|
||||
log.warn(
|
||||
`Relay ping not started for peer: ${peerIdStr}, skipping stop for relay ping`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.relayKeepAliveTimers.get(peerIdStr)?.map(clearInterval);
|
||||
this.relayKeepAliveTimers.delete(peerIdStr);
|
||||
}
|
||||
|
||||
private async pingRelay(encoder: IEncoder): Promise<void> {
|
||||
try {
|
||||
log.info("Sending Waku Relay ping message");
|
||||
await this.relay!.send(encoder, { payload: new Uint8Array([1]) });
|
||||
} catch (e) {
|
||||
log.error("Failed to send relay ping", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async pingLibp2p(peerId: PeerId): Promise<void> {
|
||||
try {
|
||||
log.info(`Pinging libp2p peer (${peerId.toString()})`);
|
||||
const ping = await this.libp2p.services.ping.ping(peerId);
|
||||
|
||||
log.info(`Ping succeeded (${peerId.toString()})`, ping);
|
||||
|
||||
await this.libp2p.peerStore.merge(peerId, {
|
||||
metadata: {
|
||||
ping: utf8ToBytes(ping.toString())
|
||||
}
|
||||
});
|
||||
log.info(`Ping updated for peer (${peerId.toString()})`);
|
||||
} catch (e) {
|
||||
log.error(`Ping failed for peer (${peerId.toString()})`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
431
packages/core/src/lib/connection_manager/network_monitor.spec.ts
Normal file
431
packages/core/src/lib/connection_manager/network_monitor.spec.ts
Normal file
@ -0,0 +1,431 @@
|
||||
import { IWakuEventEmitter, Libp2p } from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { NetworkMonitor } from "./network_monitor.js";
|
||||
|
||||
describe("NetworkMonitor", () => {
|
||||
let libp2p: Libp2p;
|
||||
let events: IWakuEventEmitter;
|
||||
let networkMonitor: NetworkMonitor;
|
||||
let originalGlobalThis: typeof globalThis;
|
||||
let mockGlobalThis: {
|
||||
addEventListener: sinon.SinonStub;
|
||||
removeEventListener: sinon.SinonStub;
|
||||
navigator: { onLine: boolean } | undefined;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
libp2p = {
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
getConnections: sinon.stub().returns([])
|
||||
} as unknown as Libp2p;
|
||||
|
||||
events = {
|
||||
dispatchEvent: sinon.stub()
|
||||
} as unknown as IWakuEventEmitter;
|
||||
|
||||
originalGlobalThis = globalThis;
|
||||
mockGlobalThis = {
|
||||
addEventListener: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
navigator: {
|
||||
onLine: true
|
||||
}
|
||||
};
|
||||
|
||||
(global as unknown as { globalThis: typeof mockGlobalThis }).globalThis =
|
||||
mockGlobalThis;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (networkMonitor) {
|
||||
networkMonitor.stop();
|
||||
}
|
||||
|
||||
(
|
||||
global as unknown as { globalThis: typeof originalGlobalThis }
|
||||
).globalThis = originalGlobalThis;
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create NetworkMonitor with libp2p and events", () => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
|
||||
expect(networkMonitor).to.be.instanceOf(NetworkMonitor);
|
||||
});
|
||||
|
||||
it("should initialize with isNetworkConnected as false", () => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("start", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
});
|
||||
|
||||
it("should add event listeners to libp2p", () => {
|
||||
networkMonitor.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
expect(addEventListenerStub.calledTwice).to.be.true;
|
||||
expect(addEventListenerStub.calledWith("peer:connect", sinon.match.func))
|
||||
.to.be.true;
|
||||
expect(
|
||||
addEventListenerStub.calledWith("peer:disconnect", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should add event listeners to globalThis", () => {
|
||||
networkMonitor.start();
|
||||
|
||||
expect(mockGlobalThis.addEventListener.calledTwice).to.be.true;
|
||||
expect(
|
||||
mockGlobalThis.addEventListener.calledWith("online", sinon.match.func)
|
||||
).to.be.true;
|
||||
expect(
|
||||
mockGlobalThis.addEventListener.calledWith("offline", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle errors when globalThis is not available", () => {
|
||||
mockGlobalThis.addEventListener.throws(new Error("No globalThis"));
|
||||
|
||||
expect(() => networkMonitor.start()).to.not.throw();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
expect(addEventListenerStub.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
networkMonitor.start();
|
||||
});
|
||||
|
||||
it("should remove event listeners from libp2p", () => {
|
||||
networkMonitor.stop();
|
||||
|
||||
const removeEventListenerStub =
|
||||
libp2p.removeEventListener as sinon.SinonStub;
|
||||
expect(removeEventListenerStub.calledTwice).to.be.true;
|
||||
expect(
|
||||
removeEventListenerStub.calledWith("peer:connect", sinon.match.func)
|
||||
).to.be.true;
|
||||
expect(
|
||||
removeEventListenerStub.calledWith("peer:disconnect", sinon.match.func)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should remove event listeners from globalThis", () => {
|
||||
networkMonitor.stop();
|
||||
|
||||
expect(mockGlobalThis.removeEventListener.calledTwice).to.be.true;
|
||||
expect(
|
||||
mockGlobalThis.removeEventListener.calledWith(
|
||||
"online",
|
||||
sinon.match.func
|
||||
)
|
||||
).to.be.true;
|
||||
expect(
|
||||
mockGlobalThis.removeEventListener.calledWith(
|
||||
"offline",
|
||||
sinon.match.func
|
||||
)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle errors when removing globalThis listeners", () => {
|
||||
mockGlobalThis.removeEventListener.throws(new Error("Remove failed"));
|
||||
|
||||
expect(() => networkMonitor.stop()).to.not.throw();
|
||||
|
||||
const removeEventListenerStub =
|
||||
libp2p.removeEventListener as sinon.SinonStub;
|
||||
expect(removeEventListenerStub.calledTwice).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConnected", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false when navigator.onLine is false", () => {
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = false;
|
||||
}
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when navigator.onLine is true but network is not connected", () => {
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = true;
|
||||
}
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle case when navigator is not available", () => {
|
||||
mockGlobalThis.navigator = undefined;
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle case when globalThis is not available", () => {
|
||||
(global as unknown as { globalThis: undefined }).globalThis = undefined;
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("peer connection events", () => {
|
||||
let connectHandler: () => void;
|
||||
let disconnectHandler: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
networkMonitor.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
|
||||
connectHandler = addEventListenerStub.getCall(0).args[1];
|
||||
disconnectHandler = addEventListenerStub.getCall(1).args[1];
|
||||
});
|
||||
|
||||
it("should handle peer connect event", () => {
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
|
||||
connectHandler();
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
expect(dispatchEventStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle peer disconnect event when no connections remain", () => {
|
||||
connectHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
dispatchEventStub.resetHistory();
|
||||
|
||||
const getConnectionsStub = libp2p.getConnections as sinon.SinonStub;
|
||||
getConnectionsStub.returns([]);
|
||||
|
||||
disconnectHandler();
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
expect(dispatchEventStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should not change state when connections remain after disconnect", () => {
|
||||
connectHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
dispatchEventStub.resetHistory();
|
||||
|
||||
const getConnectionsStub = libp2p.getConnections as sinon.SinonStub;
|
||||
getConnectionsStub.returns([{ id: "connection1" }]);
|
||||
|
||||
disconnectHandler();
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
expect(dispatchEventStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should not dispatch event when already connected", () => {
|
||||
connectHandler();
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
dispatchEventStub.resetHistory();
|
||||
|
||||
connectHandler();
|
||||
|
||||
expect(dispatchEventStub.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should not dispatch event when already disconnected", () => {
|
||||
connectHandler();
|
||||
|
||||
const getConnectionsStub = libp2p.getConnections as sinon.SinonStub;
|
||||
getConnectionsStub.returns([]);
|
||||
disconnectHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
dispatchEventStub.resetHistory();
|
||||
|
||||
disconnectHandler();
|
||||
|
||||
expect(dispatchEventStub.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser online/offline events", () => {
|
||||
let onlineHandler: () => void;
|
||||
let offlineHandler: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
networkMonitor.start();
|
||||
|
||||
onlineHandler = mockGlobalThis.addEventListener.getCall(0).args[1];
|
||||
offlineHandler = mockGlobalThis.addEventListener.getCall(1).args[1];
|
||||
});
|
||||
|
||||
it("should dispatch network event when browser goes online", () => {
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = true;
|
||||
}
|
||||
|
||||
onlineHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
expect(dispatchEventStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should dispatch network event when browser goes offline", () => {
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = false;
|
||||
}
|
||||
|
||||
offlineHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
expect(dispatchEventStub.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("dispatchNetworkEvent", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch CustomEvent with correct type and detail", () => {
|
||||
networkMonitor.start();
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
const connectHandler = addEventListenerStub.getCall(0).args[1];
|
||||
connectHandler();
|
||||
|
||||
const dispatchEventStub = events.dispatchEvent as sinon.SinonStub;
|
||||
expect(dispatchEventStub.calledOnce).to.be.true;
|
||||
const dispatchedEvent = dispatchEventStub.getCall(0)
|
||||
.args[0] as CustomEvent<boolean>;
|
||||
expect(dispatchedEvent).to.be.instanceOf(CustomEvent);
|
||||
expect(dispatchedEvent.type).to.equal("waku:connection");
|
||||
expect(dispatchedEvent.detail).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle errors when getting connections", () => {
|
||||
const getConnectionsStub = libp2p.getConnections as sinon.SinonStub;
|
||||
getConnectionsStub.throws(new Error("Get connections failed"));
|
||||
|
||||
networkMonitor.start();
|
||||
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
const connectHandler = addEventListenerStub.getCall(0).args[1];
|
||||
const disconnectHandler = addEventListenerStub.getCall(1).args[1];
|
||||
|
||||
connectHandler();
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
|
||||
expect(() => disconnectHandler()).to.throw("Get connections failed");
|
||||
});
|
||||
|
||||
it("should handle errors when accessing navigator", () => {
|
||||
Object.defineProperty(mockGlobalThis, "navigator", {
|
||||
get: () => {
|
||||
throw new Error("Navigator access failed");
|
||||
}
|
||||
});
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", () => {
|
||||
beforeEach(() => {
|
||||
networkMonitor = new NetworkMonitor({
|
||||
libp2p,
|
||||
events
|
||||
});
|
||||
networkMonitor.start();
|
||||
});
|
||||
|
||||
it("should handle complete connection lifecycle", () => {
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
const connectHandler = addEventListenerStub.getCall(0).args[1];
|
||||
const disconnectHandler = addEventListenerStub.getCall(1).args[1];
|
||||
const getConnectionsStub = libp2p.getConnections as sinon.SinonStub;
|
||||
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
|
||||
connectHandler();
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
|
||||
getConnectionsStub.returns([{ id: "other" }]);
|
||||
disconnectHandler();
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
|
||||
getConnectionsStub.returns([]);
|
||||
disconnectHandler();
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle browser offline state overriding peer connections", () => {
|
||||
const addEventListenerStub = libp2p.addEventListener as sinon.SinonStub;
|
||||
const connectHandler = addEventListenerStub.getCall(0).args[1];
|
||||
|
||||
connectHandler();
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = false;
|
||||
}
|
||||
expect(networkMonitor.isConnected()).to.be.false;
|
||||
|
||||
if (mockGlobalThis.navigator) {
|
||||
mockGlobalThis.navigator.onLine = true;
|
||||
}
|
||||
expect(networkMonitor.isConnected()).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
112
packages/core/src/lib/connection_manager/network_monitor.ts
Normal file
112
packages/core/src/lib/connection_manager/network_monitor.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { IWakuEventEmitter, Libp2p } from "@waku/interfaces";
|
||||
|
||||
type NetworkMonitorConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
events: IWakuEventEmitter;
|
||||
};
|
||||
|
||||
interface INetworkMonitor {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
isConnected(): boolean;
|
||||
isP2PConnected(): boolean;
|
||||
isBrowserConnected(): boolean;
|
||||
}
|
||||
|
||||
export class NetworkMonitor implements INetworkMonitor {
|
||||
private readonly libp2p: Libp2p;
|
||||
private readonly events: IWakuEventEmitter;
|
||||
|
||||
private isNetworkConnected: boolean = false;
|
||||
|
||||
public constructor(options: NetworkMonitorConstructorOptions) {
|
||||
this.libp2p = options.libp2p;
|
||||
this.events = options.events;
|
||||
|
||||
this.onConnectedEvent = this.onConnectedEvent.bind(this);
|
||||
this.onDisconnectedEvent = this.onDisconnectedEvent.bind(this);
|
||||
this.dispatchNetworkEvent = this.dispatchNetworkEvent.bind(this);
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.libp2p.addEventListener("peer:connect", this.onConnectedEvent);
|
||||
this.libp2p.addEventListener("peer:disconnect", this.onDisconnectedEvent);
|
||||
|
||||
try {
|
||||
globalThis.addEventListener("online", this.dispatchNetworkEvent);
|
||||
globalThis.addEventListener("offline", this.dispatchNetworkEvent);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.libp2p.removeEventListener("peer:connect", this.onConnectedEvent);
|
||||
this.libp2p.removeEventListener(
|
||||
"peer:disconnect",
|
||||
this.onDisconnectedEvent
|
||||
);
|
||||
|
||||
try {
|
||||
globalThis.removeEventListener("online", this.dispatchNetworkEvent);
|
||||
globalThis.removeEventListener("offline", this.dispatchNetworkEvent);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the node is connected to the network via libp2p and browser.
|
||||
*/
|
||||
public isConnected(): boolean {
|
||||
if (!this.isBrowserConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isP2PConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the node is connected to the network via libp2p.
|
||||
*/
|
||||
public isP2PConnected(): boolean {
|
||||
return this.isNetworkConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the node is connected to the network via browser.
|
||||
*/
|
||||
public isBrowserConnected(): boolean {
|
||||
try {
|
||||
if (globalThis?.navigator && !globalThis?.navigator?.onLine) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private onConnectedEvent(): void {
|
||||
if (!this.isNetworkConnected) {
|
||||
this.isNetworkConnected = true;
|
||||
this.dispatchNetworkEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private onDisconnectedEvent(): void {
|
||||
if (this.isNetworkConnected && this.libp2p.getConnections().length === 0) {
|
||||
this.isNetworkConnected = false;
|
||||
this.dispatchNetworkEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchNetworkEvent(): void {
|
||||
this.events.dispatchEvent(
|
||||
new CustomEvent<boolean>("waku:connection", {
|
||||
detail: this.isConnected()
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
327
packages/core/src/lib/connection_manager/shard_reader.spec.ts
Normal file
327
packages/core/src/lib/connection_manager/shard_reader.spec.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import { PeerId } from "@libp2p/interface";
|
||||
import {
|
||||
NetworkConfig,
|
||||
PubsubTopic,
|
||||
ShardInfo,
|
||||
SingleShardInfo
|
||||
} from "@waku/interfaces";
|
||||
import { contentTopicToShardIndex, encodeRelayShard } from "@waku/utils";
|
||||
import { expect } from "chai";
|
||||
import { Libp2p } from "libp2p";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ShardReader } from "./shard_reader.js";
|
||||
|
||||
const createMockPeerId = (): PeerId => {
|
||||
const mockPeerId = {
|
||||
toString: () => "12D3KooWTest123",
|
||||
equals: (other: PeerId) => other.toString() === "12D3KooWTest123"
|
||||
};
|
||||
return mockPeerId as unknown as PeerId;
|
||||
};
|
||||
|
||||
describe("ShardReader", function () {
|
||||
let mockLibp2p: sinon.SinonStubbedInstance<Libp2p>;
|
||||
let mockPeerStore: any;
|
||||
let shardReader: ShardReader;
|
||||
let testPeerId: PeerId;
|
||||
|
||||
const testContentTopic = "/test/1/waku-light-push/utf8";
|
||||
const testClusterId = 3;
|
||||
const testShardIndex = contentTopicToShardIndex(testContentTopic);
|
||||
|
||||
const testNetworkConfig: NetworkConfig = {
|
||||
contentTopics: [testContentTopic],
|
||||
clusterId: testClusterId
|
||||
};
|
||||
|
||||
const testShardInfo: ShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shards: [testShardIndex]
|
||||
};
|
||||
|
||||
beforeEach(async function () {
|
||||
testPeerId = createMockPeerId();
|
||||
|
||||
mockPeerStore = {
|
||||
get: sinon.stub(),
|
||||
save: sinon.stub(),
|
||||
merge: sinon.stub()
|
||||
};
|
||||
|
||||
mockLibp2p = {
|
||||
peerStore: mockPeerStore
|
||||
} as any;
|
||||
|
||||
shardReader = new ShardReader({
|
||||
libp2p: mockLibp2p as any,
|
||||
networkConfig: testNetworkConfig
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", function () {
|
||||
it("should create ShardReader with contentTopics network config", function () {
|
||||
const config: NetworkConfig = {
|
||||
contentTopics: ["/test/1/waku-light-push/utf8"],
|
||||
clusterId: 3
|
||||
};
|
||||
|
||||
const reader = new ShardReader({
|
||||
libp2p: mockLibp2p as any,
|
||||
networkConfig: config
|
||||
});
|
||||
|
||||
expect(reader).to.be.instanceOf(ShardReader);
|
||||
});
|
||||
|
||||
it("should create ShardReader with shards network config", function () {
|
||||
const config: NetworkConfig = {
|
||||
clusterId: 3,
|
||||
shards: [1, 2, 3]
|
||||
};
|
||||
|
||||
const reader = new ShardReader({
|
||||
libp2p: mockLibp2p as any,
|
||||
networkConfig: config
|
||||
});
|
||||
|
||||
expect(reader).to.be.instanceOf(ShardReader);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPeerOnNetwork", function () {
|
||||
it("should return true when peer is on the same network", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.true;
|
||||
sinon.assert.calledWith(mockPeerStore.get, testPeerId);
|
||||
});
|
||||
|
||||
it("should return false when peer is on different cluster", async function () {
|
||||
const differentClusterShardInfo: ShardInfo = {
|
||||
clusterId: 5,
|
||||
shards: [1, 2]
|
||||
};
|
||||
const shardInfoBytes = encodeRelayShard(differentClusterShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer has no overlapping shards", async function () {
|
||||
const noOverlapShardInfo: ShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shards: [testShardIndex + 100, testShardIndex + 200] // Use different shards
|
||||
};
|
||||
const shardInfoBytes = encodeRelayShard(noOverlapShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer has no shard info", async function () {
|
||||
const mockPeer = {
|
||||
metadata: new Map()
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer is not found", async function () {
|
||||
mockPeerStore.get.rejects(new Error("Peer not found"));
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPeerOnShard", function () {
|
||||
it("should return true when peer is on the specified shard", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const shard: SingleShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shard: testShardIndex
|
||||
};
|
||||
|
||||
const result = await shardReader.isPeerOnShard(testPeerId, shard);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false when peer is on different cluster", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const shard: SingleShardInfo = {
|
||||
clusterId: 5,
|
||||
shard: testShardIndex
|
||||
};
|
||||
|
||||
const result = await shardReader.isPeerOnShard(testPeerId, shard);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer is not on the specified shard", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const shard: SingleShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shard: testShardIndex + 100
|
||||
};
|
||||
|
||||
const result = await shardReader.isPeerOnShard(testPeerId, shard);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when shard info is undefined", async function () {
|
||||
const shard: SingleShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shard: undefined
|
||||
};
|
||||
|
||||
const result = await shardReader.isPeerOnShard(testPeerId, shard);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer shard info is not found", async function () {
|
||||
mockPeerStore.get.rejects(new Error("Peer not found"));
|
||||
|
||||
const shard: SingleShardInfo = {
|
||||
clusterId: testClusterId,
|
||||
shard: testShardIndex
|
||||
};
|
||||
|
||||
const result = await shardReader.isPeerOnShard(testPeerId, shard);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPeerOnTopic", function () {
|
||||
it("should return true when peer is on the pubsub topic shard", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const pubsubTopic: PubsubTopic = `/waku/2/rs/${testClusterId}/${testShardIndex}`;
|
||||
|
||||
const result = await shardReader.isPeerOnTopic(testPeerId, pubsubTopic);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false when peer is not on the pubsub topic shard", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const pubsubTopic: PubsubTopic = `/waku/2/rs/${testClusterId}/${testShardIndex + 100}`;
|
||||
|
||||
const result = await shardReader.isPeerOnTopic(testPeerId, pubsubTopic);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when pubsub topic parsing fails", async function () {
|
||||
const shardInfoBytes = encodeRelayShard(testShardInfo);
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", shardInfoBytes]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const invalidPubsubTopic: PubsubTopic = "/invalid/topic";
|
||||
|
||||
const result = await shardReader.isPeerOnTopic(
|
||||
testPeerId,
|
||||
invalidPubsubTopic
|
||||
);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should return false when peer is not found", async function () {
|
||||
mockPeerStore.get.rejects(new Error("Peer not found"));
|
||||
|
||||
const pubsubTopic: PubsubTopic = `/waku/2/rs/${testClusterId}/${testShardIndex}`;
|
||||
|
||||
const result = await shardReader.isPeerOnTopic(testPeerId, pubsubTopic);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", function () {
|
||||
it("should handle errors gracefully when getting peer info", async function () {
|
||||
mockPeerStore.get.rejects(new Error("Network error"));
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it("should handle corrupted shard info gracefully", async function () {
|
||||
const mockPeer = {
|
||||
metadata: new Map([["shardInfo", new Uint8Array([1, 2, 3])]])
|
||||
};
|
||||
|
||||
mockPeerStore.get.resolves(mockPeer);
|
||||
|
||||
const result = await shardReader.isPeerOnNetwork(testPeerId);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
134
packages/core/src/lib/connection_manager/shard_reader.ts
Normal file
134
packages/core/src/lib/connection_manager/shard_reader.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import type {
|
||||
NetworkConfig,
|
||||
PubsubTopic,
|
||||
ShardInfo,
|
||||
SingleShardInfo,
|
||||
StaticSharding
|
||||
} from "@waku/interfaces";
|
||||
import {
|
||||
contentTopicToShardIndex,
|
||||
decodeRelayShard,
|
||||
Logger,
|
||||
pubsubTopicToSingleShardInfo
|
||||
} from "@waku/utils";
|
||||
import { Libp2p } from "libp2p";
|
||||
|
||||
const log = new Logger("shard-reader");
|
||||
|
||||
type ShardReaderConstructorOptions = {
|
||||
libp2p: Libp2p;
|
||||
networkConfig: NetworkConfig;
|
||||
};
|
||||
|
||||
interface IShardReader {
|
||||
hasShardInfo(id: PeerId): Promise<boolean>;
|
||||
isPeerOnNetwork(id: PeerId): Promise<boolean>;
|
||||
isPeerOnShard(id: PeerId, shard: SingleShardInfo): Promise<boolean>;
|
||||
isPeerOnTopic(id: PeerId, pubsubTopic: PubsubTopic): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is responsible for reading the shard info from the libp2p peer store or from the current node's network config.
|
||||
*/
|
||||
export class ShardReader implements IShardReader {
|
||||
private readonly libp2p: Libp2p;
|
||||
|
||||
private readonly staticShard: StaticSharding;
|
||||
|
||||
public constructor(options: ShardReaderConstructorOptions) {
|
||||
this.libp2p = options.libp2p;
|
||||
|
||||
this.staticShard = this.getStaticShardFromNetworkConfig(
|
||||
options.networkConfig
|
||||
);
|
||||
}
|
||||
|
||||
public async isPeerOnNetwork(id: PeerId): Promise<boolean> {
|
||||
const shardInfo = await this.getShardInfo(id);
|
||||
|
||||
if (!shardInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const clusterMatch = shardInfo.clusterId === this.staticShard.clusterId;
|
||||
const shardOverlap = this.staticShard.shards.some((s) =>
|
||||
shardInfo.shards.includes(s)
|
||||
);
|
||||
|
||||
return clusterMatch && shardOverlap;
|
||||
}
|
||||
|
||||
public async hasShardInfo(id: PeerId): Promise<boolean> {
|
||||
const shardInfo = await this.getShardInfo(id);
|
||||
return !!shardInfo;
|
||||
}
|
||||
|
||||
public async isPeerOnTopic(
|
||||
id: PeerId,
|
||||
pubsubTopic: PubsubTopic
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const shardInfo = pubsubTopicToSingleShardInfo(pubsubTopic);
|
||||
return await this.isPeerOnShard(id, shardInfo);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Error comparing pubsub topic ${pubsubTopic} with shard info for ${id}`,
|
||||
error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async isPeerOnShard(
|
||||
id: PeerId,
|
||||
shard: SingleShardInfo
|
||||
): Promise<boolean> {
|
||||
const peerShardInfo = await this.getShardInfo(id);
|
||||
|
||||
if (!peerShardInfo || shard.shard === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
peerShardInfo.clusterId === shard.clusterId &&
|
||||
peerShardInfo.shards.includes(shard.shard)
|
||||
);
|
||||
}
|
||||
|
||||
private async getShardInfo(id: PeerId): Promise<ShardInfo | undefined> {
|
||||
try {
|
||||
const peer = await this.libp2p.peerStore.get(id);
|
||||
|
||||
const shardInfoBytes = peer.metadata.get("shardInfo");
|
||||
|
||||
if (!shardInfoBytes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const decodedShardInfo = decodeRelayShard(shardInfoBytes);
|
||||
|
||||
return decodedShardInfo;
|
||||
} catch (error) {
|
||||
log.error(`Error getting shard info for ${id}`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getStaticShardFromNetworkConfig(
|
||||
networkConfig: NetworkConfig
|
||||
): StaticSharding {
|
||||
if ("shards" in networkConfig) {
|
||||
return networkConfig;
|
||||
}
|
||||
|
||||
const shards = networkConfig.contentTopics.map((topic) =>
|
||||
contentTopicToShardIndex(topic)
|
||||
);
|
||||
|
||||
return {
|
||||
clusterId: networkConfig.clusterId!,
|
||||
shards
|
||||
};
|
||||
}
|
||||
}
|
||||
46
packages/core/src/lib/connection_manager/utils.spec.ts
Normal file
46
packages/core/src/lib/connection_manager/utils.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { peerIdFromString } from "@libp2p/peer-id";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { mapToPeerId, mapToPeerIdOrMultiaddr } from "./utils.js";
|
||||
|
||||
describe("mapToPeerIdOrMultiaddr", () => {
|
||||
it("should return PeerId when PeerId is provided", async () => {
|
||||
const peerId = peerIdFromString(
|
||||
"12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE"
|
||||
);
|
||||
|
||||
const result = mapToPeerIdOrMultiaddr(peerId);
|
||||
|
||||
expect(result).to.equal(peerId);
|
||||
});
|
||||
|
||||
it("should return Multiaddr when Multiaddr input is provided", () => {
|
||||
const multiAddr =
|
||||
"/ip4/127.0.0.1/tcp/8000/p2p/12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE";
|
||||
|
||||
const result = mapToPeerIdOrMultiaddr(multiAddr);
|
||||
|
||||
expect(result.toString()).to.equal(multiAddr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapToPeerId", () => {
|
||||
it("should return PeerId when PeerId is provided", async () => {
|
||||
const peerId = peerIdFromString(
|
||||
"12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE"
|
||||
);
|
||||
const result = mapToPeerId(peerId);
|
||||
expect(result).to.equal(peerId);
|
||||
expect(result.toString()).to.equal(peerId.toString());
|
||||
});
|
||||
|
||||
it("should return PeerId when Multiaddr input is provided", () => {
|
||||
const multiAddr =
|
||||
"/ip4/127.0.0.1/tcp/8000/p2p/12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE";
|
||||
|
||||
const result = mapToPeerId(multiAddr);
|
||||
expect(result.toString()).to.equal(
|
||||
"12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE"
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,6 @@
|
||||
import type { Peer } from "@libp2p/interface";
|
||||
import { isPeerId, type Peer, type PeerId } from "@libp2p/interface";
|
||||
import { peerIdFromString } from "@libp2p/peer-id";
|
||||
import { Multiaddr, multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import { bytesToUtf8 } from "@waku/utils/bytes";
|
||||
|
||||
/**
|
||||
@ -23,3 +25,27 @@ export const getPeerPing = (peer: Peer | null): number => {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a PeerId or MultiaddrInput to a PeerId or Multiaddr.
|
||||
* @param input - The PeerId or MultiaddrInput to map.
|
||||
* @returns The PeerId or Multiaddr.
|
||||
* @throws {Error} If the input is not a valid PeerId or MultiaddrInput.
|
||||
*/
|
||||
export const mapToPeerIdOrMultiaddr = (
|
||||
input: PeerId | MultiaddrInput
|
||||
): PeerId | Multiaddr => {
|
||||
return isPeerId(input) ? input : multiaddr(input);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a PeerId or MultiaddrInput to a PeerId.
|
||||
* @param input - The PeerId or MultiaddrInput to map.
|
||||
* @returns The PeerId.
|
||||
* @throws {Error} If the input is not a valid PeerId or MultiaddrInput.
|
||||
*/
|
||||
export const mapToPeerId = (input: PeerId | MultiaddrInput): PeerId => {
|
||||
return isPeerId(input)
|
||||
? input
|
||||
: peerIdFromString(multiaddr(input).getPeerId()!);
|
||||
};
|
||||
|
||||
@ -42,7 +42,6 @@ export class FilterCore {
|
||||
|
||||
public constructor(
|
||||
private handleIncomingMessage: IncomingMessageHandler,
|
||||
public readonly pubsubTopics: PubsubTopic[],
|
||||
libp2p: Libp2p
|
||||
) {
|
||||
this.streamManager = new StreamManager(
|
||||
|
||||
@ -5,7 +5,6 @@ import {
|
||||
type IMessage,
|
||||
type Libp2p,
|
||||
ProtocolError,
|
||||
PubsubTopic,
|
||||
type ThisOrThat
|
||||
} from "@waku/interfaces";
|
||||
import { PushResponse } from "@waku/proto";
|
||||
@ -36,10 +35,7 @@ export class LightPushCore {
|
||||
|
||||
public readonly multicodec = LightPushCodec;
|
||||
|
||||
public constructor(
|
||||
public readonly pubsubTopics: PubsubTopic[],
|
||||
libp2p: Libp2p
|
||||
) {
|
||||
public constructor(libp2p: Libp2p) {
|
||||
this.streamManager = new StreamManager(LightPushCodec, libp2p.components);
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { NodeCapabilityCount } from "@waku/interfaces";
|
||||
import { type NodeCapabilityCount, Tags } from "@waku/interfaces";
|
||||
|
||||
/**
|
||||
* The ENR tree for the different fleets.
|
||||
@ -10,7 +10,7 @@ export const enrTree = {
|
||||
TEST: "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im"
|
||||
};
|
||||
|
||||
export const DEFAULT_BOOTSTRAP_TAG_NAME = "bootstrap";
|
||||
export const DEFAULT_BOOTSTRAP_TAG_NAME = Tags.BOOTSTRAP;
|
||||
export const DEFAULT_BOOTSTRAP_TAG_VALUE = 50;
|
||||
export const DEFAULT_BOOTSTRAP_TAG_TTL = 100_000_000;
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from "@waku/interfaces";
|
||||
import { getWsMultiaddrFromMultiaddrs, Logger } from "@waku/utils";
|
||||
|
||||
const log = new Logger("peer-exchange-discovery");
|
||||
const log = new Logger("local-cache-discovery");
|
||||
|
||||
type LocalPeerCacheDiscoveryOptions = {
|
||||
tagName?: string;
|
||||
@ -23,7 +23,7 @@ type LocalPeerCacheDiscoveryOptions = {
|
||||
tagTTL?: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_LOCAL_TAG_NAME = Tags.LOCAL;
|
||||
const DEFAULT_LOCAL_TAG_NAME = Tags.LOCAL;
|
||||
const DEFAULT_LOCAL_TAG_VALUE = 50;
|
||||
const DEFAULT_LOCAL_TAG_TTL = 100_000_000;
|
||||
|
||||
|
||||
@ -6,6 +6,5 @@ export {
|
||||
export {
|
||||
wakuPeerExchangeDiscovery,
|
||||
PeerExchangeDiscovery,
|
||||
Options,
|
||||
DEFAULT_PEER_EXCHANGE_TAG_NAME
|
||||
Options
|
||||
} from "./waku_peer_exchange_discovery.js";
|
||||
|
||||
@ -54,7 +54,7 @@ interface CustomDiscoveryEvent extends PeerDiscoveryEvents {
|
||||
"waku:peer-exchange:started": CustomEvent<boolean>;
|
||||
}
|
||||
|
||||
export const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
|
||||
const DEFAULT_PEER_EXCHANGE_TAG_NAME = Tags.PEER_EXCHANGE;
|
||||
const DEFAULT_PEER_EXCHANGE_TAG_VALUE = 50;
|
||||
const DEFAULT_PEER_EXCHANGE_TAG_TTL = 100_000_000;
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Peer, PeerId, TypedEventEmitter } from "@libp2p/interface";
|
||||
import type { Peer, PeerId, Stream } from "@libp2p/interface";
|
||||
import type { MultiaddrInput } from "@multiformats/multiaddr";
|
||||
|
||||
import { PubsubTopic } from "./misc.js";
|
||||
import type { PubsubTopic } from "./misc.js";
|
||||
|
||||
export enum Tags {
|
||||
BOOTSTRAP = "bootstrap",
|
||||
@ -9,28 +10,13 @@ export enum Tags {
|
||||
}
|
||||
|
||||
export type ConnectionManagerOptions = {
|
||||
/**
|
||||
* Number of attempts before a peer is considered non-dialable.
|
||||
* This is used to not spam a peer with dial attempts when it is not dialable.
|
||||
*
|
||||
* @default 3
|
||||
*/
|
||||
maxDialAttemptsForPeer: number;
|
||||
|
||||
/**
|
||||
* Max number of bootstrap peers allowed to be connected to initially.
|
||||
* This is used to increase intention of dialing non-bootstrap peers, found using other discovery mechanisms (like Peer Exchange).
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
maxBootstrapPeersAllowed: number;
|
||||
|
||||
/**
|
||||
* Max number of parallel dials allowed.
|
||||
*
|
||||
* @default 3
|
||||
*/
|
||||
maxParallelDials: number;
|
||||
maxBootstrapPeers: number;
|
||||
|
||||
/**
|
||||
* Keep alive libp2p pings interval in seconds.
|
||||
@ -47,47 +33,107 @@ export type ConnectionManagerOptions = {
|
||||
relayKeepAlive: number;
|
||||
};
|
||||
|
||||
export enum EPeersByDiscoveryEvents {
|
||||
PEER_DISCOVERY_BOOTSTRAP = "peer:discovery:bootstrap",
|
||||
PEER_DISCOVERY_PEER_EXCHANGE = "peer:discovery:peer-exchange",
|
||||
PEER_CONNECT_BOOTSTRAP = "peer:connected:bootstrap",
|
||||
PEER_CONNECT_PEER_EXCHANGE = "peer:connected:peer-exchange"
|
||||
}
|
||||
export interface IConnectionManager {
|
||||
/**
|
||||
* Starts network monitoring, dialing discovered peers, keep-alive management, and connection limiting.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const connectionManager = new ConnectionManager(options);
|
||||
* connectionManager.start();
|
||||
* ```
|
||||
*/
|
||||
start(): void;
|
||||
|
||||
export interface IPeersByDiscoveryEvents {
|
||||
[EPeersByDiscoveryEvents.PEER_DISCOVERY_BOOTSTRAP]: CustomEvent<PeerId>;
|
||||
[EPeersByDiscoveryEvents.PEER_DISCOVERY_PEER_EXCHANGE]: CustomEvent<PeerId>;
|
||||
[EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP]: CustomEvent<PeerId>;
|
||||
[EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE]: CustomEvent<PeerId>;
|
||||
}
|
||||
|
||||
export interface PeersByDiscoveryResult {
|
||||
DISCOVERED: {
|
||||
[Tags.BOOTSTRAP]: Peer[];
|
||||
[Tags.PEER_EXCHANGE]: Peer[];
|
||||
[Tags.LOCAL]: Peer[];
|
||||
};
|
||||
CONNECTED: {
|
||||
[Tags.BOOTSTRAP]: Peer[];
|
||||
[Tags.PEER_EXCHANGE]: Peer[];
|
||||
[Tags.LOCAL]: Peer[];
|
||||
};
|
||||
}
|
||||
|
||||
export enum EConnectionStateEvents {
|
||||
CONNECTION_STATUS = "waku:connection"
|
||||
}
|
||||
|
||||
export interface IConnectionStateEvents {
|
||||
// true when online, false when offline
|
||||
[EConnectionStateEvents.CONNECTION_STATUS]: CustomEvent<boolean>;
|
||||
}
|
||||
|
||||
export interface IConnectionManager
|
||||
extends TypedEventEmitter<IPeersByDiscoveryEvents & IConnectionStateEvents> {
|
||||
pubsubTopics: PubsubTopic[];
|
||||
getConnectedPeers(codec?: string): Promise<Peer[]>;
|
||||
dropConnection(peerId: PeerId): Promise<void>;
|
||||
getPeersByDiscovery(): Promise<PeersByDiscoveryResult>;
|
||||
/**
|
||||
* Stops network monitoring, discovery dialing, keep-alive management, and connection limiting.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* connectionManager.stop();
|
||||
* ```
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Connects to a peer using specific protocol codecs.
|
||||
* This is a direct proxy to libp2p's dialProtocol method.
|
||||
*
|
||||
* @param peer - The peer to connect to (PeerId or multiaddr)
|
||||
* @param protocolCodecs - Array of protocol codec strings to establish
|
||||
* @returns Promise resolving to a Stream connection to the peer
|
||||
* @throws Error if the connection cannot be established
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const stream = await connectionManager.dial(
|
||||
* peerId,
|
||||
* ["/vac/waku/store/2.0.0-beta4"]
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
dial(
|
||||
peer: PeerId | MultiaddrInput,
|
||||
protocolCodecs: string[]
|
||||
): Promise<Stream>;
|
||||
|
||||
/**
|
||||
* Terminates the connection to a specific peer.
|
||||
*
|
||||
* @param peer - The peer to disconnect from (PeerId or multiaddr)
|
||||
* @returns Promise resolving to true if disconnection was successful, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const success = await connectionManager.hangUp(peerId);
|
||||
* if (success) {
|
||||
* console.log("Peer disconnected successfully");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
hangUp(peer: PeerId | MultiaddrInput): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Retrieves a list of currently connected peers, optionally filtered by protocol codec.
|
||||
* Results are sorted by ping time (lowest first).
|
||||
*
|
||||
* @param codec - Optional protocol codec to filter peers by
|
||||
* @returns Promise resolving to an array of connected Peer objects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Get all connected peers
|
||||
* const allPeers = await connectionManager.getConnectedPeers();
|
||||
*
|
||||
* // Get peers supporting a specific protocol
|
||||
* const storePeers = await connectionManager.getConnectedPeers(
|
||||
* "/vac/waku/store/2.0.0-beta4"
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
getConnectedPeers(codec?: string): Promise<Peer[]>;
|
||||
|
||||
/**
|
||||
* Checks if a specific pubsub topic is configured in the connection manager.
|
||||
*
|
||||
* @param pubsubTopic - The pubsub topic to check
|
||||
* @returns True if the topic is configured, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const isConfigured = connectionManager.isTopicConfigured("/waku/2/default-waku/proto");
|
||||
* if (isConfigured) {
|
||||
* console.log("Topic is configured");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
isTopicConfigured(pubsubTopic: PubsubTopic): boolean;
|
||||
|
||||
/**
|
||||
* Checks if a peer has shard info.
|
||||
*
|
||||
* @param peerId - The peer to check
|
||||
* @returns Promise resolving to true if the peer has shard info, false otherwise
|
||||
*/
|
||||
hasShardInfo(peerId: PeerId): Promise<boolean>;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import type { ContentTopic, PubsubTopic } from "./misc.js";
|
||||
export interface SingleShardInfo {
|
||||
clusterId: number;
|
||||
/**
|
||||
* TODO: make shard required
|
||||
* Specifying this field indicates to the encoder/decoder that static sharding must be used.
|
||||
*/
|
||||
shard?: number;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import type { Peer, PeerId, Stream } from "@libp2p/interface";
|
||||
import type {
|
||||
Peer,
|
||||
PeerId,
|
||||
Stream,
|
||||
TypedEventEmitter
|
||||
} from "@libp2p/interface";
|
||||
import type { MultiaddrInput } from "@multiformats/multiaddr";
|
||||
|
||||
import type { IConnectionManager } from "./connection_manager.js";
|
||||
import type { IFilter } from "./filter.js";
|
||||
import type { IHealthIndicator } from "./health_indicator.js";
|
||||
import type { Libp2p } from "./libp2p.js";
|
||||
@ -30,15 +34,21 @@ export type CreateEncoderParams = CreateDecoderParams & {
|
||||
ephemeral?: boolean;
|
||||
};
|
||||
|
||||
export interface IWakuEvents {
|
||||
"waku:connection": CustomEvent<boolean>;
|
||||
}
|
||||
|
||||
export type IWakuEventEmitter = TypedEventEmitter<IWakuEvents>;
|
||||
|
||||
export interface IWaku {
|
||||
libp2p: Libp2p;
|
||||
relay?: IRelay;
|
||||
store?: IStore;
|
||||
|
||||
filter?: IFilter;
|
||||
lightPush?: ILightPush;
|
||||
connectionManager: IConnectionManager;
|
||||
|
||||
health: IHealthIndicator;
|
||||
events: IWakuEventEmitter;
|
||||
|
||||
/**
|
||||
* Returns a unique identifier for a node on the network.
|
||||
@ -66,7 +76,7 @@ export interface IWaku {
|
||||
* @param {PeerId | MultiaddrInput} peer information to use for dialing
|
||||
* @param {Protocols[]} [protocols] array of Waku protocols to be used for dialing. If no provided - will be derived from mounted protocols.
|
||||
*
|
||||
* @returns {Promise<Stream>} `Promise` that will resolve to a `Stream` to a dialed peer
|
||||
* @returns {Promise<Stream>} `Promise` that will resolve to a `Stream` to a dialed peer and will reject if the connection fails
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@ -77,6 +87,15 @@ export interface IWaku {
|
||||
*/
|
||||
dial(peer: PeerId | MultiaddrInput, protocols?: Protocols[]): Promise<Stream>;
|
||||
|
||||
/**
|
||||
* Hang up a connection to a peer
|
||||
*
|
||||
* @param {PeerId | MultiaddrInput} peer information to use for hanging up
|
||||
*
|
||||
* @returns {Promise<boolean>} `Promise` that will resolve to `true` if the connection is hung up, `false` otherwise
|
||||
*/
|
||||
hangUp(peer: PeerId | MultiaddrInput): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Starts all services and components related to functionality of Waku node.
|
||||
*
|
||||
|
||||
@ -32,11 +32,18 @@ export async function createRelayNode(
|
||||
libp2p
|
||||
});
|
||||
|
||||
return new WakuNode(
|
||||
const node = new WakuNode(
|
||||
pubsubTopics,
|
||||
options as CreateNodeOptions,
|
||||
libp2p,
|
||||
{},
|
||||
relay
|
||||
) as RelayNode;
|
||||
|
||||
// only if `false` is passed explicitly
|
||||
if (options?.autoStart !== false) {
|
||||
await node.start();
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -160,8 +160,10 @@ function mockLibp2p(): Libp2p {
|
||||
|
||||
function mockConnectionManager(): ConnectionManager {
|
||||
return {
|
||||
pubsubTopics: [PUBSUB_TOPIC]
|
||||
} as ConnectionManager;
|
||||
isTopicConfigured: sinon.stub().callsFake((topic: string) => {
|
||||
return topic === PUBSUB_TOPIC;
|
||||
})
|
||||
} as unknown as ConnectionManager;
|
||||
}
|
||||
|
||||
function mockPeerManager(): PeerManager {
|
||||
|
||||
@ -39,7 +39,6 @@ export class Filter implements IFilter {
|
||||
|
||||
this.protocol = new FilterCore(
|
||||
this.onIncomingMessage.bind(this),
|
||||
params.connectionManager.pubsubTopics,
|
||||
params.libp2p
|
||||
);
|
||||
}
|
||||
@ -174,7 +173,7 @@ export class Filter implements IFilter {
|
||||
|
||||
private throwIfTopicNotSupported(pubsubTopic: string): void {
|
||||
const supportedPubsubTopic =
|
||||
this.connectionManager.pubsubTopics.includes(pubsubTopic);
|
||||
this.connectionManager.isTopicConfigured(pubsubTopic);
|
||||
|
||||
if (!supportedPubsubTopic) {
|
||||
throw Error(
|
||||
|
||||
@ -169,8 +169,9 @@ type MockLightPushOptions = {
|
||||
function mockLightPush(options: MockLightPushOptions): LightPush {
|
||||
const lightPush = new LightPush({
|
||||
connectionManager: {
|
||||
pubsubTopics: options.pubsubTopics || [PUBSUB_TOPIC]
|
||||
} as ConnectionManager,
|
||||
isTopicConfigured: (topic: string) =>
|
||||
(options.pubsubTopics || [PUBSUB_TOPIC]).includes(topic)
|
||||
} as unknown as ConnectionManager,
|
||||
peerManager: {
|
||||
getPeers: () =>
|
||||
options.libp2p
|
||||
|
||||
@ -40,6 +40,7 @@ export class LightPush implements ILightPush {
|
||||
private readonly config: LightPushProtocolOptions;
|
||||
private readonly retryManager: RetryManager;
|
||||
private readonly peerManager: PeerManager;
|
||||
private readonly connectionManager: ConnectionManager;
|
||||
private readonly protocol: LightPushCore;
|
||||
|
||||
public constructor(params: LightPushConstructorParams) {
|
||||
@ -49,10 +50,8 @@ export class LightPush implements ILightPush {
|
||||
} as LightPushProtocolOptions;
|
||||
|
||||
this.peerManager = params.peerManager;
|
||||
this.protocol = new LightPushCore(
|
||||
params.connectionManager.pubsubTopics,
|
||||
params.libp2p
|
||||
);
|
||||
this.connectionManager = params.connectionManager;
|
||||
this.protocol = new LightPushCore(params.libp2p);
|
||||
this.retryManager = new RetryManager({
|
||||
peerManager: params.peerManager,
|
||||
retryIntervalMs: this.config.retryIntervalMs
|
||||
@ -85,7 +84,7 @@ export class LightPush implements ILightPush {
|
||||
|
||||
log.info("send: attempting to send a message to pubsubTopic:", pubsubTopic);
|
||||
|
||||
if (!this.protocol.pubsubTopics.includes(pubsubTopic)) {
|
||||
if (!this.connectionManager.isTopicConfigured(pubsubTopic)) {
|
||||
return {
|
||||
successes: [],
|
||||
failures: [
|
||||
|
||||
@ -61,7 +61,7 @@ describe("PeerManager", () => {
|
||||
pubsubTopics: [TEST_PUBSUB_TOPIC],
|
||||
getConnectedPeers: async () => peers,
|
||||
getPeers: async () => peers,
|
||||
isPeerOnPubsubTopic: async (_id: PeerId, _topic: string) => true
|
||||
isPeerOnTopic: async (_id: PeerId, _topic: string) => true
|
||||
} as unknown as IConnectionManager;
|
||||
peerManager = new PeerManager({
|
||||
libp2p,
|
||||
|
||||
@ -114,7 +114,7 @@ export class PeerManager {
|
||||
|
||||
for (const peer of connectedPeers) {
|
||||
const hasProtocol = this.hasPeerProtocol(peer, params.protocol);
|
||||
const hasSamePubsub = await this.connectionManager.isPeerOnPubsubTopic(
|
||||
const hasSamePubsub = await this.connectionManager.isPeerOnTopic(
|
||||
peer.id,
|
||||
params.pubsubTopic
|
||||
);
|
||||
@ -187,7 +187,14 @@ export class PeerManager {
|
||||
id: PeerId,
|
||||
pubsubTopic: string
|
||||
): Promise<boolean> {
|
||||
return this.connectionManager.isPeerOnPubsubTopic(id, pubsubTopic);
|
||||
const hasShardInfo = await this.connectionManager.hasShardInfo(id);
|
||||
|
||||
// allow to use peers that we don't know information about yet
|
||||
if (!hasShardInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.connectionManager.isPeerOnTopic(id, pubsubTopic);
|
||||
}
|
||||
|
||||
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
|
||||
|
||||
@ -229,12 +229,12 @@ export class Store implements IStore {
|
||||
}
|
||||
|
||||
const pubsubTopicForQuery = uniquePubsubTopicsInQuery[0];
|
||||
const isPubsubSupported =
|
||||
this.connectionManager.pubsubTopics.includes(pubsubTopicForQuery);
|
||||
const isTopicSupported =
|
||||
this.connectionManager.isTopicConfigured(pubsubTopicForQuery);
|
||||
|
||||
if (!isPubsubSupported) {
|
||||
if (!isTopicSupported) {
|
||||
throw new Error(
|
||||
`Pubsub topic ${pubsubTopicForQuery} has not been configured on this instance. Configured topics are: ${this.connectionManager.pubsubTopics}`
|
||||
`Pubsub topic ${pubsubTopicForQuery} has not been configured on this instance.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,38 +1,12 @@
|
||||
import { peerIdFromString } from "@libp2p/peer-id";
|
||||
import { DEFAULT_NUM_SHARDS, DefaultNetworkConfig } from "@waku/interfaces";
|
||||
import { contentTopicToShardIndex } from "@waku/utils";
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
decoderParamsToShardInfo,
|
||||
isShardCompatible,
|
||||
mapToPeerIdOrMultiaddr
|
||||
} from "./utils.js";
|
||||
import { decoderParamsToShardInfo, isShardCompatible } from "./utils.js";
|
||||
|
||||
const TestContentTopic = "/test/1/waku-sdk/utf8";
|
||||
|
||||
describe("IWaku utils", () => {
|
||||
describe("mapToPeerIdOrMultiaddr", () => {
|
||||
it("should return PeerId when PeerId is provided", async () => {
|
||||
const peerId = peerIdFromString(
|
||||
"12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE"
|
||||
);
|
||||
|
||||
const result = mapToPeerIdOrMultiaddr(peerId);
|
||||
|
||||
expect(result).to.equal(peerId);
|
||||
});
|
||||
|
||||
it("should return Multiaddr when Multiaddr input is provided", () => {
|
||||
const multiAddr =
|
||||
"/ip4/127.0.0.1/tcp/8000/p2p/12D3KooWHFJGwBXD7ukXqKaQZYmV1U3xxN1XCNrgriSEyvkxf6nE";
|
||||
|
||||
const result = mapToPeerIdOrMultiaddr(multiAddr);
|
||||
|
||||
expect(result.toString()).to.equal(multiAddr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decoderParamsToShardInfo", () => {
|
||||
it("should use provided shard info when available", () => {
|
||||
const params = {
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import { isPeerId } from "@libp2p/interface";
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { multiaddr, Multiaddr, MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import type {
|
||||
CreateDecoderParams,
|
||||
NetworkConfig,
|
||||
@ -9,12 +6,6 @@ import type {
|
||||
import { DEFAULT_NUM_SHARDS } from "@waku/interfaces";
|
||||
import { contentTopicToShardIndex } from "@waku/utils";
|
||||
|
||||
export const mapToPeerIdOrMultiaddr = (
|
||||
peerId: PeerId | MultiaddrInput
|
||||
): PeerId | Multiaddr => {
|
||||
return isPeerId(peerId) ? peerId : multiaddr(peerId);
|
||||
};
|
||||
|
||||
export const decoderParamsToShardInfo = (
|
||||
params: CreateDecoderParams,
|
||||
networkConfig: NetworkConfig
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import type { Peer, PeerId, Stream } from "@libp2p/interface";
|
||||
import { MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import {
|
||||
type Peer,
|
||||
type PeerId,
|
||||
type Stream,
|
||||
TypedEventEmitter
|
||||
} from "@libp2p/interface";
|
||||
import type { MultiaddrInput } from "@multiformats/multiaddr";
|
||||
import { ConnectionManager, createDecoder, createEncoder } from "@waku/core";
|
||||
import type {
|
||||
CreateDecoderParams,
|
||||
@ -13,6 +18,7 @@ import type {
|
||||
IRelay,
|
||||
IStore,
|
||||
IWaku,
|
||||
IWakuEventEmitter,
|
||||
Libp2p,
|
||||
NetworkConfig,
|
||||
PubsubTopic
|
||||
@ -26,11 +32,7 @@ import { LightPush } from "../light_push/index.js";
|
||||
import { PeerManager } from "../peer_manager/index.js";
|
||||
import { Store } from "../store/index.js";
|
||||
|
||||
import {
|
||||
decoderParamsToShardInfo,
|
||||
isShardCompatible,
|
||||
mapToPeerIdOrMultiaddr
|
||||
} from "./utils.js";
|
||||
import { decoderParamsToShardInfo, isShardCompatible } from "./utils.js";
|
||||
import { waitForRemotePeer } from "./wait_for_remote_peer.js";
|
||||
|
||||
const log = new Logger("waku");
|
||||
@ -47,19 +49,21 @@ export class WakuNode implements IWaku {
|
||||
public store?: IStore;
|
||||
public filter?: IFilter;
|
||||
public lightPush?: ILightPush;
|
||||
public connectionManager: ConnectionManager;
|
||||
public health: HealthIndicator;
|
||||
|
||||
public readonly networkConfig: NetworkConfig;
|
||||
public readonly health: HealthIndicator;
|
||||
public readonly events: IWakuEventEmitter = new TypedEventEmitter();
|
||||
|
||||
private readonly networkConfig: NetworkConfig;
|
||||
|
||||
// needed to create a lock for async operations
|
||||
private _nodeStateLock = false;
|
||||
private _nodeStarted = false;
|
||||
|
||||
private readonly connectionManager: ConnectionManager;
|
||||
private readonly peerManager: PeerManager;
|
||||
|
||||
public constructor(
|
||||
public readonly pubsubTopics: PubsubTopic[],
|
||||
pubsubTopics: PubsubTopic[],
|
||||
options: CreateNodeOptions,
|
||||
libp2p: Libp2p,
|
||||
protocolsEnabled: ProtocolsEnabled,
|
||||
@ -81,7 +85,9 @@ export class WakuNode implements IWaku {
|
||||
this.connectionManager = new ConnectionManager({
|
||||
libp2p,
|
||||
relay: this.relay,
|
||||
pubsubTopics: this.pubsubTopics,
|
||||
events: this.events,
|
||||
pubsubTopics: pubsubTopics,
|
||||
networkConfig: this.networkConfig,
|
||||
config: options?.connectionManager
|
||||
});
|
||||
|
||||
@ -191,9 +197,15 @@ export class WakuNode implements IWaku {
|
||||
}
|
||||
}
|
||||
|
||||
const peerId = mapToPeerIdOrMultiaddr(peer);
|
||||
log.info(`Dialing to ${peerId.toString()} with protocols ${_protocols}`);
|
||||
return await this.connectionManager.rawDialPeerWithProtocols(peer, codecs);
|
||||
log.info(`Dialing to ${peer?.toString()} with protocols ${_protocols}`);
|
||||
|
||||
return await this.connectionManager.dial(peer, codecs);
|
||||
}
|
||||
|
||||
public async hangUp(peer: PeerId | MultiaddrInput): Promise<boolean> {
|
||||
log.info(`Hanging up peer:${peer?.toString()}.`);
|
||||
|
||||
return this.connectionManager.hangUp(peer);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
@ -202,6 +214,7 @@ export class WakuNode implements IWaku {
|
||||
this._nodeStateLock = true;
|
||||
|
||||
await this.libp2p.start();
|
||||
this.connectionManager.start();
|
||||
this.peerManager.start();
|
||||
this.health.start();
|
||||
this.lightPush?.start();
|
||||
|
||||
@ -16,9 +16,9 @@ import { ServiceNode } from "./service_node.js";
|
||||
export const log = new Logger("test:runNodes");
|
||||
|
||||
export const DEFAULT_DISCOVERIES_ENABLED = {
|
||||
dns: true,
|
||||
dns: false,
|
||||
peerExchange: true,
|
||||
localPeerCache: true
|
||||
localPeerCache: false
|
||||
};
|
||||
|
||||
type RunNodesOptions = {
|
||||
|
||||
@ -24,7 +24,8 @@ export async function runMultipleNodes(
|
||||
customArgs?: Args,
|
||||
strictChecking: boolean = false,
|
||||
numServiceNodes = 2,
|
||||
withoutFilter = false
|
||||
withoutFilter = false,
|
||||
jsWakuParams: CreateNodeOptions = {}
|
||||
): Promise<[ServiceNodesFleet, LightNode]> {
|
||||
// create numServiceNodes nodes
|
||||
const serviceNodes = await ServiceNodesFleet.createAndRun(
|
||||
@ -43,7 +44,8 @@ export async function runMultipleNodes(
|
||||
},
|
||||
networkConfig,
|
||||
lightPush: { numPeersToUse: numServiceNodes },
|
||||
discovery: DEFAULT_DISCOVERIES_ENABLED
|
||||
discovery: DEFAULT_DISCOVERIES_ENABLED,
|
||||
...jsWakuParams
|
||||
};
|
||||
|
||||
const waku = await createLightNode(wakuOptions);
|
||||
|
||||
@ -0,0 +1,168 @@
|
||||
import { LightNode, Tags } from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
runMultipleNodes,
|
||||
ServiceNodesFleet,
|
||||
teardownNodesWithRedundancy
|
||||
} from "../../src/index.js";
|
||||
|
||||
import { TestShardInfo } from "./utils.js";
|
||||
|
||||
describe("Connection Limiter", function () {
|
||||
let waku: LightNode;
|
||||
let serviceNodes: ServiceNodesFleet;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
[serviceNodes, waku] = await runMultipleNodes(
|
||||
this.ctx,
|
||||
TestShardInfo,
|
||||
{ lightpush: true, filter: true, peerExchange: true },
|
||||
false,
|
||||
2,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await teardownNodesWithRedundancy(serviceNodes, [waku]);
|
||||
});
|
||||
|
||||
it("should dial all known peers when reached zero connections", async function () {
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const addr = await node.getMultiaddrWithId();
|
||||
await waku.hangUp(addr);
|
||||
}
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(0, "Connection should be dropped");
|
||||
|
||||
const connectPromise = new Promise((resolve) => {
|
||||
waku.libp2p.addEventListener("peer:connect", (event) => {
|
||||
resolve(event.detail);
|
||||
});
|
||||
});
|
||||
|
||||
await connectPromise;
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
// checking for greater than 0 because in CI environment, nwaku not always accepts dial
|
||||
expect(peers.length).to.be.greaterThan(
|
||||
0,
|
||||
"Connection should be established"
|
||||
);
|
||||
});
|
||||
|
||||
it("should discard bootstrap peers when has more than 1 (default limit)", async function () {
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const peerId = await node.getPeerId();
|
||||
await waku.libp2p.peerStore.patch(peerId, {
|
||||
tags: new Map([[Tags.BOOTSTRAP, { value: 1 }]])
|
||||
});
|
||||
}
|
||||
|
||||
const disconnectPromise = new Promise((resolve) => {
|
||||
waku.libp2p.addEventListener("peer:disconnect", (event) => {
|
||||
resolve(event.detail);
|
||||
});
|
||||
});
|
||||
|
||||
// simulate connection to a peer
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent("peer:connect", {
|
||||
detail: await serviceNodes.nodes[0].getPeerId()
|
||||
})
|
||||
);
|
||||
|
||||
await disconnectPromise;
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length - 1,
|
||||
"Should have only one peer dropped"
|
||||
);
|
||||
|
||||
const bootstrapPeers = peers.filter((peer) =>
|
||||
peer.tags.has(Tags.BOOTSTRAP)
|
||||
);
|
||||
expect(bootstrapPeers.length).to.equal(
|
||||
1,
|
||||
"Should have only one bootstrap peer"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not discard bootstrap peers if under the limit", async function () {
|
||||
this.timeout(15_000); // increase due to additional initialization
|
||||
|
||||
await teardownNodesWithRedundancy(serviceNodes, [waku]);
|
||||
|
||||
[serviceNodes, waku] = await runMultipleNodes(
|
||||
this.ctx,
|
||||
TestShardInfo,
|
||||
{ lightpush: true, filter: true, peerExchange: true },
|
||||
false,
|
||||
2,
|
||||
true,
|
||||
{ connectionManager: { maxBootstrapPeers: 2 } }
|
||||
);
|
||||
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const peerId = await node.getPeerId();
|
||||
await waku.libp2p.peerStore.patch(peerId, {
|
||||
tags: new Map([[Tags.BOOTSTRAP, { value: 1 }]])
|
||||
});
|
||||
}
|
||||
|
||||
const disconnectPromise = new Promise((resolve) => {
|
||||
waku.libp2p.addEventListener("peer:disconnect", () => {
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), 1000);
|
||||
});
|
||||
|
||||
// simulate connection to a peer
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent("peer:connect", {
|
||||
detail: await serviceNodes.nodes[0].getPeerId()
|
||||
})
|
||||
);
|
||||
|
||||
const hasDisconnected = await disconnectPromise;
|
||||
expect(hasDisconnected).to.equal(false, "Should not disconnect");
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Should have all peers"
|
||||
);
|
||||
|
||||
const bootstrapPeers = peers.filter((peer) =>
|
||||
peer.tags.has(Tags.BOOTSTRAP)
|
||||
);
|
||||
expect(bootstrapPeers.length).to.equal(
|
||||
2,
|
||||
"Should have only two bootstrap peers"
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,195 +0,0 @@
|
||||
import { Multiaddr } from "@multiformats/multiaddr";
|
||||
import { EConnectionStateEvents, LightNode, Protocols } from "@waku/interfaces";
|
||||
import { createRelayNode } from "@waku/relay";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
DefaultTestShardInfo,
|
||||
delay,
|
||||
NOISE_KEY_1
|
||||
} from "../../src/index.js";
|
||||
import {
|
||||
makeLogFileName,
|
||||
ServiceNode,
|
||||
tearDownNodes
|
||||
} from "../../src/index.js";
|
||||
|
||||
const TEST_TIMEOUT = 30_000;
|
||||
|
||||
describe("Connection state", function () {
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
let waku: LightNode;
|
||||
|
||||
let nwaku1: ServiceNode;
|
||||
let nwaku2: ServiceNode;
|
||||
let nwaku1PeerId: Multiaddr;
|
||||
let nwaku2PeerId: Multiaddr;
|
||||
|
||||
let navigatorMock: any;
|
||||
let originalNavigator: any;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode({ networkConfig: DefaultTestShardInfo });
|
||||
nwaku1 = new ServiceNode(makeLogFileName(this.ctx) + "1");
|
||||
nwaku2 = new ServiceNode(makeLogFileName(this.ctx) + "2");
|
||||
await nwaku1.start({ filter: true });
|
||||
await nwaku2.start({ filter: true });
|
||||
nwaku1PeerId = await nwaku1.getMultiaddrWithId();
|
||||
nwaku2PeerId = await nwaku2.getMultiaddrWithId();
|
||||
|
||||
navigatorMock = { onLine: true };
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: navigatorMock,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([nwaku1, nwaku2], waku);
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: originalNavigator,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit `waku:online` event only when first peer is connected", async function () {
|
||||
let eventCount = 0;
|
||||
const connectionStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(await connectionStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("should emit `waku:offline` event only when all peers disconnect", async function () {
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
|
||||
let eventCount = 0;
|
||||
const connectionStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await nwaku1.stop();
|
||||
await delay(400);
|
||||
expect(eventCount).to.be.eq(0);
|
||||
|
||||
await nwaku2.stop();
|
||||
expect(await connectionStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("`waku:online` between 2 js-waku relay nodes", async function () {
|
||||
const waku1 = await createRelayNode({
|
||||
staticNoiseKey: NOISE_KEY_1,
|
||||
networkConfig: DefaultTestShardInfo
|
||||
});
|
||||
const waku2 = await createRelayNode({
|
||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } },
|
||||
networkConfig: DefaultTestShardInfo
|
||||
});
|
||||
|
||||
let eventCount1 = 0;
|
||||
const connectionStatus1 = new Promise<boolean>((resolve) => {
|
||||
waku1.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount1++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
let eventCount2 = 0;
|
||||
const connectionStatus2 = new Promise<boolean>((resolve) => {
|
||||
waku2.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount2++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await waku1.libp2p.peerStore.merge(waku2.libp2p.peerId, {
|
||||
multiaddrs: waku2.libp2p.getMultiaddrs()
|
||||
});
|
||||
await Promise.all([waku1.dial(waku2.libp2p.peerId)]);
|
||||
await delay(400);
|
||||
|
||||
expect(await connectionStatus1).to.eq(true);
|
||||
expect(await connectionStatus2).to.eq(true);
|
||||
expect(eventCount1).to.be.eq(1);
|
||||
expect(eventCount2).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("isConnected should return true after first peer connects", async function () {
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
});
|
||||
|
||||
it("isConnected should return false after all peers disconnect", async function () {
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
await delay(250);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
|
||||
await waku.libp2p.hangUp(nwaku1PeerId);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
await waku.libp2p.hangUp(nwaku2PeerId);
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("isConnected return false after peer stops", async function () {
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
|
||||
await nwaku1.stop();
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("isConnected between 2 js-waku relay nodes", async function () {
|
||||
const waku1 = await createRelayNode({
|
||||
staticNoiseKey: NOISE_KEY_1
|
||||
});
|
||||
const waku2 = await createRelayNode({
|
||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }
|
||||
});
|
||||
await waku1.libp2p.peerStore.merge(waku2.libp2p.peerId, {
|
||||
multiaddrs: waku2.libp2p.getMultiaddrs()
|
||||
});
|
||||
await Promise.all([waku1.dial(waku2.libp2p.peerId)]);
|
||||
await delay(400);
|
||||
expect(waku1.isConnected()).to.be.true;
|
||||
expect(waku2.isConnected()).to.be.true;
|
||||
});
|
||||
});
|
||||
152
packages/tests/tests/connection-mananger/dialing.spec.ts
Normal file
152
packages/tests/tests/connection-mananger/dialing.spec.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { LightNode } from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
import type { Context } from "mocha";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
runMultipleNodes,
|
||||
ServiceNodesFleet,
|
||||
teardownNodesWithRedundancy
|
||||
} from "../../src/index.js";
|
||||
|
||||
import { TestShardInfo } from "./utils.js";
|
||||
|
||||
describe("Dialing", function () {
|
||||
const ctx: Context = this.ctx;
|
||||
let waku: LightNode;
|
||||
let serviceNodes: ServiceNodesFleet;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
[serviceNodes, waku] = await runMultipleNodes(
|
||||
this.ctx,
|
||||
TestShardInfo,
|
||||
{ lightpush: true, filter: true, peerExchange: true },
|
||||
false,
|
||||
2,
|
||||
true
|
||||
);
|
||||
|
||||
await teardownNodesWithRedundancy(serviceNodes, []);
|
||||
|
||||
serviceNodes = await ServiceNodesFleet.createAndRun(
|
||||
ctx,
|
||||
2,
|
||||
false,
|
||||
TestShardInfo,
|
||||
{
|
||||
lightpush: true,
|
||||
filter: true,
|
||||
peerExchange: true
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await teardownNodesWithRedundancy(serviceNodes, [waku]);
|
||||
});
|
||||
|
||||
it("should dial all peers on dial", async function () {
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const addr = await node.getMultiaddrWithId();
|
||||
await waku.dial(addr);
|
||||
}
|
||||
|
||||
const peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
});
|
||||
|
||||
it("should drop connection to all peers on hangUp", async function () {
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const addr = await node.getMultiaddrWithId();
|
||||
await waku.dial(addr);
|
||||
}
|
||||
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const peerId = await node.getPeerId();
|
||||
await waku.hangUp(peerId);
|
||||
}
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(0, "Connection should be dropped");
|
||||
});
|
||||
|
||||
it("should dial one peer on dial", async function () {
|
||||
const addr = await serviceNodes.nodes[0].getMultiaddrWithId();
|
||||
await waku.dial(addr);
|
||||
|
||||
const peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(1, "Connection should be established");
|
||||
});
|
||||
|
||||
it("should drop connection to one peer on hangUp", async function () {
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const addr = await node.getMultiaddrWithId();
|
||||
await waku.dial(addr);
|
||||
}
|
||||
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
const peerId = await serviceNodes.nodes[0].getPeerId();
|
||||
await waku.hangUp(peerId);
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length - 1,
|
||||
"Connection should be dropped"
|
||||
);
|
||||
});
|
||||
|
||||
it("should drop connection via multiaddr with hangUp", async function () {
|
||||
for (const node of serviceNodes.nodes) {
|
||||
const addr = await node.getMultiaddrWithId();
|
||||
await waku.dial(addr);
|
||||
}
|
||||
|
||||
let peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length,
|
||||
"Connection should be established"
|
||||
);
|
||||
|
||||
const addr = await serviceNodes.nodes[0].getMultiaddrWithId();
|
||||
await waku.hangUp(addr);
|
||||
|
||||
peers = await waku.getConnectedPeers();
|
||||
expect(peers.length).to.equal(
|
||||
serviceNodes.nodes.length - 1,
|
||||
"Connection should be dropped"
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to dial TLS multiaddrs", async function () {
|
||||
let tlsWorks = true;
|
||||
|
||||
const multiaddr = `/ip4/127.0.0.1/tcp/30303/tls/ws`;
|
||||
try {
|
||||
// dummy multiaddr, doesn't have to be valid
|
||||
await waku.dial(multiaddr);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).to.eq(`Could not connect to ${multiaddr}`);
|
||||
tlsWorks = !error.message.includes("Unsupported protocol tls");
|
||||
}
|
||||
}
|
||||
|
||||
expect(tlsWorks).to.eq(true);
|
||||
});
|
||||
});
|
||||
@ -1,221 +0,0 @@
|
||||
import { generateKeyPair } from "@libp2p/crypto/keys";
|
||||
import type { PeerInfo } from "@libp2p/interface";
|
||||
import { peerIdFromPrivateKey } from "@libp2p/peer-id";
|
||||
import { LightNode, Tags } from "@waku/interfaces";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
import sinon, { SinonSpy, SinonStub } from "sinon";
|
||||
|
||||
import { afterEachCustom, beforeEachCustom, delay } from "../../src/index.js";
|
||||
import { tearDownNodes } from "../../src/index.js";
|
||||
|
||||
const DELAY_MS = 1_000;
|
||||
const TEST_TIMEOUT = 20_000;
|
||||
|
||||
describe("Dials", function () {
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
let dialPeerStub: SinonStub;
|
||||
let getConnectionsStub: SinonStub;
|
||||
let getTagNamesForPeerStub: SinonStub;
|
||||
let isPeerOnSameShard: SinonStub;
|
||||
let waku: LightNode;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode();
|
||||
isPeerOnSameShard = sinon.stub(
|
||||
waku.connectionManager as any,
|
||||
"isPeerOnSameShard"
|
||||
);
|
||||
isPeerOnSameShard.resolves(true);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([], waku);
|
||||
isPeerOnSameShard.restore();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("attemptDial method", function () {
|
||||
let attemptDialSpy: SinonSpy;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
attemptDialSpy = sinon.spy(waku.connectionManager as any, "attemptDial");
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
attemptDialSpy.restore();
|
||||
});
|
||||
|
||||
it("should be called at least once on all `peer:discovery` events", async function () {
|
||||
const totalPeerIds = 5;
|
||||
for (let i = 1; i <= totalPeerIds; i++) {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerId = peerIdFromPrivateKey(privateKey);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: peerId,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(attemptDialSpy.callCount).to.be.greaterThanOrEqual(
|
||||
totalPeerIds,
|
||||
"attemptDial should be called at least once for each peer:discovery event"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dialPeer method", function () {
|
||||
let peerStoreHasStub: SinonStub;
|
||||
let dialAttemptsForPeerHasStub: SinonStub;
|
||||
beforeEachCustom(this, async () => {
|
||||
getConnectionsStub = sinon.stub(
|
||||
(waku.connectionManager as any).libp2p,
|
||||
"getConnections"
|
||||
);
|
||||
getTagNamesForPeerStub = sinon.stub(
|
||||
waku.connectionManager as any,
|
||||
"getTagNamesForPeer"
|
||||
);
|
||||
dialPeerStub = sinon.stub(waku.connectionManager as any, "dialPeer");
|
||||
peerStoreHasStub = sinon.stub(waku.libp2p.peerStore, "has");
|
||||
dialAttemptsForPeerHasStub = sinon.stub(
|
||||
(waku.connectionManager as any).dialAttemptsForPeer,
|
||||
"has"
|
||||
);
|
||||
|
||||
// simulate that the peer is not connected
|
||||
getConnectionsStub.returns([]);
|
||||
|
||||
// simulate that the peer is a bootstrap peer
|
||||
getTagNamesForPeerStub.resolves([Tags.BOOTSTRAP]);
|
||||
|
||||
// simulate that the peer is not in the peerStore
|
||||
peerStoreHasStub.returns(false);
|
||||
|
||||
// simulate that the peer has not been dialed before
|
||||
dialAttemptsForPeerHasStub.returns(false);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
dialPeerStub.restore();
|
||||
getTagNamesForPeerStub.restore();
|
||||
getConnectionsStub.restore();
|
||||
peerStoreHasStub.restore();
|
||||
dialAttemptsForPeerHasStub.restore();
|
||||
});
|
||||
|
||||
describe("For bootstrap peers", function () {
|
||||
it("should be called for bootstrap peers", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const bootstrapPeer = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
// emit a peer:discovery event
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: { id: bootstrapPeer, multiaddrs: [] }
|
||||
})
|
||||
);
|
||||
|
||||
// wait for the async function calls within attemptDial to finish
|
||||
await delay(DELAY_MS);
|
||||
|
||||
// check that dialPeer was called once
|
||||
expect(dialPeerStub.callCount).to.equal(
|
||||
1,
|
||||
"dialPeer should be called for bootstrap peers"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not be called more than DEFAULT_MAX_BOOTSTRAP_PEERS_ALLOWED times for bootstrap peers", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const bootstrapPeer = peerIdFromPrivateKey(privateKey);
|
||||
// emit first peer:discovery event
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: bootstrapPeer,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
await delay(500);
|
||||
|
||||
// simulate that the peer is connected
|
||||
getConnectionsStub.returns([{ tags: [{ name: Tags.BOOTSTRAP }] }]);
|
||||
|
||||
// emit multiple peer:discovery events
|
||||
const totalBootstrapPeers = 5;
|
||||
for (let i = 1; i <= totalBootstrapPeers; i++) {
|
||||
await delay(500);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: bootstrapPeer,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// check that dialPeer was called only once
|
||||
expect(dialPeerStub.callCount).to.equal(
|
||||
1,
|
||||
"dialPeer should not be called more than once for bootstrap peers"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("For peer-exchange peers", function () {
|
||||
it("should be called for peers with PEER_EXCHANGE tags", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const pxPeer = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
// emit a peer:discovery event
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: pxPeer,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// wait for the async function calls within attemptDial to finish
|
||||
await delay(DELAY_MS);
|
||||
|
||||
// check that dialPeer was called once
|
||||
expect(dialPeerStub.callCount).to.equal(
|
||||
1,
|
||||
"dialPeer should be called for peers with PEER_EXCHANGE tags"
|
||||
);
|
||||
});
|
||||
|
||||
it("should be called for every peer with PEER_EXCHANGE tags", async function () {
|
||||
// emit multiple peer:discovery events
|
||||
const totalPxPeers = 5;
|
||||
for (let i = 0; i < totalPxPeers; i++) {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const pxPeer = peerIdFromPrivateKey(privateKey);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: pxPeer,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
await delay(500);
|
||||
}
|
||||
|
||||
// check that dialPeer was called for each peer with PEER_EXCHANGE tags
|
||||
expect(dialPeerStub.callCount).to.equal(totalPxPeers);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,110 @@
|
||||
import { LightNode } from "@waku/interfaces";
|
||||
import { expect } from "chai";
|
||||
import type { Context } from "mocha";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
delay,
|
||||
runMultipleNodes,
|
||||
ServiceNodesFleet,
|
||||
teardownNodesWithRedundancy
|
||||
} from "../../src/index.js";
|
||||
|
||||
import { TestShardInfo } from "./utils.js";
|
||||
|
||||
// TODO: investigate and re-enable in https://github.com/waku-org/js-waku/issues/2453
|
||||
describe.skip("DiscoveryDialer", function () {
|
||||
const ctx: Context = this.ctx;
|
||||
let waku: LightNode;
|
||||
let serviceNodes: ServiceNodesFleet;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
[serviceNodes, waku] = await runMultipleNodes(
|
||||
this.ctx,
|
||||
TestShardInfo,
|
||||
{ lightpush: true, filter: true, peerExchange: true },
|
||||
false,
|
||||
2,
|
||||
true
|
||||
);
|
||||
|
||||
await teardownNodesWithRedundancy(serviceNodes, []);
|
||||
|
||||
serviceNodes = await ServiceNodesFleet.createAndRun(
|
||||
ctx,
|
||||
2,
|
||||
false,
|
||||
TestShardInfo,
|
||||
{
|
||||
lightpush: true,
|
||||
filter: true,
|
||||
peerExchange: true
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||
});
|
||||
|
||||
it("should dial second nwaku node that was discovered", async function () {
|
||||
const maddrs = await Promise.all(
|
||||
serviceNodes.nodes.map((n) => n.getMultiaddrWithId())
|
||||
);
|
||||
|
||||
expect(waku.isConnected(), "waku is connected").to.be.false;
|
||||
expect(
|
||||
await waku.getConnectedPeers(),
|
||||
"waku has no connected peers"
|
||||
).to.have.length(0);
|
||||
|
||||
const connectPromise = new Promise((resolve, reject) => {
|
||||
waku.libp2p.addEventListener("peer:connect", () => {
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timeout waiting for peer:connect event"));
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
try {
|
||||
await waku.dial(maddrs[0]);
|
||||
} catch (error) {
|
||||
throw Error(error as string);
|
||||
}
|
||||
|
||||
await connectPromise;
|
||||
|
||||
expect(waku.isConnected(), "waku is connected").to.be.true;
|
||||
|
||||
expect(
|
||||
await waku.getConnectedPeers(),
|
||||
"waku has one connected peer"
|
||||
).to.have.length(1);
|
||||
|
||||
const secondPeerId = await serviceNodes.nodes[1].getPeerId();
|
||||
const discoveryPromise = new Promise((resolve) => {
|
||||
waku.libp2p.addEventListener("peer:discovery", (event) => {
|
||||
if (event.detail.id.equals(secondPeerId)) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(weboko): investigate why peer-exchange discovery is not working https://github.com/waku-org/js-waku/issues/2446
|
||||
await waku.libp2p.peerStore.save(secondPeerId, {
|
||||
multiaddrs: [maddrs[1]]
|
||||
});
|
||||
|
||||
await discoveryPromise;
|
||||
await delay(500);
|
||||
|
||||
expect(
|
||||
waku.libp2p.getConnections(),
|
||||
"waku has two connections"
|
||||
).to.have.length(2);
|
||||
});
|
||||
});
|
||||
@ -1,341 +0,0 @@
|
||||
import { generateKeyPair } from "@libp2p/crypto/keys";
|
||||
import type { PeerId, PeerInfo } from "@libp2p/interface";
|
||||
import { TypedEventEmitter } from "@libp2p/interface";
|
||||
import { peerIdFromPrivateKey } from "@libp2p/peer-id";
|
||||
import {
|
||||
EConnectionStateEvents,
|
||||
EPeersByDiscoveryEvents,
|
||||
LightNode,
|
||||
Tags
|
||||
} from "@waku/interfaces";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { afterEachCustom, beforeEachCustom, delay } from "../../src/index.js";
|
||||
import { tearDownNodes } from "../../src/index.js";
|
||||
|
||||
const TEST_TIMEOUT = 20_000;
|
||||
|
||||
describe("Events", function () {
|
||||
let waku: LightNode;
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode();
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([], waku);
|
||||
});
|
||||
|
||||
describe("peer:discovery", () => {
|
||||
it("should emit `peer:discovery:bootstrap` event when a peer is discovered", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const peerDiscoveryBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_DISCOVERY_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: peerIdBootstrap,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(await peerDiscoveryBootstrap).to.eq(true);
|
||||
});
|
||||
|
||||
it("should emit `peer:discovery:peer-exchange` event when a peer is discovered", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const peerDiscoveryPeerExchange = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_DISCOVERY_PEER_EXCHANGE,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdPx.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: peerIdPx,
|
||||
multiaddrs: []
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(await peerDiscoveryPeerExchange).to.eq(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("peer:connect", () => {
|
||||
it("should emit `peer:connected:bootstrap` event when a peer is connected", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
|
||||
expect(await peerConnectedBootstrap).to.eq(true);
|
||||
});
|
||||
it("should emit `peer:connected:peer-exchange` event when a peer is connected", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const peerConnectedPeerExchange = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdPx.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx })
|
||||
);
|
||||
|
||||
expect(await peerConnectedPeerExchange).to.eq(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EConnectionStateEvents.CONNECTION_STATUS, function () {
|
||||
let navigatorMock: any;
|
||||
let originalNavigator: any;
|
||||
|
||||
before(() => {
|
||||
originalNavigator = global.navigator;
|
||||
});
|
||||
|
||||
this.beforeEach(() => {
|
||||
navigatorMock = { onLine: true };
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: navigatorMock,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
|
||||
const eventEmmitter = new TypedEventEmitter();
|
||||
globalThis.addEventListener =
|
||||
eventEmmitter.addEventListener.bind(eventEmmitter);
|
||||
globalThis.removeEventListener =
|
||||
eventEmmitter.removeEventListener.bind(eventEmmitter);
|
||||
globalThis.dispatchEvent =
|
||||
eventEmmitter.dispatchEvent.bind(eventEmmitter);
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: originalNavigator,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.addEventListener = undefined;
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.removeEventListener = undefined;
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.dispatchEvent = undefined;
|
||||
});
|
||||
|
||||
it(`should emit events and trasition isConnected state when has peers or no peers`, async function () {
|
||||
const privateKey1 = await generateKeyPair("secp256k1");
|
||||
const privateKey2 = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey1);
|
||||
const peerIdPx2 = peerIdFromPrivateKey(privateKey2);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx2, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let eventCount = 0;
|
||||
const connectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx })
|
||||
);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx2 })
|
||||
);
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectedStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
const disconnectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:disconnect", { detail: peerIdPx })
|
||||
);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:disconnect", { detail: peerIdPx2 })
|
||||
);
|
||||
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
expect(await disconnectedStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(2);
|
||||
});
|
||||
|
||||
it("should be online or offline if network state changed", async function () {
|
||||
// have to recreate js-waku for it to pick up new globalThis
|
||||
waku = await createLightNode();
|
||||
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let eventCount = 0;
|
||||
const connectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx })
|
||||
);
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectedStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
const disconnectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
navigatorMock.onLine = false;
|
||||
globalThis.dispatchEvent(new CustomEvent("offline"));
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
expect(await disconnectedStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(2);
|
||||
|
||||
const connectionRecoveredStatus = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EConnectionStateEvents.CONNECTION_STATUS,
|
||||
({ detail: status }) => {
|
||||
resolve(status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
navigatorMock.onLine = true;
|
||||
globalThis.dispatchEvent(new CustomEvent("online"));
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectionRecoveredStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,332 +0,0 @@
|
||||
import { generateKeyPair } from "@libp2p/crypto/keys";
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { peerIdFromPrivateKey } from "@libp2p/peer-id";
|
||||
import {
|
||||
EPeersByDiscoveryEvents,
|
||||
LightNode,
|
||||
PeersByDiscoveryResult,
|
||||
Tags
|
||||
} from "@waku/interfaces";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { afterEachCustom, beforeEachCustom, delay } from "../../src/index.js";
|
||||
import { tearDownNodes } from "../../src/index.js";
|
||||
|
||||
const TEST_TIMEOUT = 20_000;
|
||||
|
||||
describe("Public methods", function () {
|
||||
let waku: LightNode;
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode();
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([], waku);
|
||||
});
|
||||
it("addEventListener with correct event", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
expect(await peerConnectedBootstrap).to.eq(true);
|
||||
});
|
||||
|
||||
it("addEventListener with wrong event", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
// setting PEER_CONNECT_PEER_EXCHANGE while the tag is BOOTSTRAP
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
const timeoutPromise = new Promise<boolean>((resolve) =>
|
||||
setTimeout(() => resolve(false), TEST_TIMEOUT - 100)
|
||||
);
|
||||
|
||||
const result = await Promise.race([peerConnectedBootstrap, timeoutPromise]);
|
||||
|
||||
// If the timeout promise resolves first, the result will be false, and we expect it to be false (test passes)
|
||||
// If the peerConnectedBootstrap resolves first, we expect its result to be true (which will now make the test fail if it's not true)
|
||||
expect(result).to.eq(false);
|
||||
});
|
||||
|
||||
it("removeEventListener with correct event", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let wasCalled = false;
|
||||
|
||||
const eventListener = (event: CustomEvent): void => {
|
||||
if (event.detail.toString() === peerIdBootstrap.toString()) {
|
||||
wasCalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
eventListener
|
||||
);
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
await delay(200);
|
||||
expect(wasCalled).to.eq(true);
|
||||
|
||||
wasCalled = false; // resetting flag back to false and remove the listener
|
||||
waku.connectionManager.removeEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
eventListener
|
||||
);
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
await delay(200);
|
||||
expect(wasCalled).to.eq(false);
|
||||
});
|
||||
|
||||
it("removeEventListener with wrong event", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let wasCalled = false;
|
||||
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
if (receivedPeerId.toString() === peerIdBootstrap.toString()) {
|
||||
wasCalled = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
waku.connectionManager.removeEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE,
|
||||
({ detail: receivedPeerId }) => {
|
||||
if (receivedPeerId.toString() === peerIdBootstrap.toString()) {
|
||||
wasCalled = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
await delay(200);
|
||||
expect(wasCalled).to.eq(true);
|
||||
});
|
||||
|
||||
it("getPeersByDiscovery", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
const peers_before = await waku.connectionManager.getPeersByDiscovery();
|
||||
expect(peers_before.DISCOVERED[Tags.BOOTSTRAP]).to.deep.eq([]);
|
||||
|
||||
const ttl = 1200000;
|
||||
const tag_value = 50;
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: tag_value,
|
||||
ttl: ttl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentTime = Date.now(); // Get the current time at the point peer connect
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
|
||||
const peers_after = <PeersByDiscoveryResult>(
|
||||
await waku.connectionManager.getPeersByDiscovery()
|
||||
);
|
||||
const bootstrap_peer = peers_after.DISCOVERED[Tags.BOOTSTRAP];
|
||||
|
||||
expect(bootstrap_peer).to.not.deep.eq([]);
|
||||
expect(bootstrap_peer[0].id.toString()).to.eq(peerIdBootstrap.toString());
|
||||
expect(bootstrap_peer[0].tags.has("bootstrap")).to.be.true;
|
||||
expect(bootstrap_peer[0].tags.get("bootstrap")!.value).to.equal(tag_value);
|
||||
// Assert that the expiry is within the expected range, considering TTL
|
||||
// Note: We allow a small margin for the execution time of the code
|
||||
const marginOfError = 1000; // 1 second in milliseconds, adjust as needed
|
||||
const expiry = (bootstrap_peer[0].tags.get("bootstrap") as any).expiry;
|
||||
expect(Number(expiry)).to.be.closeTo(currentTime + ttl, marginOfError);
|
||||
});
|
||||
|
||||
it("listenerCount", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
receivedPeerId.toString() === peerIdBootstrap.toString();
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
waku.connectionManager.listenerCount(
|
||||
EPeersByDiscoveryEvents.PEER_DISCOVERY_BOOTSTRAP
|
||||
)
|
||||
).to.eq(0);
|
||||
expect(
|
||||
waku.connectionManager.listenerCount(
|
||||
EPeersByDiscoveryEvents.PEER_DISCOVERY_PEER_EXCHANGE
|
||||
)
|
||||
).to.eq(0);
|
||||
expect(
|
||||
waku.connectionManager.listenerCount(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP
|
||||
)
|
||||
).to.eq(1);
|
||||
expect(
|
||||
waku.connectionManager.listenerCount(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_PEER_EXCHANGE
|
||||
)
|
||||
).to.eq(0);
|
||||
});
|
||||
|
||||
it("dispatchEvent via connectionManager", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
waku.connectionManager.dispatchEvent(
|
||||
new CustomEvent<PeerId>(EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP, {
|
||||
detail: peerIdBootstrap
|
||||
})
|
||||
);
|
||||
expect(await peerConnectedBootstrap).to.eq(true);
|
||||
});
|
||||
|
||||
it("safeDispatchEvent", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.connectionManager.safeDispatchEvent(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
{ detail: peerIdBootstrap }
|
||||
);
|
||||
|
||||
expect(await peerConnectedBootstrap).to.eq(true);
|
||||
});
|
||||
|
||||
it("stop", async function () {
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdBootstrap = peerIdFromPrivateKey(privateKey);
|
||||
await waku.libp2p.peerStore.save(peerIdBootstrap, {
|
||||
tags: {
|
||||
[Tags.BOOTSTRAP]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const peerConnectedBootstrap = new Promise<boolean>((resolve) => {
|
||||
waku.connectionManager.addEventListener(
|
||||
EPeersByDiscoveryEvents.PEER_CONNECT_BOOTSTRAP,
|
||||
({ detail: receivedPeerId }) => {
|
||||
resolve(receivedPeerId.toString() === peerIdBootstrap.toString());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
waku.connectionManager.stop();
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdBootstrap })
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<boolean>((resolve) =>
|
||||
setTimeout(() => resolve(false), TEST_TIMEOUT - 100)
|
||||
);
|
||||
|
||||
const result = await Promise.race([peerConnectedBootstrap, timeoutPromise]);
|
||||
|
||||
// If the timeout promise resolves first, the result will be false, and we expect it to be false (test passes)
|
||||
// If the peerConnectedBootstrap resolves first, we expect its result to be true (which will now make the test fail if it's not true)
|
||||
expect(result).to.eq(false);
|
||||
});
|
||||
});
|
||||
358
packages/tests/tests/connection-mananger/network_monitor.spec.ts
Normal file
358
packages/tests/tests/connection-mananger/network_monitor.spec.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import { generateKeyPair } from "@libp2p/crypto/keys";
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { TypedEventEmitter } from "@libp2p/interface";
|
||||
import { peerIdFromPrivateKey } from "@libp2p/peer-id";
|
||||
import { Multiaddr } from "@multiformats/multiaddr";
|
||||
import { LightNode, Protocols, Tags } from "@waku/interfaces";
|
||||
import { createRelayNode } from "@waku/relay";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
DefaultTestShardInfo,
|
||||
delay,
|
||||
NOISE_KEY_1
|
||||
} from "../../src/index.js";
|
||||
import {
|
||||
makeLogFileName,
|
||||
ServiceNode,
|
||||
tearDownNodes
|
||||
} from "../../src/index.js";
|
||||
|
||||
const TEST_TIMEOUT = 30_000;
|
||||
|
||||
describe("Connection state", function () {
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
let waku: LightNode;
|
||||
|
||||
let nwaku1: ServiceNode;
|
||||
let nwaku2: ServiceNode;
|
||||
let nwaku1PeerId: Multiaddr;
|
||||
let nwaku2PeerId: Multiaddr;
|
||||
|
||||
let navigatorMock: any;
|
||||
let originalNavigator: any;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode({ networkConfig: DefaultTestShardInfo });
|
||||
nwaku1 = new ServiceNode(makeLogFileName(this.ctx) + "1");
|
||||
nwaku2 = new ServiceNode(makeLogFileName(this.ctx) + "2");
|
||||
await nwaku1.start({ filter: true });
|
||||
await nwaku2.start({ filter: true });
|
||||
nwaku1PeerId = await nwaku1.getMultiaddrWithId();
|
||||
nwaku2PeerId = await nwaku2.getMultiaddrWithId();
|
||||
|
||||
navigatorMock = { onLine: true };
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: navigatorMock,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([nwaku1, nwaku2], waku);
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: originalNavigator,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit `waku:online` event only when first peer is connected", async function () {
|
||||
let eventCount = 0;
|
||||
const connectionStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(await connectionStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("should emit `waku:offline` event only when all peers disconnect", async function () {
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
|
||||
let eventCount = 0;
|
||||
const connectionStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
await nwaku1.stop();
|
||||
await delay(400);
|
||||
expect(eventCount).to.be.eq(0);
|
||||
|
||||
await nwaku2.stop();
|
||||
expect(await connectionStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("`waku:online` between 2 js-waku relay nodes", async function () {
|
||||
const waku1 = await createRelayNode({
|
||||
staticNoiseKey: NOISE_KEY_1,
|
||||
networkConfig: DefaultTestShardInfo
|
||||
});
|
||||
const waku2 = await createRelayNode({
|
||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } },
|
||||
networkConfig: DefaultTestShardInfo
|
||||
});
|
||||
|
||||
let eventCount1 = 0;
|
||||
const connectionStatus1 = new Promise<boolean>((resolve) => {
|
||||
waku1.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount1++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
let eventCount2 = 0;
|
||||
const connectionStatus2 = new Promise<boolean>((resolve) => {
|
||||
waku2.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount2++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
await waku1.libp2p.peerStore.merge(waku2.peerId, {
|
||||
multiaddrs: waku2.libp2p.getMultiaddrs()
|
||||
});
|
||||
|
||||
await waku1.dial(waku2.peerId);
|
||||
|
||||
expect(await connectionStatus1).to.eq(true);
|
||||
expect(await connectionStatus2).to.eq(true);
|
||||
expect(eventCount1).to.be.eq(1);
|
||||
expect(eventCount2).to.be.eq(1);
|
||||
});
|
||||
|
||||
it("isConnected should return true after first peer connects", async function () {
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
});
|
||||
|
||||
it("isConnected should return false after all peers disconnect", async function () {
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await waku.dial(nwaku2PeerId, [Protocols.Filter]);
|
||||
await delay(250);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
|
||||
await waku.libp2p.hangUp(nwaku1PeerId);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
await waku.libp2p.hangUp(nwaku2PeerId);
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("isConnected return false after peer stops", async function () {
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
|
||||
await nwaku1.stop();
|
||||
await delay(400);
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
});
|
||||
|
||||
it("isConnected between 2 js-waku relay nodes", async function () {
|
||||
const waku1 = await createRelayNode({
|
||||
staticNoiseKey: NOISE_KEY_1
|
||||
});
|
||||
const waku2 = await createRelayNode({
|
||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } }
|
||||
});
|
||||
await waku1.libp2p.peerStore.merge(waku2.libp2p.peerId, {
|
||||
multiaddrs: waku2.libp2p.getMultiaddrs()
|
||||
});
|
||||
await Promise.all([waku1.dial(waku2.libp2p.peerId)]);
|
||||
await delay(400);
|
||||
expect(waku1.isConnected()).to.be.true;
|
||||
expect(waku2.isConnected()).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("waku:connection", function () {
|
||||
let navigatorMock: any;
|
||||
let originalNavigator: any;
|
||||
|
||||
let waku: LightNode;
|
||||
this.timeout(TEST_TIMEOUT);
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
waku = await createLightNode();
|
||||
originalNavigator = global.navigator;
|
||||
|
||||
navigatorMock = { onLine: true };
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: navigatorMock,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
|
||||
const eventEmmitter = new TypedEventEmitter();
|
||||
globalThis.addEventListener =
|
||||
eventEmmitter.addEventListener.bind(eventEmmitter);
|
||||
globalThis.removeEventListener =
|
||||
eventEmmitter.removeEventListener.bind(eventEmmitter);
|
||||
globalThis.dispatchEvent = eventEmmitter.dispatchEvent.bind(eventEmmitter);
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([], waku);
|
||||
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
value: originalNavigator,
|
||||
configurable: true,
|
||||
writable: false
|
||||
});
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.addEventListener = undefined;
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.removeEventListener = undefined;
|
||||
// @ts-expect-error: resetting set value
|
||||
globalThis.dispatchEvent = undefined;
|
||||
});
|
||||
|
||||
it(`should emit events and trasition isConnected state when has peers or no peers`, async function () {
|
||||
const privateKey1 = await generateKeyPair("secp256k1");
|
||||
const privateKey2 = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey1);
|
||||
const peerIdPx2 = peerIdFromPrivateKey(privateKey2);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx2, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let eventCount = 0;
|
||||
const connectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx })
|
||||
);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx2 })
|
||||
);
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectedStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
const disconnectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:disconnect", { detail: peerIdPx })
|
||||
);
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:disconnect", { detail: peerIdPx2 })
|
||||
);
|
||||
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
expect(await disconnectedStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(2);
|
||||
});
|
||||
|
||||
it("should be online or offline if network state changed", async function () {
|
||||
// have to recreate js-waku for it to pick up new globalThis
|
||||
waku = await createLightNode();
|
||||
|
||||
const privateKey = await generateKeyPair("secp256k1");
|
||||
const peerIdPx = peerIdFromPrivateKey(privateKey);
|
||||
|
||||
await waku.libp2p.peerStore.save(peerIdPx, {
|
||||
tags: {
|
||||
[Tags.PEER_EXCHANGE]: {
|
||||
value: 50,
|
||||
ttl: 1200000
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let eventCount = 0;
|
||||
const connectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
eventCount++;
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerId>("peer:connect", { detail: peerIdPx })
|
||||
);
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectedStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(1);
|
||||
|
||||
const disconnectedStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
navigatorMock.onLine = false;
|
||||
globalThis.dispatchEvent(new CustomEvent("offline"));
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.false;
|
||||
expect(await disconnectedStatus).to.eq(false);
|
||||
expect(eventCount).to.be.eq(2);
|
||||
|
||||
const connectionRecoveredStatus = new Promise<boolean>((resolve) => {
|
||||
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
navigatorMock.onLine = true;
|
||||
globalThis.dispatchEvent(new CustomEvent("online"));
|
||||
|
||||
await delay(100);
|
||||
|
||||
expect(waku.isConnected()).to.be.true;
|
||||
expect(await connectionRecoveredStatus).to.eq(true);
|
||||
expect(eventCount).to.be.eq(3);
|
||||
});
|
||||
});
|
||||
6
packages/tests/tests/connection-mananger/utils.ts
Normal file
6
packages/tests/tests/connection-mananger/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const TestContentTopic = "/test/1/waku-light-push/utf8";
|
||||
export const ClusterId = 3;
|
||||
export const TestShardInfo = {
|
||||
contentTopics: [TestContentTopic],
|
||||
clusterId: ClusterId
|
||||
};
|
||||
@ -300,16 +300,21 @@ const runTests = (strictCheckNodes: boolean): void => {
|
||||
TestShardInfo,
|
||||
{
|
||||
lightpush: true,
|
||||
filter: true
|
||||
filter: true,
|
||||
peerExchange: true
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
callback = serviceNodes.messageCollector.callback;
|
||||
|
||||
let cnt = 0;
|
||||
const peerConnectEvent = new Promise((resolve, reject) => {
|
||||
waku.libp2p.addEventListener("peer:connect", (e) => {
|
||||
resolve(e);
|
||||
cnt += 1;
|
||||
if (cnt === 2) {
|
||||
resolve(e);
|
||||
}
|
||||
});
|
||||
setTimeout(() => reject, 1000);
|
||||
});
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import type { PeerInfo } from "@libp2p/interface";
|
||||
import { multiaddr } from "@multiformats/multiaddr";
|
||||
import type { Multiaddr } from "@multiformats/multiaddr";
|
||||
import type { IWaku } from "@waku/interfaces";
|
||||
import { createLightNode } from "@waku/sdk";
|
||||
import { expect } from "chai";
|
||||
import Sinon, { SinonSpy, SinonStub } from "sinon";
|
||||
|
||||
import {
|
||||
afterEachCustom,
|
||||
beforeEachCustom,
|
||||
delay,
|
||||
makeLogFileName,
|
||||
ServiceNode,
|
||||
tearDownNodes
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("multiaddr: dialing", function () {
|
||||
let waku: IWaku;
|
||||
let nwaku: ServiceNode;
|
||||
let dialPeerSpy: SinonSpy;
|
||||
let isPeerOnSameShard: SinonStub;
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes(nwaku, waku);
|
||||
});
|
||||
|
||||
it("can dial TLS multiaddrs", async function () {
|
||||
this.timeout(20_000);
|
||||
|
||||
let tlsWorks = true;
|
||||
|
||||
waku = await createLightNode();
|
||||
await waku.start();
|
||||
try {
|
||||
// dummy multiaddr, doesn't have to be valid
|
||||
await waku.dial(multiaddr(`/ip4/127.0.0.1/tcp/30303/tls/ws`));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// if the error is of tls unsupported, the test should fail
|
||||
// for any other dial errors, the test should pass
|
||||
if (error.message === "Unsupported protocol tls") {
|
||||
tlsWorks = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(tlsWorks).to.eq(true);
|
||||
});
|
||||
|
||||
describe("does not attempt the same peer discovered multiple times more than once", function () {
|
||||
const PEER_DISCOVERY_COUNT = 3;
|
||||
let peerId: PeerId;
|
||||
let multiaddr: Multiaddr;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
nwaku = new ServiceNode(makeLogFileName(this.ctx));
|
||||
await nwaku.start();
|
||||
|
||||
waku = await createLightNode();
|
||||
|
||||
peerId = await nwaku.getPeerId();
|
||||
multiaddr = await nwaku.getMultiaddrWithId();
|
||||
|
||||
isPeerOnSameShard = Sinon.stub(
|
||||
waku.connectionManager as any,
|
||||
"isPeerOnSameShard"
|
||||
);
|
||||
isPeerOnSameShard.resolves(true);
|
||||
dialPeerSpy = Sinon.spy(waku.connectionManager as any, "dialPeer");
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
dialPeerSpy.restore();
|
||||
});
|
||||
|
||||
it("through manual discovery", async function () {
|
||||
this.timeout(20_000);
|
||||
|
||||
const discoverPeer = (): void => {
|
||||
waku.libp2p.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer:discovery", {
|
||||
detail: {
|
||||
id: peerId,
|
||||
multiaddrs: [multiaddr]
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
for (let i = 0; i < PEER_DISCOVERY_COUNT; i++) {
|
||||
discoverPeer();
|
||||
await delay(100);
|
||||
}
|
||||
|
||||
expect(dialPeerSpy.callCount).to.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,7 @@
|
||||
import { bootstrap } from "@libp2p/bootstrap";
|
||||
import type { PeerId } from "@libp2p/interface";
|
||||
import { wakuPeerExchangeDiscovery } from "@waku/discovery";
|
||||
import type { LightNode, PeersByDiscoveryResult } from "@waku/interfaces";
|
||||
import type { LightNode } from "@waku/interfaces";
|
||||
import { createLightNode, Tags } from "@waku/sdk";
|
||||
import { Logger } from "@waku/utils";
|
||||
import { expect } from "chai";
|
||||
@ -25,7 +25,6 @@ describe("Peer Exchange", function () {
|
||||
let nwaku2: ServiceNode;
|
||||
let nwaku3: ServiceNode;
|
||||
let dialPeerSpy: SinonSpy;
|
||||
let nwaku1PeerId: PeerId;
|
||||
|
||||
beforeEachCustom(this, async () => {
|
||||
nwaku1 = new ServiceNode(makeLogFileName(this.ctx) + "1");
|
||||
@ -45,14 +44,13 @@ describe("Peer Exchange", function () {
|
||||
discv5BootstrapNode: (await nwaku1.info()).enrUri,
|
||||
relay: true
|
||||
});
|
||||
nwaku1PeerId = await nwaku1.getPeerId();
|
||||
});
|
||||
|
||||
afterEachCustom(this, async () => {
|
||||
await tearDownNodes([nwaku1, nwaku2, nwaku3], waku);
|
||||
});
|
||||
|
||||
it("getPeersByDiscovery", async function () {
|
||||
it("peer exchange sets tag", async function () {
|
||||
waku = await createLightNode({
|
||||
networkConfig: DefaultTestShardInfo,
|
||||
libp2p: {
|
||||
@ -63,8 +61,10 @@ describe("Peer Exchange", function () {
|
||||
}
|
||||
});
|
||||
await waku.start();
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
const pxPeersDiscovered = new Set<PeerId>();
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
waku.libp2p.addEventListener("peer:discovery", (evt) => {
|
||||
return void (async () => {
|
||||
@ -80,23 +80,9 @@ describe("Peer Exchange", function () {
|
||||
})();
|
||||
});
|
||||
});
|
||||
expect(dialPeerSpy.callCount).to.equal(1);
|
||||
|
||||
const peers_after = <PeersByDiscoveryResult>(
|
||||
await waku.connectionManager.getPeersByDiscovery()
|
||||
);
|
||||
const discovered_peer_exchange = peers_after.DISCOVERED[Tags.PEER_EXCHANGE];
|
||||
const discovered_bootstram = peers_after.DISCOVERED[Tags.BOOTSTRAP];
|
||||
const connected_peer_exchange = peers_after.CONNECTED[Tags.PEER_EXCHANGE];
|
||||
const connected_bootstram = peers_after.CONNECTED[Tags.BOOTSTRAP];
|
||||
expect(discovered_peer_exchange.length).to.eq(1);
|
||||
expect(discovered_peer_exchange[0].id.toString()).to.eq(
|
||||
nwaku1PeerId.toString()
|
||||
);
|
||||
expect(discovered_peer_exchange[0].tags.has("peer-exchange")).to.be.true;
|
||||
expect(discovered_bootstram.length).to.eq(1);
|
||||
expect(connected_peer_exchange.length).to.eq(0);
|
||||
expect(connected_bootstram.length).to.eq(1);
|
||||
expect(dialPeerSpy.callCount).to.equal(1);
|
||||
expect(pxPeersDiscovered.size).to.equal(1);
|
||||
});
|
||||
|
||||
// will be skipped until https://github.com/waku-org/js-waku/issues/1860 is fixed
|
||||
@ -208,7 +194,7 @@ describe("Peer Exchange", function () {
|
||||
}
|
||||
});
|
||||
await waku.start();
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
|
||||
const pxPeersDiscovered = new Set<PeerId>();
|
||||
await new Promise<void>((resolve) => {
|
||||
|
||||
@ -93,7 +93,7 @@ describe("Static Sharding: Peer Management", function () {
|
||||
|
||||
await waku.start();
|
||||
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
|
||||
const pxPeersDiscovered = new Set<PeerId>();
|
||||
|
||||
@ -164,7 +164,7 @@ describe("Static Sharding: Peer Management", function () {
|
||||
}
|
||||
});
|
||||
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
|
||||
await waku.start();
|
||||
|
||||
@ -270,7 +270,7 @@ describe("Autosharding: Peer Management", function () {
|
||||
|
||||
await waku.start();
|
||||
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
|
||||
const pxPeersDiscovered = new Set<PeerId>();
|
||||
|
||||
@ -346,7 +346,7 @@ describe("Autosharding: Peer Management", function () {
|
||||
}
|
||||
});
|
||||
|
||||
dialPeerSpy = Sinon.spy((waku as any).connectionManager, "dialPeer");
|
||||
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
|
||||
|
||||
await waku.start();
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ describe("Waku Store, error handling", function () {
|
||||
if (
|
||||
!(err instanceof Error) ||
|
||||
!err.message.includes(
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance. Configured topics are: ${TestDecoder.pubsubTopic}`
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance.`
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
@ -110,7 +110,7 @@ describe("Waku Store, error handling", function () {
|
||||
if (
|
||||
!(err instanceof Error) ||
|
||||
!err.message.includes(
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance. Configured topics are: ${TestDecoder.pubsubTopic}`
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance.`
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
@ -168,7 +168,7 @@ describe("Waku Store, error handling", function () {
|
||||
if (
|
||||
!(err instanceof Error) ||
|
||||
!err.message.includes(
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance. Configured topics are: ${TestDecoder.pubsubTopic}`
|
||||
`Pubsub topic ${wrongDecoder.pubsubTopic} has not been configured on this instance.`
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
contentTopicToPubsubTopic,
|
||||
contentTopicToShardIndex,
|
||||
determinePubsubTopic,
|
||||
ensurePubsubTopicIsConfigured,
|
||||
ensureShardingConfigured,
|
||||
ensureValidContentTopic,
|
||||
pubsubTopicToSingleShardInfo,
|
||||
@ -357,32 +356,6 @@ describe("pubsubTopicToSingleShardInfo with various invalid formats", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensurePubsubTopicIsConfigured", () => {
|
||||
it("should not throw an error for a single configured topic", () => {
|
||||
const topic = "/waku/2/rs/1/2";
|
||||
const configuredTopics = [topic];
|
||||
expect(() =>
|
||||
ensurePubsubTopicIsConfigured(topic, configuredTopics)
|
||||
).not.to.throw();
|
||||
});
|
||||
|
||||
it("should not throw an error when the topic is within a list of configured topics", () => {
|
||||
const topic = "/waku/2/rs/1/2";
|
||||
const configuredTopics = ["/waku/2/rs/1/1", topic, "/waku/2/rs/1/3"];
|
||||
expect(() =>
|
||||
ensurePubsubTopicIsConfigured(topic, configuredTopics)
|
||||
).not.to.throw();
|
||||
});
|
||||
|
||||
it("should throw an error for an unconfigured topic", () => {
|
||||
const topic = "/waku/2/rs/1/2";
|
||||
const configuredTopics = ["/waku/2/rs/1/3"];
|
||||
expect(() =>
|
||||
ensurePubsubTopicIsConfigured(topic, configuredTopics)
|
||||
).to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe("determinePubsubTopic", () => {
|
||||
const contentTopic = "/app/46/sometopic/someencoding";
|
||||
it("should return the pubsub topic directly if a string is provided", () => {
|
||||
|
||||
@ -67,6 +67,9 @@ export const singleShardInfosToShardInfo = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated will be removed, use cluster and shard comparison directly
|
||||
*/
|
||||
export const shardInfoToPubsubTopics = (
|
||||
shardInfo: Partial<NetworkConfig>
|
||||
): PubsubTopic[] => {
|
||||
@ -103,6 +106,9 @@ export const shardInfoToPubsubTopics = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated will be removed
|
||||
*/
|
||||
export const pubsubTopicToSingleShardInfo = (
|
||||
pubsubTopics: PubsubTopic
|
||||
): SingleShardInfo => {
|
||||
@ -161,19 +167,6 @@ export const pubsubTopicsToShardInfo = (
|
||||
};
|
||||
};
|
||||
|
||||
//TODO: move part of BaseProtocol instead of utils
|
||||
// return `ProtocolError.TOPIC_NOT_CONFIGURED` instead of throwing
|
||||
export function ensurePubsubTopicIsConfigured(
|
||||
pubsubTopic: PubsubTopic,
|
||||
configuredTopics: PubsubTopic[]
|
||||
): void {
|
||||
if (!configuredTopics.includes(pubsubTopic)) {
|
||||
throw new Error(
|
||||
`Pubsub topic ${pubsubTopic} has not been configured on this instance. Configured topics are: ${configuredTopics}. Please update your configuration by passing in the topic during Waku node instantiation.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ContentTopic {
|
||||
generation: number;
|
||||
application: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user