Skip to content

fix(network, DHT): stricter typing for event listeners in interfaces #2517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
6 changes: 2 additions & 4 deletions packages/dht/src/dht/DhtNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { StoreManager } from './store/StoreManager'
import { StoreRpcRemote } from './store/StoreRpcRemote'
import { getLocalRegionByCoordinates, getLocalRegionWithCache } from '@streamr/cdn-location'

export interface DhtNodeEvents {
export interface DhtNodeEvents extends TransportEvents {
nearbyContactAdded: (peerDescriptor: PeerDescriptor) => void
nearbyContactRemoved: (peerDescriptor: PeerDescriptor) => void
randomContactAdded: (peerDescriptor: PeerDescriptor) => void
Expand Down Expand Up @@ -133,9 +133,7 @@ const PERIODICAL_PING_INTERVAL = 60 * 1000
// TODO move this to trackerless-network package and change serviceId to be a required paramater
export const CONTROL_LAYER_NODE_SERVICE_ID = 'layer0'

export type Events = TransportEvents & DhtNodeEvents

export class DhtNode extends EventEmitter<Events> implements ITransport {
export class DhtNode extends EventEmitter<DhtNodeEvents> implements ITransport {

private readonly options: StrictDhtNodeOptions
private rpcCommunicator?: RoutingRpcCommunicator
Expand Down
18 changes: 3 additions & 15 deletions packages/dht/src/transport/ITransport.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Message, PeerDescriptor } from '../../generated/packages/dht/protos/DhtRpc'
import { EventEmitterType } from '@streamr/utils'

export interface TransportEvents {
connected: (peerDescriptor: PeerDescriptor) => void
disconnected: (peerDescriptor: PeerDescriptor, gracefulLeave: boolean) => void
message: (message: Message) => void
connected: (peerDescriptor: PeerDescriptor) => void
}

export interface SendOptions {
Expand All @@ -18,20 +19,7 @@ export const DEFAULT_SEND_OPTIONS = {
doNotBufferWhileConnecting: false
}

export interface ITransport {
on<T extends keyof TransportEvents>(eventName: T, listener: (message: Message) => void): void
on<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
on<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor, gracefulLeave: boolean) => void): void

once<T extends keyof TransportEvents>(eventName: T, listener: (message: Message) => void): void
once<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
once<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor,
gracefulLeave: boolean) => void): void

off<T extends keyof TransportEvents>(eventName: T, listener: (message: Message) => void): void
off<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
off<T extends keyof TransportEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor, gracefulLeave: boolean) => void): void

export interface ITransport extends EventEmitterType<TransportEvents> {
send(msg: Message, opts?: SendOptions): Promise<void>
getLocalPeerDescriptor(): PeerDescriptor
stop(): void | Promise<void>
Expand Down
2 changes: 1 addition & 1 deletion packages/dht/test/integration/RouteMessage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DhtNode, Events as DhtNodeEvents } from '../../src/dht/DhtNode'
import { DhtNode, DhtNodeEvents } from '../../src/dht/DhtNode'
import { Message, PeerDescriptor, RouteMessageWrapper } from '../../generated/packages/dht/protos/DhtRpc'
import { RpcMessage } from '../../generated/packages/proto-rpc/protos/ProtoRpc'
import { Logger, runAndWaitForEvents3, until } from '@streamr/utils'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,46 +117,48 @@ export class ContentDeliveryLayerNode extends EventEmitter<Events> {
})
}

