90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
/**
|
|
* Super-peer topology — well-resourced "super peers" form a full
|
|
* mesh among themselves and accept connections from any peer; ordinary
|
|
* peers connect to a bounded number of super peers (default 3).
|
|
*/
|
|
|
|
import type { PeerInfo } from "../../types/peer.types.js";
|
|
import {
|
|
type AdmitDecision,
|
|
pickLowestScore,
|
|
type TopologySelectOptions,
|
|
type TopologyStrategy,
|
|
} from "./topology-strategy.js";
|
|
|
|
export interface SuperPeerStrategyOptions {
|
|
/** Whether this node is itself a super peer. */
|
|
readonly selfIsSuperPeer: boolean;
|
|
/** Set of peer ids known to be super peers. */
|
|
readonly superPeerIds: ReadonlySet<string>;
|
|
/** Cap on outbound super-peer connections for non-super peers. */
|
|
readonly targetSuperPeers?: number;
|
|
}
|
|
|
|
const DEFAULT_TARGET_SUPER_PEERS = 3;
|
|
|
|
export class SuperPeerStrategy implements TopologyStrategy {
|
|
public readonly name = "super-peer" as const;
|
|
private readonly selfIsSuperPeer: boolean;
|
|
private readonly superPeerIds: ReadonlySet<string>;
|
|
private readonly targetSuperPeers: number;
|
|
|
|
public constructor(opts: SuperPeerStrategyOptions) {
|
|
this.selfIsSuperPeer = opts.selfIsSuperPeer;
|
|
this.superPeerIds = opts.superPeerIds;
|
|
this.targetSuperPeers =
|
|
opts.targetSuperPeers ?? DEFAULT_TARGET_SUPER_PEERS;
|
|
}
|
|
|
|
public selectConnections(
|
|
currentPeers: readonly PeerInfo[],
|
|
newPeer: PeerInfo,
|
|
options: TopologySelectOptions
|
|
): readonly PeerInfo[] {
|
|
if (newPeer.id === options.selfPeerId) return [];
|
|
if (this.selfIsSuperPeer) {
|
|
if (currentPeers.length >= options.maxPeers) return [];
|
|
return [newPeer];
|
|
}
|
|
if (!this.superPeerIds.has(newPeer.id)) return [];
|
|
const connectedSuperPeers = currentPeers.filter((p) =>
|
|
this.superPeerIds.has(p.id)
|
|
).length;
|
|
if (connectedSuperPeers >= this.targetSuperPeers) return [];
|
|
if (currentPeers.length >= options.maxPeers) return [];
|
|
return [newPeer];
|
|
}
|
|
|
|
public admitOrEvict(
|
|
currentPeers: readonly PeerInfo[],
|
|
newPeer: PeerInfo,
|
|
options: TopologySelectOptions
|
|
): AdmitDecision {
|
|
if (currentPeers.some((p) => p.id === newPeer.id)) {
|
|
return { admit: false };
|
|
}
|
|
if (this.selfIsSuperPeer) {
|
|
if (currentPeers.length < options.maxPeers) {
|
|
return { admit: true };
|
|
}
|
|
const evict = pickLowestScore(
|
|
currentPeers.filter((p) => !this.superPeerIds.has(p.id)),
|
|
options.metricsFor
|
|
);
|
|
if (evict === undefined) return { admit: false };
|
|
return { admit: true, evict };
|
|
}
|
|
if (!this.superPeerIds.has(newPeer.id)) return { admit: false };
|
|
const connectedSuperPeers = currentPeers.filter((p) =>
|
|
this.superPeerIds.has(p.id)
|
|
);
|
|
if (
|
|
connectedSuperPeers.length < this.targetSuperPeers &&
|
|
currentPeers.length < options.maxPeers
|
|
) {
|
|
return { admit: true };
|
|
}
|
|
return { admit: false };
|
|
}
|
|
}
|