rhizome-node/src/peers.ts
Ladd 002c0a9fa9 trying to get jest to exit cleanly when running all tests.
Seems like maybe something in libp2p. Giving up for now.
2024-12-31 15:27:12 -06:00

195 lines
6.5 KiB
TypeScript

import Debug from 'debug';
import {Message} from 'zeromq';
import {Delta} from "./delta.js";
import {RhizomeNode} from "./node.js";
import {Subscription} from './pub-sub.js';
import {PeerRequest, RequestSocket, ResponseSocket} from "./request-reply.js";
const debug = Debug('rz:peers');
export class PeerAddress {
addr: string;
port: number;
constructor(addr: string, port: number) {
this.addr = addr;
this.port = port;
}
static fromString(addrString: string): PeerAddress {
const [addr, port] = addrString.trim().split(':');
return new PeerAddress(addr, parseInt(port));
}
toAddrString() {
return `${this.addr}:${this.port}`;
}
toJSON() {
return this.toAddrString();
}
isEqual(other: PeerAddress) {
return this.addr === other.addr && this.port === other.port;
}
};
export function parseAddressList(input: string): PeerAddress[] {
return input.split(',')
.filter(x => !!x)
.map((peer: string) => PeerAddress.fromString(peer));
}
export enum RequestMethods {
GetPublishAddress,
AskForDeltas
}
class Peer {
rhizomeNode: RhizomeNode;
reqAddr: PeerAddress;
reqSock?: RequestSocket;
publishAddr: PeerAddress | undefined;
isSelf: boolean;
isSeedPeer: boolean;
subscription?: Subscription;
constructor(rhizomeNode: RhizomeNode, reqAddr: PeerAddress) {
this.rhizomeNode = rhizomeNode;
this.reqAddr = reqAddr;
this.isSelf = reqAddr.isEqual(this.rhizomeNode.myRequestAddr);
this.isSeedPeer = this.rhizomeNode.config.seedPeers.some((seedPeer) => reqAddr.isEqual(seedPeer));
}
async request(method: RequestMethods): Promise<Message> {
if (!this.reqSock) {
this.reqSock = this.rhizomeNode.requestReply.createRequestSocket(this.reqAddr);
}
return this.reqSock.request(method);
}
async subscribeDeltas() {
if (!this.publishAddr) {
debug(`[${this.rhizomeNode.config.peerId}]`, `Requesting publish addr from peer ${this.reqAddr.toAddrString()}`);
const res = await this.request(RequestMethods.GetPublishAddress);
this.publishAddr = PeerAddress.fromString(res.toString());
debug(`[${this.rhizomeNode.config.peerId}]`, `Received publish addr ${this.publishAddr.toAddrString()} from peer ${this.reqAddr.toAddrString()}`);
}
debug(`[${this.rhizomeNode.config.peerId}]`, `Subscribing to peer ${this.reqAddr.toAddrString()}`);
// ZeroMQ subscription
this.subscription = this.rhizomeNode.pubSub.subscribe(
this.publishAddr,
this.rhizomeNode.config.pubSubTopic,
(sender, msg) => {
const delta = this.rhizomeNode.deltaStream.deserializeDelta(msg);
delta.receivedFrom = sender;
debug(`[${this.rhizomeNode.config.peerId}]`, `Received delta: ${JSON.stringify(delta)}`);
this.rhizomeNode.deltaStream.ingestDelta(delta);
});
this.subscription.start();
}
async askForDeltas(): Promise<Delta[]> {
// TODO as a first approximation we are trying to cram the entire history
// of accepted deltas, into one (potentially gargantuan) json message.
// A second approximation would be to stream the deltas.
// Third pass should find a way to reduce the number of deltas transmitted.
// TODO: requestTimeout
const res = await this.request(RequestMethods.AskForDeltas);
const deltas = JSON.parse(res.toString());
return deltas;
}
}
export class Peers {
rhizomeNode: RhizomeNode;
peers: Peer[] = [];
constructor(rhizomeNode: RhizomeNode) {
this.rhizomeNode = rhizomeNode;
// Add self to the list of peers, but don't connect
this.addPeer(this.rhizomeNode.myRequestAddr);
this.rhizomeNode.requestReply.registerRequestHandler(async (req: PeerRequest, res: ResponseSocket) => {
debug(`[${this.rhizomeNode.config.peerId}]`, 'Inspecting peer request');
switch (req.method) {
case RequestMethods.GetPublishAddress: {
debug(`[${this.rhizomeNode.config.peerId}]`, 'It\'s a request for our publish address');
await res.send(this.rhizomeNode.myPublishAddr.toAddrString());
break;
}
case RequestMethods.AskForDeltas: {
debug(`[${this.rhizomeNode.config.peerId}]`, 'It\'s a request for deltas');
// TODO: stream these rather than
// trying to write them all in one message
const deltas = this.rhizomeNode.deltaStream.deltasAccepted;
debug(`[${this.rhizomeNode.config.peerId}]`, `Sending ${deltas.length} deltas`);
await res.send(JSON.stringify(deltas));
break;
}
}
});
}
start() {
// TODO: Move this somewhere that makes more sense
this.rhizomeNode.pubSub.subscribeTopic(
this.rhizomeNode.config.pubSubTopic,
(sender, msg) => {
const delta = this.rhizomeNode.deltaStream.deserializeDelta(msg);
delta.receivedFrom = sender;
debug(`[${this.rhizomeNode.config.peerId}]`, `Received delta: ${JSON.stringify(delta)}`);
this.rhizomeNode.deltaStream.ingestDelta(delta);
}
);
}
stop() {
debug(`[${this.rhizomeNode.config.peerId}]`, 'Closing all peer request sockets');
for (const peer of this.peers) {
peer.reqSock?.close();
}
}
addPeer(addr: PeerAddress): Peer {
const peer = new Peer(this.rhizomeNode, addr);
this.peers.push(peer);
debug(`[${this.rhizomeNode.config.peerId}]`, 'Added peer', addr);
return peer;
}
async subscribeToSeeds() {
const {seedPeers} = this.rhizomeNode.config;
debug(`[${this.rhizomeNode.config.peerId}]`, `SubscribeToSeeds, seedPeers: ${JSON.stringify(seedPeers)}`);
seedPeers.forEach(async (addr, idx) => {
const peer = this.addPeer(addr);
debug(`[${this.rhizomeNode.config.peerId}]`, `SEED PEERS[${idx}]=${addr.toAddrString()}, isSelf:`, peer.isSelf);
if (!peer.isSelf) {
await peer.subscribeDeltas();
}
});
}
//! TODO Expect abysmal scaling properties with this function
async askAllPeersForDeltas() {
this.peers
.forEach(async (peer, idx) => {
debug(`[${this.rhizomeNode.config.peerId}]`, `Peer ${peer.reqAddr.toAddrString()} isSelf`, peer.isSelf);
if (peer.isSelf) return;
debug(`[${this.rhizomeNode.config.peerId}]`, `Asking peer ${idx} for deltas`);
const deltas = await peer.askForDeltas();
debug(`[${this.rhizomeNode.config.peerId}]`, `Received ${deltas.length} deltas from ${peer.reqAddr.toAddrString()}`);
for (const delta of deltas) {
delta.receivedFrom = peer.reqAddr;
this.rhizomeNode.deltaStream.receiveDelta(delta);
}
this.rhizomeNode.deltaStream.ingestAll();
});
}
}