Merge pull request #55 from lklynet/pr-52

Pr 52
This commit is contained in:
LKLY
2026-01-10 11:54:14 -05:00
committed by GitHub
4 changed files with 191 additions and 141 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hypermind",
"version": "0.9.0",
"version": "0.9.1",
"description": "A decentralized P2P counter of active deployments",
"main": "server.js",
"scripts": {
+13
View File
@@ -447,10 +447,23 @@ const getColorFromId = (id) => {
return "#" + "00000".substring(0, 6 - c.length) + c;
};
const seenMessageIds = new Set();
const messageIdHistory = [];
const appendMessage = (msg) => {
const div = document.createElement("div");
if (msg.type === "CHAT") {
if (msg.id) {
if (seenMessageIds.has(msg.id)) return;
seenMessageIds.add(msg.id);
messageIdHistory.push(msg.id);
if (messageIdHistory.length > 100) {
const oldest = messageIdHistory.shift();
seenMessageIds.delete(oldest);
}
}
// Block check
if (blockedUsers.has(msg.sender)) return;
+171 -140
View File
@@ -1,172 +1,203 @@
const Hyperswarm = require("hyperswarm");
const { signMessage } = require("../core/security");
const { TOPIC, TOPIC_NAME, HEARTBEAT_INTERVAL, MAX_CONNECTIONS, CONNECTION_ROTATION_INTERVAL, ENABLE_CHAT } = require("../config/constants");
const {
TOPIC,
TOPIC_NAME,
HEARTBEAT_INTERVAL,
MAX_CONNECTIONS,
CONNECTION_ROTATION_INTERVAL,
ENABLE_CHAT,
} = require("../config/constants");
const { generateScreenname } = require("../utils/name-generator");
class SwarmManager {
constructor(identity, peerManager, diagnostics, messageHandler, relayFn, broadcastFn, chatSystemFn) {
this.identity = identity;
this.peerManager = peerManager;
this.diagnostics = diagnostics;
this.messageHandler = messageHandler;
this.relayFn = relayFn;
this.broadcastFn = broadcastFn;
this.chatSystemFn = chatSystemFn;
constructor(
identity,
peerManager,
diagnostics,
messageHandler,
relayFn,
broadcastFn,
chatSystemFn
) {
this.identity = identity;
this.peerManager = peerManager;
this.diagnostics = diagnostics;
this.messageHandler = messageHandler;
this.relayFn = relayFn;
this.broadcastFn = broadcastFn;
this.chatSystemFn = chatSystemFn;
this.swarm = new Hyperswarm();
this.heartbeatInterval = null;
this.rotationInterval = null;
this.swarm = new Hyperswarm();
this.heartbeatInterval = null;
this.rotationInterval = null;
}
async start() {
this.swarm.on("connection", (socket) => this.handleConnection(socket));
const discovery = this.swarm.join(TOPIC);
await discovery.flushed();
this.startHeartbeat();
this.startRotation();
}
handleConnection(socket) {
if (this.swarm.connections.size > MAX_CONNECTIONS) {
socket.destroy();
return;
}
async start() {
this.swarm.on("connection", (socket) => this.handleConnection(socket));
socket.connectedAt = Date.now();
const discovery = this.swarm.join(TOPIC);
await discovery.flushed();
const sig = signMessage(
`seq:${this.peerManager.getSeq()}`,
this.identity.privateKey
);
const hello = JSON.stringify({
type: "HEARTBEAT",
id: this.identity.id,
seq: this.peerManager.getSeq(),
hops: 0,
nonce: this.identity.nonce,
sig,
});
socket.write(hello);
this.broadcastFn();
this.startHeartbeat();
this.startRotation();
}
socket.buffer = "";
handleConnection(socket) {
if (this.swarm.connections.size > MAX_CONNECTIONS) {
socket.destroy();
return;
}
socket.on("data", (data) => {
this.diagnostics.increment("bytesReceived", data.length);
socket.buffer += data.toString();
socket.connectedAt = Date.now();
const lines = socket.buffer.split("\n");
const sig = signMessage(`seq:${this.peerManager.getSeq()}`, this.identity.privateKey);
const hello = JSON.stringify({
type: "HEARTBEAT",
id: this.identity.id,
seq: this.peerManager.getSeq(),
hops: 0,
nonce: this.identity.nonce,
sig,
});
socket.write(hello);
this.broadcastFn();
socket.buffer = lines.pop();
socket.buffer = "";
for (const msgStr of lines) {
if (!msgStr.trim()) continue;
try {
const msg = JSON.parse(msgStr);
this.messageHandler.handleMessage(msg, socket);
} catch (e) {}
}
});
socket.on("data", (data) => {
this.diagnostics.increment("bytesReceived", data.length);
socket.buffer += data.toString();
socket.on("close", () => {
if (socket.peerId && this.peerManager.hasPeer(socket.peerId)) {
this.peerManager.removePeer(socket.peerId);
}
this.broadcastFn();
});
const lines = socket.buffer.split("\n");
// The last element is either an empty string (if data ended with \n)
// or the incomplete part of the next message.
socket.buffer = lines.pop();
socket.on("error", () => {});
}
for (const msgStr of lines) {
if (!msgStr.trim()) continue;
try {
const msg = JSON.parse(msgStr);
this.messageHandler.handleMessage(msg, socket);
} catch (e) {
// Invalid JSON or partial message (shouldn't happen with buffering logic unless data is corrupted)
}
}
});
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
const seq = this.peerManager.incrementSeq();
this.peerManager.addOrUpdatePeer(this.identity.id, seq, null);
socket.on("close", () => {
if (socket.peerId && this.peerManager.hasPeer(socket.peerId)) {
this.peerManager.removePeer(socket.peerId);
}
this.broadcastFn();
});
this.messageHandler.bloomFilter.markRelayed(this.identity.id, seq);
socket.on("error", () => { });
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
const seq = this.peerManager.incrementSeq();
this.peerManager.addOrUpdatePeer(this.identity.id, seq, null);
const sig = signMessage(`seq:${seq}`, this.identity.privateKey);
const heartbeat = JSON.stringify({
type: "HEARTBEAT",
id: this.identity.id,
seq,
hops: 0,
nonce: this.identity.nonce,
sig,
}) + "\n";
for (const socket of this.swarm.connections) {
socket.write(heartbeat);
}
const removed = this.peerManager.cleanupStalePeers();
if (removed > 0) {
this.broadcastFn();
}
}, HEARTBEAT_INTERVAL);
}
startRotation() {
this.rotationInterval = setInterval(() => {
if (this.swarm.connections.size < MAX_CONNECTIONS / 2) return;
let oldest = null;
for (const socket of this.swarm.connections) {
if (!oldest || socket.connectedAt < oldest.connectedAt) {
oldest = socket;
}
}
if (oldest) {
if (ENABLE_CHAT && this.chatSystemFn && oldest.peerId) {
this.chatSystemFn({
type: "SYSTEM",
content: `Connection with Node ...${oldest.peerId.slice(-8)} severed (Rotation).`,
timestamp: Date.now()
});
}
oldest.destroy();
}
}, CONNECTION_ROTATION_INTERVAL);
}
shutdown() {
const sig = signMessage(`type:LEAVE:${this.identity.id}`, this.identity.privateKey);
const goodbye = JSON.stringify({
type: "LEAVE",
id: this.identity.id,
hops: 0,
sig,
const sig = signMessage(`seq:${seq}`, this.identity.privateKey);
const heartbeat =
JSON.stringify({
type: "HEARTBEAT",
id: this.identity.id,
seq,
hops: 0,
nonce: this.identity.nonce,
sig,
}) + "\n";
for (const socket of this.swarm.connections) {
socket.write(goodbye);
}
for (const socket of this.swarm.connections) {
socket.write(heartbeat);
}
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
const removed = this.peerManager.cleanupStalePeers();
if (removed > 0) {
this.broadcastFn();
}
}, HEARTBEAT_INTERVAL);
}
if (this.rotationInterval) {
clearInterval(this.rotationInterval);
}
startRotation() {
this.rotationInterval = setInterval(() => {
if (this.swarm.connections.size < MAX_CONNECTIONS / 2) return;
setTimeout(() => {
process.exit(0);
}, 500);
let oldest = null;
for (const socket of this.swarm.connections) {
if (!oldest || socket.connectedAt < oldest.connectedAt) {
oldest = socket;
}
}
if (oldest) {
if (ENABLE_CHAT && this.chatSystemFn && oldest.peerId) {
this.chatSystemFn({
type: "SYSTEM",
content: `Connection with Node ...${oldest.peerId.slice(
-8
)} severed (Rotation).`,
timestamp: Date.now(),
});
}
oldest.destroy();
}
}, CONNECTION_ROTATION_INTERVAL);
}
shutdown() {
const sig = signMessage(
`type:LEAVE:${this.identity.id}`,
this.identity.privateKey
);
const goodbye =
JSON.stringify({
type: "LEAVE",
id: this.identity.id,
hops: 0,
sig,
}) + "\n";
this.messageHandler.bloomFilter.markRelayed(this.identity.id, "leave");
for (const socket of this.swarm.connections) {
socket.write(goodbye);
}
getSwarm() {
return this.swarm;
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
broadcastChat(msg) {
if (!ENABLE_CHAT) return;
const msgStr = JSON.stringify(msg) + "\n";
for (const socket of this.swarm.connections) {
socket.write(msgStr);
}
if (this.rotationInterval) {
clearInterval(this.rotationInterval);
}
setTimeout(() => {
process.exit(0);
}, 500);
}
getSwarm() {
return this.swarm;
}
broadcastChat(msg) {
if (!ENABLE_CHAT) return;
if (msg.id) {
this.messageHandler.bloomFilter.markRelayed(msg.id, "chat");
}
const msgStr = JSON.stringify(msg) + "\n";
for (const socket of this.swarm.connections) {
socket.write(msgStr);
}
}
}
module.exports = { SwarmManager };
+6
View File
@@ -11,6 +11,12 @@ class PeerManager {
addOrUpdatePeer(id, seq, ip = null) {
const stored = this.seenPeers.get(id);
// If we have a stored peer, only update if the new sequence is higher
if (stored && seq <= stored.seq) {
return false;
}
const wasNew = !stored;
// Track in HyperLogLog for total unique estimation