@kronos-integration/service-swarm
Version:
manage a set of remote services
198 lines (167 loc) • 5 kB
JavaScript
import { pipeline } from "node:stream";
import Hyperswarm from "hyperswarm";
import { Decode, Encode } from "length-prefix-framed-stream";
import {
getAttributesJSON,
prepareAttributesDefinitions,
private_key_attribute,
boolean_attribute_false,
integer_attribute,
object_attribute
} from "pacc";
import { Service } from "@kronos-integration/service";
import { Topic } from "./topic.mjs";
import { TopicEndpoint } from "./topic-endpoint.mjs";
import { PeersEndpoint } from "./peers-endpoint.mjs";
/**
* Swarm detecting sync service.
*/
export class ServiceSwarm extends Service {
/**
* @return {string} 'swarm'
*/
static get name() {
return "swarm";
}
static attributes = prepareAttributesDefinitions(
{
server: {
...boolean_attribute_false,
needsRestart: true
},
client: {
...boolean_attribute_false,
needsRestart: true
},
dht: {
...object_attribute,
description: "well known dht addresses",
needsRestart: true,
connectionOption: true
},
maxPeers: {
...integer_attribute,
description: "total amount of peers that this peer will connect to",
default: 10,
needsRestart: true,
connectionOption: true
},
seed: {
...private_key_attribute,
description: "key pair seed",
needsRestart: true
}
},
Service.attributes
);
get topics() {
if (!this._topics) {
this._topics = new Map();
}
return this._topics;
}
get topicsByName() {
if (!this._topicsByName) {
this._topicsByName = new Map();
}
return this._topicsByName;
}
createTopic(name, options) {
let topic = this.topicsByName.get(name);
if (!topic) {
topic = new Topic(this, name, options);
this.topicsByName.set(name, topic);
this.topics.set(topic.key, topic);
}
return topic;
}
/**
* On demand create topic endpoints.
* @param {string} name
* @param {Object|string} definition
* @param {InitializationContext} ic
* @return {Object} TopicEndpoint if name starts with 'topic.'
*/
endpointFactoryFromConfig(name, definition, ic) {
if (TopicEndpoint.isTopicName(name)) {
return TopicEndpoint;
}
if (PeersEndpoint.isPeersName(name)) {
return PeersEndpoint;
}
return super.endpointFactoryFromConfig(name, definition, ic);
}
async _start() {
const swarm = (this.swarm = new Hyperswarm({
...getAttributesJSON(
this,
this.attributes,
(name, attribute) => attribute.connectionOption
),
...(await this.getCredentials())
}));
swarm.on("update", () => {
console.log("Hyperswarm update", swarm);
});
swarm.on("peer", peer => {
const topic = this.topics.get(peer.topic);
topic.addPeer(peer);
});
swarm.on("connection", async (socket, peerInfo) => {
console.log(socket, peerInfo);
/*
this.trace(
`connection: peer=${info.peer ? "true" : "false"} client=${
info.client ? "true" : "false"
} ${JSON.stringify(socket.address())} ${socket.remoteAddress}`
);*/
if (peerInfo) {
const topic = this.topics.get(peerInfo.peer.topic);
this.trace(`Connection for topic ${topic.name}`);
topic.addSocket(socket);
socket.on("drain", () => this.trace("socket drain"));
socket.on("timeout", () => this.trace("socket timeout"));
const encode = new Encode();
pipeline(encode, socket, e => this.trace(`Encoding pipeline end ${e}`));
this.trace(`Encoding pipeline established ${topic.name}`);
}
const decode = new Decode({ objectMode: true, encoding: "utf8" });
pipeline(socket, decode, e => this.trace(`Decoding pipeline end ${e}`));
this.trace(`Decoding pipeline established`);
decode.on("data", data => this.trace(`got ${data}`));
});
swarm.on("disconnection", (socket, info) => {
if (info.peer) {
this.trace(`disconnection: ${JSON.stringify(info.peer)}`);
const topic = this.topics.get(info.peer.topic);
if (topic) {
topic.removePeer(info.peer);
} else {
this.trace(`disconnection: unknown topic`);
}
}
});
await Promise.all(
[...this.topics.values()].map(topic => {
this.trace(`join topic ${topic.name} ${JSON.stringify(topic.options)}`);
const discovery = this.swarm.join(topic.key, topic.options);
return discovery.flushed();
})
);
}
async _stop() {
const swarm = this.swarm;
delete this.swarm;
this.swam = undefined;
return Promise.all(
[...this.topics.values()].map(topic => {
this.trace(`leave topic ${topic.name}`);
return swarm.leave(topic.key);
})
);
await swarm.suspend({
log: (...args) => this.info(...args)
});
}
}
export default ServiceSwarm;