feat: StoreConnect events (#2601)

* store connect evt: use enum instead of free strings for Waku event types

* store connect evt: more accurate enum name

* store connect evt: add store connect event on peer manager

* store connect evt: simplify logic statements

* store connect evt: test store connect

* store connect evt: export event types

* test: use enum

* Shorter name for waku events
This commit is contained in:
fryorcraken 2025-08-27 12:29:22 +10:00 committed by GitHub
parent 78c856d079
commit 0dfbcf6b6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 177 additions and 107 deletions

View File

@ -391,7 +391,7 @@ test.describe("Waku Server API", () => {
});
await axios.post(`${API_URL}/admin/v1/start-node`);
// Connect to peers
// FilterConnect to peers
const dialResponse = await axios.post(`${API_URL}/admin/v1/peers`, {
peerMultiaddrs: PEERS
});
@ -425,7 +425,7 @@ test.describe("Waku Server API", () => {
});
await axios.post(`${API_URL}/admin/v1/start-node`);
// Connect to peers
// FilterConnect to peers
await axios.post(`${API_URL}/admin/v1/peers`, {
peerMultiaddrs: PEERS
});
@ -465,7 +465,7 @@ test.describe("Waku Server API", () => {
});
await axios.post(`${API_URL}/admin/v1/start-node`);
// Connect to peers
// FilterConnect to peers
await axios.post(`${API_URL}/admin/v1/peers`, {
peerMultiaddrs: PEERS
});
@ -577,7 +577,7 @@ test.describe("Waku Server API", () => {
// Start node
await axios.post(`${API_URL}/admin/v1/start-node`);
// Connect to peers
// FilterConnect to peers
await axios.post(`${API_URL}/admin/v1/peers`, {
peerMultiaddrs: PEERS
});

View File

@ -3,7 +3,8 @@ import { multiaddr } from "@multiformats/multiaddr";
import {
CONNECTION_LOCKED_TAG,
IWakuEventEmitter,
Tags
Tags,
WakuEvent
} from "@waku/interfaces";
import { expect } from "chai";
import sinon from "sinon";
@ -143,7 +144,7 @@ describe("ConnectionLimiter", () => {
.true;
expect(
(events.addEventListener as sinon.SinonStub).calledWith(
"waku:connection",
WakuEvent.Connection,
sinon.match.func
)
).to.be.true;
@ -178,7 +179,7 @@ describe("ConnectionLimiter", () => {
.true;
expect(
(events.removeEventListener as sinon.SinonStub).calledWith(
"waku:connection",
WakuEvent.Connection,
sinon.match.func
)
).to.be.true;

View File

@ -5,7 +5,8 @@ import {
IWakuEventEmitter,
Libp2p,
Libp2pEventHandler,
Tags
Tags,
WakuEvent
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
@ -69,7 +70,10 @@ export class ConnectionLimiter implements IConnectionLimiter {
);
}
this.events.addEventListener("waku:connection", this.onWakuConnectionEvent);
this.events.addEventListener(
WakuEvent.Connection,
this.onWakuConnectionEvent
);
/**
* NOTE: Event is not being emitted on closing nor losing a connection.
@ -90,7 +94,7 @@ export class ConnectionLimiter implements IConnectionLimiter {
public stop(): void {
this.events.removeEventListener(
"waku:connection",
WakuEvent.Connection,
this.onWakuConnectionEvent
);
@ -274,11 +278,9 @@ export class ConnectionLimiter implements IConnectionLimiter {
.map((id) => this.getPeer(id))
);
const bootstrapPeers = peers.filter(
return peers.filter(
(peer) => peer && peer.tags.has(Tags.BOOTSTRAP)
) as Peer[];
return bootstrapPeers;
}
private async getPeer(peerId: PeerId): Promise<Peer | null> {

View File

@ -1,4 +1,4 @@
import { IWakuEventEmitter, Libp2p } from "@waku/interfaces";
import { IWakuEventEmitter, Libp2p, WakuEvent } from "@waku/interfaces";
import { expect } from "chai";
import sinon from "sinon";
@ -341,7 +341,7 @@ describe("NetworkMonitor", () => {
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.type).to.equal(WakuEvent.Connection);
expect(dispatchedEvent.detail).to.be.true;
});
});

View File

@ -1,4 +1,4 @@
import { IWakuEventEmitter, Libp2p } from "@waku/interfaces";
import { IWakuEventEmitter, Libp2p, WakuEvent } from "@waku/interfaces";
type NetworkMonitorConstructorOptions = {
libp2p: Libp2p;
@ -104,7 +104,7 @@ export class NetworkMonitor implements INetworkMonitor {
private dispatchNetworkEvent(): void {
this.events.dispatchEvent(
new CustomEvent<boolean>("waku:connection", {
new CustomEvent<boolean>(WakuEvent.Connection, {
detail: this.isConnected()
})
);

View File

@ -25,28 +25,33 @@ export type CreateEncoderParams = CreateDecoderParams & {
ephemeral?: boolean;
};
export enum WakuEvent {
Connection = "waku:connection",
Health = "waku:health"
}
export interface IWakuEvents {
/**
* Emitted when a connection is established or lost.
*
* @example
* ```typescript
* waku.addEventListener("waku:connection", (event) => {
* waku.addEventListener(WakuEvent.Connection, (event) => {
* console.log(event.detail); // true if connected, false if disconnected
* });
*/
"waku:connection": CustomEvent<boolean>;
[WakuEvent.Connection]: CustomEvent<boolean>;
/**
* Emitted when the health status changes.
*
* @example
* ```typescript
* waku.addEventListener("waku:health", (event) => {
* waku.addEventListener(WakuEvent.Health, (event) => {
* console.log(event.detail); // 'Unhealthy', 'MinimallyHealthy', or 'SufficientlyHealthy'
* });
*/
"waku:health": CustomEvent<HealthStatus>;
[WakuEvent.Health]: CustomEvent<HealthStatus>;
}
export type IWakuEventEmitter = TypedEventEmitter<IWakuEvents>;
@ -61,12 +66,12 @@ export interface IWaku {
/**
* Emits events related to the Waku node.
* Those are:
* - "waku:connection"
* - "waku:health"
* - WakuEvent.Connection
* - WakuEvent.Health
*
* @example
* ```typescript
* waku.events.addEventListener("waku:connection", (event) => {
* waku.events.addEventListener(WakuEvent.Connection, (event) => {
* console.log(event.detail); // true if connected, false if disconnected
* });
* ```

View File

@ -363,11 +363,11 @@ export class Subscription {
private setupEventListeners(): void {
this.peerManager.events.addEventListener(
PeerManagerEventNames.Connect,
PeerManagerEventNames.FilterConnect,
this.onPeerConnected as Libp2pEventHandler
);
this.peerManager.events.addEventListener(
PeerManagerEventNames.Disconnect,
PeerManagerEventNames.FilterDisconnect,
this.onPeerDisconnected as Libp2pEventHandler
);
}
@ -398,11 +398,11 @@ export class Subscription {
private disposeEventListeners(): void {
this.peerManager.events.removeEventListener(
PeerManagerEventNames.Connect,
PeerManagerEventNames.FilterConnect,
this.onPeerConnected as Libp2pEventHandler
);
this.peerManager.events.removeEventListener(
PeerManagerEventNames.Disconnect,
PeerManagerEventNames.FilterDisconnect,
this.onPeerDisconnected as Libp2pEventHandler
);
}

View File

@ -1,6 +1,11 @@
import { Connection, Peer } from "@libp2p/interface";
import { FilterCodecs, LightPushCodec } from "@waku/core";
import { HealthStatus, IWakuEventEmitter, Libp2p } from "@waku/interfaces";
import {
HealthStatus,
IWakuEventEmitter,
Libp2p,
WakuEvent
} from "@waku/interfaces";
import { expect } from "chai";
import sinon from "sinon";
@ -34,8 +39,9 @@ describe("HealthIndicator", () => {
// Start monitoring
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -53,8 +59,9 @@ describe("HealthIndicator", () => {
healthIndicator.start();
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -76,8 +83,9 @@ describe("HealthIndicator", () => {
healthIndicator.start();
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -131,8 +139,9 @@ describe("HealthIndicator", () => {
peerStoreStub.withArgs(connection2.remotePeer).resolves(peer2);
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -144,8 +153,9 @@ describe("HealthIndicator", () => {
);
const statusChangePromise2 = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -166,8 +176,9 @@ describe("HealthIndicator", () => {
sinon.stub(libp2p.peerStore, "get").resolves(peer);
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -189,8 +200,9 @@ describe("HealthIndicator", () => {
sinon.stub(libp2p.peerStore, "get").rejects(new Error("Peer not found"));
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});
@ -217,8 +229,9 @@ describe("HealthIndicator", () => {
peerStoreStub.withArgs(connection2.remotePeer).resolves(peer2);
const statusChangePromise = new Promise<HealthStatus>((resolve) => {
events.addEventListener("waku:health", (e: CustomEvent<HealthStatus>) =>
resolve(e.detail)
events.addEventListener(
WakuEvent.Health,
(e: CustomEvent<HealthStatus>) => resolve(e.detail)
);
});

View File

@ -1,6 +1,11 @@
import type { IdentifyResult, PeerId } from "@libp2p/interface";
import { FilterCodecs, LightPushCodec } from "@waku/core";
import { HealthStatus, IWakuEventEmitter, Libp2p } from "@waku/interfaces";
import {
HealthStatus,
IWakuEventEmitter,
Libp2p,
WakuEvent
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
type PeerEvent<T> = (_event: CustomEvent<T>) => void;
@ -130,7 +135,7 @@ export class HealthIndicator implements IHealthIndicator {
if (this.value !== newValue) {
this.value = newValue;
this.events.dispatchEvent(
new CustomEvent<HealthStatus>("waku:health", {
new CustomEvent<HealthStatus>(WakuEvent.Health, {
detail: this.value
})
);

View File

@ -43,10 +43,7 @@ describe("PeerManager", () => {
};
const skipIfNoPeers = (result: PeerId[] | null): boolean => {
if (!result || result.length === 0) {
return true;
}
return false;
return !result || result.length === 0;
};
beforeEach(() => {
@ -151,20 +148,27 @@ describe("PeerManager", () => {
});
it("should dispatch connect and disconnect events", () => {
const connectSpy = sinon.spy();
const disconnectSpy = sinon.spy();
const filterConnectSpy = sinon.spy();
const storeConnectSpy = sinon.spy();
const filterDisconnectSpy = sinon.spy();
peerManager.events.addEventListener(
PeerManagerEventNames.Connect,
connectSpy
PeerManagerEventNames.FilterConnect,
filterConnectSpy
);
peerManager.events.addEventListener(
PeerManagerEventNames.Disconnect,
disconnectSpy
PeerManagerEventNames.StoreConnect,
storeConnectSpy
);
peerManager.events.addEventListener(
PeerManagerEventNames.FilterDisconnect,
filterDisconnectSpy
);
peerManager["dispatchFilterPeerConnect"](peers[0].id);
peerManager["dispatchStorePeerConnect"](peers[0].id);
peerManager["dispatchFilterPeerDisconnect"](peers[0].id);
expect(connectSpy.calledOnce).to.be.true;
expect(disconnectSpy.calledOnce).to.be.true;
expect(filterConnectSpy.calledOnce).to.be.true;
expect(storeConnectSpy.calledOnce).to.be.true;
expect(filterDisconnectSpy.calledOnce).to.be.true;
});
it("should handle onConnected and onDisconnected", async () => {

View File

@ -34,20 +34,26 @@ type GetPeersParams = {
};
export enum PeerManagerEventNames {
Connect = "filter:connect",
Disconnect = "filter:disconnect"
FilterConnect = "filter:connect",
FilterDisconnect = "filter:disconnect",
StoreConnect = "store:connect"
}
interface IPeerManagerEvents {
export interface IPeerManagerEvents {
/**
* Notifies about Filter peer being connected.
*/
[PeerManagerEventNames.Connect]: CustomEvent<PeerId>;
[PeerManagerEventNames.FilterConnect]: CustomEvent<PeerId>;
/**
* Notifies about Filter peer being disconnected.
*/
[PeerManagerEventNames.Disconnect]: CustomEvent<PeerId>;
[PeerManagerEventNames.FilterDisconnect]: CustomEvent<PeerId>;
/**
* Notifies about a Store peer being connected.
*/
[PeerManagerEventNames.StoreConnect]: CustomEvent<PeerId>;
}
/**
@ -198,13 +204,14 @@ export class PeerManager {
private async onConnected(event: CustomEvent<IdentifyResult>): Promise<void> {
const result = event.detail;
const isFilterPeer = result.protocols.includes(
this.matchProtocolToCodec(Protocols.Filter)
);
if (isFilterPeer) {
if (
result.protocols.includes(this.matchProtocolToCodec(Protocols.Filter))
) {
this.dispatchFilterPeerConnect(result.peerId);
}
if (result.protocols.includes(this.matchProtocolToCodec(Protocols.Store))) {
this.dispatchStorePeerConnect(result.peerId);
}
}
private async onDisconnected(event: CustomEvent<PeerId>): Promise<void> {
@ -261,18 +268,24 @@ export class PeerManager {
}
const wasUnlocked = new Date(value).getTime();
return Date.now() - wasUnlocked >= 10_000 ? true : false;
return Date.now() - wasUnlocked >= 10_000;
}
private dispatchFilterPeerConnect(id: PeerId): void {
this.events.dispatchEvent(
new CustomEvent(PeerManagerEventNames.Connect, { detail: id })
new CustomEvent(PeerManagerEventNames.FilterConnect, { detail: id })
);
}
private dispatchStorePeerConnect(id: PeerId): void {
this.events.dispatchEvent(
new CustomEvent(PeerManagerEventNames.StoreConnect, { detail: id })
);
}
private dispatchFilterPeerDisconnect(id: PeerId): void {
this.events.dispatchEvent(
new CustomEvent(PeerManagerEventNames.Disconnect, { detail: id })
new CustomEvent(PeerManagerEventNames.FilterDisconnect, { detail: id })
);
}

View File

@ -3,7 +3,7 @@ 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 { LightNode, Protocols, Tags, WakuEvent } from "@waku/interfaces";
import { createRelayNode } from "@waku/relay";
import { createLightNode } from "@waku/sdk";
import { expect } from "chai";
@ -65,10 +65,13 @@ describe("Connection state", function () {
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);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
eventCount++;
resolve(status);
}
);
});
await waku.dial(nwaku1PeerId, [Protocols.Filter]);
@ -87,10 +90,13 @@ describe("Connection state", function () {
let eventCount = 0;
const connectionStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
eventCount++;
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
eventCount++;
resolve(status);
}
);
});
await nwaku1.stop();
@ -116,18 +122,24 @@ describe("Connection state", function () {
let eventCount1 = 0;
const connectionStatus1 = new Promise<boolean>((resolve) => {
waku1.events.addEventListener("waku:connection", ({ detail: status }) => {
eventCount1++;
resolve(status);
});
waku1.events.addEventListener(
WakuEvent.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);
});
waku2.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
eventCount2++;
resolve(status);
}
);
});
await waku1.libp2p.peerStore.merge(waku2.peerId, {
@ -191,7 +203,7 @@ describe("Connection state", function () {
});
});
describe("waku:connection", function () {
describe(WakuEvent.Connection, function () {
let navigatorMock: any;
let originalNavigator: any;
@ -259,10 +271,13 @@ describe("waku:connection", function () {
let eventCount = 0;
const connectedStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
eventCount++;
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
eventCount++;
resolve(status);
}
);
});
waku.libp2p.dispatchEvent(
@ -279,9 +294,12 @@ describe("waku:connection", function () {
expect(eventCount).to.be.eq(1);
const disconnectedStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
resolve(status);
}
);
});
waku.libp2p.dispatchEvent(
@ -314,10 +332,13 @@ describe("waku:connection", function () {
let eventCount = 0;
const connectedStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
eventCount++;
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
eventCount++;
resolve(status);
}
);
});
waku.libp2p.dispatchEvent(
@ -331,9 +352,12 @@ describe("waku:connection", function () {
expect(eventCount).to.be.eq(1);
const disconnectedStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
resolve(status);
}
);
});
navigatorMock.onLine = false;
@ -346,9 +370,12 @@ describe("waku:connection", function () {
expect(eventCount).to.be.eq(2);
const connectionRecoveredStatus = new Promise<boolean>((resolve) => {
waku.events.addEventListener("waku:connection", ({ detail: status }) => {
resolve(status);
});
waku.events.addEventListener(
WakuEvent.Connection,
({ detail: status }) => {
resolve(status);
}
);
});
navigatorMock.onLine = true;