|
1 | 1 | const Hyperswarm = require("hyperswarm"); |
2 | 2 | const { signMessage } = require("../core/security"); |
3 | | -const { TOPIC, TOPIC_NAME, HEARTBEAT_INTERVAL, MAX_CONNECTIONS, CONNECTION_ROTATION_INTERVAL, ENABLE_CHAT } = require("../config/constants"); |
| 3 | +const { |
| 4 | + TOPIC, |
| 5 | + TOPIC_NAME, |
| 6 | + HEARTBEAT_INTERVAL, |
| 7 | + MAX_CONNECTIONS, |
| 8 | + CONNECTION_ROTATION_INTERVAL, |
| 9 | + ENABLE_CHAT, |
| 10 | +} = require("../config/constants"); |
4 | 11 | const { generateScreenname } = require("../utils/name-generator"); |
5 | 12 |
|
6 | 13 | class SwarmManager { |
7 | | - constructor(identity, peerManager, diagnostics, messageHandler, relayFn, broadcastFn, chatSystemFn) { |
8 | | - this.identity = identity; |
9 | | - this.peerManager = peerManager; |
10 | | - this.diagnostics = diagnostics; |
11 | | - this.messageHandler = messageHandler; |
12 | | - this.relayFn = relayFn; |
13 | | - this.broadcastFn = broadcastFn; |
14 | | - this.chatSystemFn = chatSystemFn; |
15 | | - |
16 | | - this.swarm = new Hyperswarm(); |
17 | | - this.heartbeatInterval = null; |
18 | | - this.rotationInterval = null; |
| 14 | + constructor( |
| 15 | + identity, |
| 16 | + peerManager, |
| 17 | + diagnostics, |
| 18 | + messageHandler, |
| 19 | + relayFn, |
| 20 | + broadcastFn, |
| 21 | + chatSystemFn |
| 22 | + ) { |
| 23 | + this.identity = identity; |
| 24 | + this.peerManager = peerManager; |
| 25 | + this.diagnostics = diagnostics; |
| 26 | + this.messageHandler = messageHandler; |
| 27 | + this.relayFn = relayFn; |
| 28 | + this.broadcastFn = broadcastFn; |
| 29 | + this.chatSystemFn = chatSystemFn; |
| 30 | + |
| 31 | + this.swarm = new Hyperswarm(); |
| 32 | + this.heartbeatInterval = null; |
| 33 | + this.rotationInterval = null; |
| 34 | + } |
| 35 | + |
| 36 | + async start() { |
| 37 | + this.swarm.on("connection", (socket) => this.handleConnection(socket)); |
| 38 | + |
| 39 | + const discovery = this.swarm.join(TOPIC); |
| 40 | + await discovery.flushed(); |
| 41 | + |
| 42 | + this.startHeartbeat(); |
| 43 | + this.startRotation(); |
| 44 | + } |
| 45 | + |
| 46 | + handleConnection(socket) { |
| 47 | + if (this.swarm.connections.size > MAX_CONNECTIONS) { |
| 48 | + socket.destroy(); |
| 49 | + return; |
19 | 50 | } |
20 | 51 |
|
21 | | - async start() { |
22 | | - this.swarm.on("connection", (socket) => this.handleConnection(socket)); |
23 | | - |
24 | | - const discovery = this.swarm.join(TOPIC); |
25 | | - await discovery.flushed(); |
26 | | - |
27 | | - this.startHeartbeat(); |
28 | | - this.startRotation(); |
29 | | - } |
| 52 | + socket.connectedAt = Date.now(); |
| 53 | + |
| 54 | + const sig = signMessage( |
| 55 | + `seq:${this.peerManager.getSeq()}`, |
| 56 | + this.identity.privateKey |
| 57 | + ); |
| 58 | + const hello = JSON.stringify({ |
| 59 | + type: "HEARTBEAT", |
| 60 | + id: this.identity.id, |
| 61 | + seq: this.peerManager.getSeq(), |
| 62 | + hops: 0, |
| 63 | + nonce: this.identity.nonce, |
| 64 | + sig, |
| 65 | + }); |
| 66 | + socket.write(hello); |
| 67 | + this.broadcastFn(); |
| 68 | + |
| 69 | + socket.buffer = ""; |
| 70 | + |
| 71 | + socket.on("data", (data) => { |
| 72 | + this.diagnostics.increment("bytesReceived", data.length); |
| 73 | + socket.buffer += data.toString(); |
| 74 | + |
| 75 | + const lines = socket.buffer.split("\n"); |
| 76 | + |
| 77 | + socket.buffer = lines.pop(); |
| 78 | + |
| 79 | + for (const msgStr of lines) { |
| 80 | + if (!msgStr.trim()) continue; |
| 81 | + try { |
| 82 | + const msg = JSON.parse(msgStr); |
| 83 | + this.messageHandler.handleMessage(msg, socket); |
| 84 | + } catch (e) {} |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | + socket.on("close", () => { |
| 89 | + if (socket.peerId && this.peerManager.hasPeer(socket.peerId)) { |
| 90 | + this.peerManager.removePeer(socket.peerId); |
| 91 | + } |
| 92 | + this.broadcastFn(); |
| 93 | + }); |
| 94 | + |
| 95 | + socket.on("error", () => {}); |
| 96 | + } |
| 97 | + |
| 98 | + startHeartbeat() { |
| 99 | + this.heartbeatInterval = setInterval(() => { |
| 100 | + const seq = this.peerManager.incrementSeq(); |
| 101 | + this.peerManager.addOrUpdatePeer(this.identity.id, seq, null); |
| 102 | + |
| 103 | + this.messageHandler.bloomFilter.markRelayed(this.identity.id, seq); |
| 104 | + |
| 105 | + const sig = signMessage(`seq:${seq}`, this.identity.privateKey); |
| 106 | + const heartbeat = |
| 107 | + JSON.stringify({ |
| 108 | + type: "HEARTBEAT", |
| 109 | + id: this.identity.id, |
| 110 | + seq, |
| 111 | + hops: 0, |
| 112 | + nonce: this.identity.nonce, |
| 113 | + sig, |
| 114 | + }) + "\n"; |
30 | 115 |
|
31 | | - handleConnection(socket) { |
32 | | - if (this.swarm.connections.size > MAX_CONNECTIONS) { |
33 | | - socket.destroy(); |
34 | | - return; |
35 | | - } |
| 116 | + for (const socket of this.swarm.connections) { |
| 117 | + socket.write(heartbeat); |
| 118 | + } |
36 | 119 |
|
37 | | - socket.connectedAt = Date.now(); |
38 | | - |
39 | | - const sig = signMessage(`seq:${this.peerManager.getSeq()}`, this.identity.privateKey); |
40 | | - const hello = JSON.stringify({ |
41 | | - type: "HEARTBEAT", |
42 | | - id: this.identity.id, |
43 | | - seq: this.peerManager.getSeq(), |
44 | | - hops: 0, |
45 | | - nonce: this.identity.nonce, |
46 | | - sig, |
47 | | - }); |
48 | | - socket.write(hello); |
| 120 | + const removed = this.peerManager.cleanupStalePeers(); |
| 121 | + if (removed > 0) { |
49 | 122 | this.broadcastFn(); |
50 | | - |
51 | | - socket.buffer = ""; |
52 | | - |
53 | | - socket.on("data", (data) => { |
54 | | - this.diagnostics.increment("bytesReceived", data.length); |
55 | | - socket.buffer += data.toString(); |
56 | | - |
57 | | - const lines = socket.buffer.split("\n"); |
58 | | - // The last element is either an empty string (if data ended with \n) |
59 | | - // or the incomplete part of the next message. |
60 | | - socket.buffer = lines.pop(); |
61 | | - |
62 | | - for (const msgStr of lines) { |
63 | | - if (!msgStr.trim()) continue; |
64 | | - try { |
65 | | - const msg = JSON.parse(msgStr); |
66 | | - this.messageHandler.handleMessage(msg, socket); |
67 | | - } catch (e) { |
68 | | - // Invalid JSON or partial message (shouldn't happen with buffering logic unless data is corrupted) |
69 | | - } |
70 | | - } |
71 | | - }); |
72 | | - |
73 | | - socket.on("close", () => { |
74 | | - if (socket.peerId && this.peerManager.hasPeer(socket.peerId)) { |
75 | | - this.peerManager.removePeer(socket.peerId); |
76 | | - } |
77 | | - this.broadcastFn(); |
78 | | - }); |
79 | | - |
80 | | - socket.on("error", () => { }); |
| 123 | + } |
| 124 | + }, HEARTBEAT_INTERVAL); |
| 125 | + } |
| 126 | + |
| 127 | + startRotation() { |
| 128 | + this.rotationInterval = setInterval(() => { |
| 129 | + if (this.swarm.connections.size < MAX_CONNECTIONS / 2) return; |
| 130 | + |
| 131 | + let oldest = null; |
| 132 | + for (const socket of this.swarm.connections) { |
| 133 | + if (!oldest || socket.connectedAt < oldest.connectedAt) { |
| 134 | + oldest = socket; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + if (oldest) { |
| 139 | + if (ENABLE_CHAT && this.chatSystemFn && oldest.peerId) { |
| 140 | + this.chatSystemFn({ |
| 141 | + type: "SYSTEM", |
| 142 | + content: `Connection with Node ...${oldest.peerId.slice( |
| 143 | + -8 |
| 144 | + )} severed (Rotation).`, |
| 145 | + timestamp: Date.now(), |
| 146 | + }); |
| 147 | + } |
| 148 | + oldest.destroy(); |
| 149 | + } |
| 150 | + }, CONNECTION_ROTATION_INTERVAL); |
| 151 | + } |
| 152 | + |
| 153 | + shutdown() { |
| 154 | + const sig = signMessage( |
| 155 | + `type:LEAVE:${this.identity.id}`, |
| 156 | + this.identity.privateKey |
| 157 | + ); |
| 158 | + const goodbye = |
| 159 | + JSON.stringify({ |
| 160 | + type: "LEAVE", |
| 161 | + id: this.identity.id, |
| 162 | + hops: 0, |
| 163 | + sig, |
| 164 | + }) + "\n"; |
| 165 | + |
| 166 | + this.messageHandler.bloomFilter.markRelayed(this.identity.id, "leave"); |
| 167 | + |
| 168 | + for (const socket of this.swarm.connections) { |
| 169 | + socket.write(goodbye); |
81 | 170 | } |
82 | 171 |
|
83 | | - startHeartbeat() { |
84 | | - this.heartbeatInterval = setInterval(() => { |
85 | | - const seq = this.peerManager.incrementSeq(); |
86 | | - this.peerManager.addOrUpdatePeer(this.identity.id, seq, null); |
87 | | - |
88 | | - const sig = signMessage(`seq:${seq}`, this.identity.privateKey); |
89 | | - const heartbeat = JSON.stringify({ |
90 | | - type: "HEARTBEAT", |
91 | | - id: this.identity.id, |
92 | | - seq, |
93 | | - hops: 0, |
94 | | - nonce: this.identity.nonce, |
95 | | - sig, |
96 | | - }) + "\n"; |
97 | | - |
98 | | - for (const socket of this.swarm.connections) { |
99 | | - socket.write(heartbeat); |
100 | | - } |
101 | | - |
102 | | - const removed = this.peerManager.cleanupStalePeers(); |
103 | | - if (removed > 0) { |
104 | | - this.broadcastFn(); |
105 | | - } |
106 | | - }, HEARTBEAT_INTERVAL); |
| 172 | + if (this.heartbeatInterval) { |
| 173 | + clearInterval(this.heartbeatInterval); |
107 | 174 | } |
108 | 175 |
|
109 | | - startRotation() { |
110 | | - this.rotationInterval = setInterval(() => { |
111 | | - if (this.swarm.connections.size < MAX_CONNECTIONS / 2) return; |
112 | | - |
113 | | - let oldest = null; |
114 | | - for (const socket of this.swarm.connections) { |
115 | | - if (!oldest || socket.connectedAt < oldest.connectedAt) { |
116 | | - oldest = socket; |
117 | | - } |
118 | | - } |
119 | | - |
120 | | - if (oldest) { |
121 | | - if (ENABLE_CHAT && this.chatSystemFn && oldest.peerId) { |
122 | | - this.chatSystemFn({ |
123 | | - type: "SYSTEM", |
124 | | - content: `Connection with Node ...${oldest.peerId.slice(-8)} severed (Rotation).`, |
125 | | - timestamp: Date.now() |
126 | | - }); |
127 | | - } |
128 | | - oldest.destroy(); |
129 | | - } |
130 | | - }, CONNECTION_ROTATION_INTERVAL); |
| 176 | + if (this.rotationInterval) { |
| 177 | + clearInterval(this.rotationInterval); |
131 | 178 | } |
132 | 179 |
|
133 | | - shutdown() { |
134 | | - const sig = signMessage(`type:LEAVE:${this.identity.id}`, this.identity.privateKey); |
135 | | - const goodbye = JSON.stringify({ |
136 | | - type: "LEAVE", |
137 | | - id: this.identity.id, |
138 | | - hops: 0, |
139 | | - sig, |
140 | | - }) + "\n"; |
141 | | - |
142 | | - for (const socket of this.swarm.connections) { |
143 | | - socket.write(goodbye); |
144 | | - } |
145 | | - |
146 | | - if (this.heartbeatInterval) { |
147 | | - clearInterval(this.heartbeatInterval); |
148 | | - } |
| 180 | + setTimeout(() => { |
| 181 | + process.exit(0); |
| 182 | + }, 500); |
| 183 | + } |
149 | 184 |
|
150 | | - if (this.rotationInterval) { |
151 | | - clearInterval(this.rotationInterval); |
152 | | - } |
| 185 | + getSwarm() { |
| 186 | + return this.swarm; |
| 187 | + } |
153 | 188 |
|
154 | | - setTimeout(() => { |
155 | | - process.exit(0); |
156 | | - }, 500); |
157 | | - } |
| 189 | + broadcastChat(msg) { |
| 190 | + if (!ENABLE_CHAT) return; |
158 | 191 |
|
159 | | - getSwarm() { |
160 | | - return this.swarm; |
| 192 | + if (msg.id) { |
| 193 | + this.messageHandler.bloomFilter.markRelayed(msg.id, "chat"); |
161 | 194 | } |
162 | 195 |
|
163 | | - broadcastChat(msg) { |
164 | | - if (!ENABLE_CHAT) return; |
165 | | - const msgStr = JSON.stringify(msg) + "\n"; |
166 | | - for (const socket of this.swarm.connections) { |
167 | | - socket.write(msgStr); |
168 | | - } |
| 196 | + const msgStr = JSON.stringify(msg) + "\n"; |
| 197 | + for (const socket of this.swarm.connections) { |
| 198 | + socket.write(msgStr); |
169 | 199 | } |
| 200 | + } |
170 | 201 | } |
171 | 202 |
|
172 | 203 | module.exports = { SwarmManager }; |
0 commit comments