/* eslint-disable indent */
// Linting for indentation is broken in addManagedEventListener for some reason.
async start(): Promise<void> {
this.started = true
this.registerDefaultServerMethods()
addManagedEventListener(
addManagedEventListener<'nearbyContactAdded', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'nearbyContactAdded',
() => this.onNearbyContactAdded(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'nearbyContactRemoved', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'nearbyContactRemoved',
() => this.onNearbyContactRemoved(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'randomContactAdded', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'randomContactAdded',
() => this.onRandomContactAdded(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'randomContactRemoved', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'randomContactRemoved',
() => this.onRandomContactRemoved(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'ringContactAdded', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'ringContactAdded',
() => this.onRingContactsUpdated(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'ringContactRemoved', (node: PeerDescriptor) => void>(
this.options.discoveryLayerNode,
'ringContactRemoved',
() => this.onRingContactsUpdated(),
this.abortController.signal
)
addManagedEventListener(
addManagedEventListener<'disconnected', (node: PeerDescriptor) => void>(
this.options.transport,
'disconnected',
(peerDescriptor: PeerDescriptor) => this.onNodeDisconnected(peerDescriptor),
Expand Down
10 changes: 2 additions & 8 deletions packages/trackerless-network/src/logic/DiscoveryLayerNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DhtAddress, PeerDescriptor, RingContacts } from '@streamr/dht'
import { EventEmitterType } from '@streamr/utils'

export interface DiscoveryLayerNodeEvents {
manualRejoinRequired: () => void
Expand All @@ -9,14 +10,7 @@ export interface DiscoveryLayerNodeEvents {
ringContactAdded: (peerDescriptor: PeerDescriptor) => void
ringContactRemoved: (peerDescriptor: PeerDescriptor) => void
}

export interface DiscoveryLayerNode {
on<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
on<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: () => void): void
off<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
off<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: () => void): void
once<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
once<T extends keyof DiscoveryLayerNodeEvents>(eventName: T, listener: () => void): void
export interface DiscoveryLayerNode extends EventEmitterType<DiscoveryLayerNodeEvents> {
removeContact: (nodeId: DhtAddress) => void
getClosestContacts: (maxCount?: number) => PeerDescriptor[]
getRandomContacts: (maxCount?: number) => PeerDescriptor[]
Expand Down
4 changes: 3 additions & 1 deletion packages/trackerless-network/src/logic/proxy/ProxyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,11 @@ export class ProxyClient extends EventEmitter<Events> {
}
}

/* eslint-disable indent */
// Linting for indentation is broken in addManagedEventListener for some reason.
async start(): Promise<void> {
this.registerDefaultServerMethods()
addManagedEventListener(
addManagedEventListener<'disconnected', (node: PeerDescriptor) => void>(
this.options.transport,
'disconnected',
// TODO should we catch possible promise rejection?
Expand Down
35 changes: 35 additions & 0 deletions packages/utils/src/EventEmitterType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Utility type EmitterOf<T>. By deriving an interface from this type,
// you declare that the interface has the standard EventEmitter3
// listener setter functions 'on', 'off', 'once' for each event type
// defined in T.

// Convert EventEmitter3 event types to corresponding listener
// setter function types (eg. types of 'on', 'off', 'once').
// For example,
// message: (payload: Payload) => void
// -> message: (event: 'message', listener: (payload: Payload) => void) => void

type ListenerSetterTypes<T> = {
[K in keyof T]: (event: K, listener: T[K]) => void
}

// In typescript, type of on overloaded function is
// the intersection of the overloaded function types
// (https://stackoverflow.com/a/54887669).
// The following code builds the intersection type
// of the given listener setter function types
// (https://stackoverflow.com/a/66445507).

type Intersection<T> = {
[K in keyof T]: (x: T[K]) => void
}[keyof T] extends
(x: infer I) => void ? I : never

type OverloadedListenerSetter<T> = Intersection<ListenerSetterTypes<T>>

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type EventEmitterType<T> = {
on: OverloadedListenerSetter<T>
off: OverloadedListenerSetter<T>
once: OverloadedListenerSetter<T>
}
2 changes: 2 additions & 0 deletions packages/utils/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { toLengthPrefixedFrame, LengthPrefixedFrameDecoder } from './lengthPrefi
import { verifySignature, createSignature, recoverSignerUserId, hash } from './signingUtils'
import { ipv4ToNumber, numberToIpv4 } from './ipv4ToNumber'
import { MapWithTtl } from './MapWithTtl'
import { EventEmitterType } from './EventEmitterType'

export {
BrandedString,
Expand Down Expand Up @@ -108,6 +109,7 @@ export {
numberToIpv4,
hash,
MapWithTtl,
EventEmitterType,
Cache
}

Expand Down