mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-03 09:30:36 +00:00
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user