the-invisible-billion-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');//it provides information about system
4
+
5
+ //returns current machine ipv4 address if not present in network returns null
6
+ function getCurrentIP() {
7
+ const ifaces = os.networkInterfaces(); // our machine has different network interfaces like wlan,eth,virtualbox net
8
+ //this networkInterfaces function gives all netowrk interface and their addresses
9
+
10
+ //something like this
11
+
12
+ /**
13
+ * {
14
+ lo: [
15
+ { address: "127.0.0.1", family: "IPv4", internal: true }
16
+ ],
17
+ eth0: [
18
+ { address: "192.168.1.12", family: "IPv4", internal: false }
19
+ ],
20
+ wlan0: [
21
+ { address: "192.168.1.15", family: "IPv4", internal: false }
22
+ ]
23
+ }
24
+ */
25
+ for (const name of Object.keys(ifaces)) {
26
+ for (const iface of ifaces[name]) {
27
+ if (iface.family === 'IPv4' && !iface.internal) {
28
+ //as we know eth0 and wlan0 are external so we have to make this condition true
29
+ //also lo(localhost) we cant take this, because own cannot connect with own
30
+ return iface.address;
31
+ }
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Start polling for network changes every 30 seconds.
39
+ * Returns a function that stops the watcher when called.
40
+ */
41
+
42
+ // The function does not define onChangeCallback itself.
43
+ //It expects the caller to pass a function when calling startNetworkWatcher
44
+ function startNetworkWatcher(onChangeCallback) {
45
+ let currentIP = getCurrentIP();
46
+ console.log(`[network] Current IP: ${currentIP || 'none'}`);
47
+
48
+ const interval = setInterval(() => {
49
+ const newIP = getCurrentIP();
50
+ if (newIP !== currentIP) {
51
+ const previousIP = currentIP;
52
+ currentIP = newIP;
53
+ console.log(`[network] Network change detected: ${previousIP} → ${newIP}`);
54
+ try {
55
+ /**
56
+ * Function that accepts a callback
57
+ function greet(callback) {
58
+ console.log("Hello");
59
+ callback();
60
+ }
61
+
62
+ Now call it:
63
+
64
+ greet(() => {
65
+ console.log("Callback executed");
66
+ });
67
+
68
+ Output:
69
+
70
+ Hello
71
+ Callback executed
72
+ */
73
+ onChangeCallback(newIP, previousIP);
74
+ } catch (err) {
75
+ console.error('[network] onChangeCallback error:', err.message);
76
+ }
77
+ }
78
+ }, 15_000);
79
+
80
+ // Return a stop function
81
+ return () => clearInterval(interval);
82
+ }
83
+
84
+ module.exports = { getCurrentIP, startNetworkWatcher };
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+
4
+ const net = require('net');
5
+
6
+ const TCP_PORT = 41235;
7
+ const CONNECT_TIMEOUT_MS = 2000; //wait 2second for peers to respond to TCP connection attempts
8
+ const SEND_TIMEOUT_MS = 8000;
9
+
10
+ let _server = null;
11
+ let _identity = null;
12
+ let _privateKey = null;
13
+ let _onMessage = null;
14
+
15
+
16
+ function startTCP({ identity, privateKey, onMessage }) {
17
+ if (_server) return () => stopTCP();
18
+
19
+ _identity = identity;
20
+ _privateKey = privateKey;
21
+ _onMessage = onMessage;
22
+
23
+ _server = net.createServer((socket) => {
24
+ handleIncomingConnection(socket); //creates tcp server and listens for incoming connections, this function will read message frame, parse JSON, process message
25
+ });
26
+
27
+ _server.listen(TCP_PORT, '0.0.0.0', () => { //0.0.0.0 means listen on all interfaces
28
+ console.log(`[tcp] Server listening on port ${TCP_PORT}`); // means node is ready to receive TCP messages from peers. Peers will connect to this port to send messages directly to this node.
29
+ });
30
+
31
+ _server.on('error', (err) => {
32
+ if (err.code === 'EADDRINUSE') {
33
+ console.error(`[tcp] Port ${TCP_PORT} is already in use. Another daemon may be running.`);
34
+ } else {
35
+ console.error('[tcp] Server error:', err.message);
36
+ }
37
+ });
38
+
39
+ return () => stopTCP();
40
+ }
41
+
42
+ /**
43
+ * Handle an incoming TCP connection from a peer.
44
+ * Uses newline-delimited JSON framing.
45
+ */
46
+
47
+ //Shivam (192.168.1.12) ───TCP───> Amrit (192.168.1.25)
48
+ function handleIncomingConnection(socket) {
49
+ let buffer = '';
50
+ const remoteIP = socket.remoteAddress; //get the IP address of the peer that connected to us. This is used for logging and to pass to the onMessage handler so it knows where the message came from.
51
+
52
+ socket.on('data', (chunk) => { //this runs whenever we receive data from the peer over TCP. chunk = <Buffer ...>
53
+ buffer += chunk.toString('utf8'); //convert the chunk to a string and append it to the buffer. We use a buffer because TCP can split messages into multiple chunks, so we need to accumulate them until we get a full message (delimited by newline).
54
+ const lines = buffer.split('\n');
55
+ buffer = lines.pop(); // keep trailing incomplete chunk, Because the last piece might be incomplete.
56
+
57
+ for (const line of lines) {
58
+ if (!line.trim()) continue;
59
+ let frame;
60
+ try {
61
+ frame = JSON.parse(line); //converts JSON string to a JavaScript object. If the peer sent invalid JSON, this will throw an error, which we catch and respond with an ACK indicating the error.
62
+ } catch {
63
+ socket.write(JSON.stringify({ type: 'ack', ok: false, error: 'Invalid JSON' }) + '\n');//if parsing fails, we send an ACK back to the peer indicating that the frame was malformed. We then skip processing this frame and wait for the next one.
64
+ continue;
65
+ }
66
+ processFrame(frame, socket, remoteIP);
67
+ }
68
+ });
69
+
70
+ socket.on('error', () => { }); // peer disconnect — ignore
71
+ socket.on('close', () => { });
72
+
73
+ // Auto-close after 30 s of inactivity
74
+ socket.setTimeout(30_000, () => socket.destroy());
75
+ }
76
+
77
+ // Process frames received from peers. This is where we handle both "deliver" frames (messages addressed to us) and "relay" frames (messages that we need to store and forward later).
78
+ function processFrame(frame, socket, remoteIP) {
79
+ //frame: { type: 'deliver' | 'relay', message: { id, from_identity, destination, ... } } --->parsed JSON from perr,
80
+ //socket: the TCP socket connected to the peer that sent this frame. We use this to send ACKs back to the peer after processing the frame.
81
+ //remoteIP: the IP address of the peer that sent this frame. This is used for logging and is also passed to the onMessage handler so it knows where the message came from.
82
+ const { type, message } = frame;
83
+
84
+ if (!message || !message.id) {
85
+ socket.write(JSON.stringify({ type: 'ack', ok: false, error: 'Malformed frame' }) + '\n');
86
+ return;
87
+ }
88
+
89
+ if (type === 'deliver') {
90
+ // Message is addressed to us — call the delivery handler
91
+ console.log(`[tcp] Incoming message id=${message.id} from=${message.from_identity || '?'} via ${remoteIP}`);
92
+ try {
93
+ if (_onMessage) _onMessage(message, remoteIP);
94
+ socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: true }) + '\n');
95
+ } catch (err) {
96
+ console.error('[tcp] onMessage handler error:', err.message);
97
+ socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: false, error: err.message }) + '\n');
98
+ }
99
+ } else if (type === 'relay') {
100
+ // Message is for someone else — store it
101
+ console.log(`[tcp] Relay message id=${message.id} → ${message.destination} via ${remoteIP}`);
102
+ try {
103
+ if (_onMessage) _onMessage(message, remoteIP, true /* isRelay */);
104
+ socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: true }) + '\n');
105
+ } catch (err) {
106
+ socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: false, error: err.message }) + '\n');
107
+ }
108
+ } else {
109
+ socket.write(JSON.stringify({ type: 'ack', ok: false, error: `Unknown frame type: ${type}` }) + '\n');
110
+ }
111
+ }
112
+
113
+ function stopTCP() {
114
+ if (_server) {
115
+ _server.close();
116
+ _server = null;
117
+ console.log('[tcp] Stopped.');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Send a message to a peer over TCP.
123
+ * Waits for ACK before resolving.
124
+ */
125
+ function sendMessage(peerIP, frameType, message) {
126
+ //connect → send frame → wait for ACK → resolve/reject
127
+ return new Promise((resolve, reject) => {
128
+ const socket = new net.Socket();
129
+ let buffer = '';
130
+ let settled = false; //A Promise must only resolve/reject once.
131
+
132
+ /*
133
+ *8 seconds passed
134
+ no response
135
+ abort connection
136
+ */
137
+ const timeout = setTimeout(() => {
138
+ if (!settled) {
139
+ settled = true;
140
+ socket.destroy();
141
+ reject(new Error(`TCP send to ${peerIP} timed out`));
142
+ }
143
+ }, SEND_TIMEOUT_MS);
144
+
145
+ socket.setTimeout(CONNECT_TIMEOUT_MS);
146
+ socket.on('timeout', () => {
147
+ if (!settled) {
148
+ settled = true;
149
+ clearTimeout(timeout);
150
+ socket.destroy();
151
+ reject(new Error(`TCP connect to ${peerIP}:${TCP_PORT} timed out`));
152
+ }
153
+ });
154
+
155
+ socket.connect(TCP_PORT, peerIP, () => {
156
+ // Connection established — send the frame
157
+ const frame = JSON.stringify({ type: frameType, message }) + '\n';
158
+ socket.write(frame);
159
+ });
160
+
161
+ socket.on('data', (chunk) => {
162
+ buffer += chunk.toString('utf8');
163
+ const lines = buffer.split('\n');
164
+ buffer = lines.pop();
165
+
166
+ for (const line of lines) {
167
+ if (!line.trim()) continue;
168
+ try {
169
+ const ack = JSON.parse(line);
170
+ if (!settled && ack.type === 'ack') {
171
+ settled = true;
172
+ clearTimeout(timeout);
173
+ socket.destroy();
174
+ resolve({ ok: ack.ok, messageId: ack.messageId });
175
+ }
176
+ } catch {
177
+ // wait for more data
178
+ }
179
+ }
180
+ });
181
+
182
+ socket.on('error', (err) => {
183
+ if (!settled) {
184
+ settled = true;
185
+ clearTimeout(timeout);
186
+ reject(err);
187
+ }
188
+ });
189
+
190
+ socket.on('close', () => {
191
+ if (!settled) {
192
+ settled = true;
193
+ clearTimeout(timeout);
194
+ reject(new Error(`Connection to ${peerIP} closed before ACK`));
195
+ }
196
+ });
197
+ });
198
+ }
199
+
200
+ module.exports = { startTCP, stopTCP, sendMessage, TCP_PORT };
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * UDP is used for:
5
+ * 1. Peer discovery (broadcast)
6
+ * 2. Message delivery fallback when TCP is blocked by firewall
7
+ *
8
+ * Broadcast address: computed from interface IP + netmask for subnet accuracy.
9
+ * Falls back to 255.255.255.255 if no usable interface found.
10
+ */
11
+
12
+ const dgram = require('dgram');
13
+ const os = require('os');
14
+
15
+ const UDP_PORT = 41234;
16
+ const BROADCAST_INTERVAL_MS = 15_000; // brodcast every 15 seconds as heartbeat, to notify peers of our presence and IP changes
17
+ const ANNOUNCE_TYPE = 'ib-announce'; // announces our presence and public key to peers on the LAN, wether new or existing peers, so they can connect to us via TCP or UDP direct message delivery
18
+ const GOODBYE_TYPE = 'ib-goodbye'; //send when we are leaving, so peers can immediately know we are leaving and remove us from their peer list, instead of waiting for a timeout to detect our absence
19
+ const DELIVER_TYPE = 'ib-deliver'; //used for direct message delivery via UDP unicast, when TCP is blocked by firewall, so we can still deliver messages to peers on the same LAN without relying on TCP, but with best effort delivery (no retries, no ordering guarantees)
20
+ const RELAY_TYPE = 'ib-relay'; //used for epidemic routing
21
+
22
+ // Max safe UDP payload size, because headers also need space
23
+ const MAX_UDP_PAYLOAD = 60000;
24
+
25
+ let _socket = null; //handles UDP communication (broadcasts + direct messages)
26
+ let _broadcastInterval = null; //handles periodic broadcast timer
27
+ let _myIdentity = null; //our identity string e.g. shivam@a3f2, included in broadcasts so peers know who we are
28
+ let _myPublicKey = null; //our RSA public key PEM, included in broadcasts so peers can encrypt messages to us for direct UDP delivery when TCP is blocked
29
+
30
+
31
+ function computeBroadcast(ip, netmask) {
32
+ const ipParts = ip.split('.').map(Number);
33
+ const maskParts = netmask.split('.').map(Number);
34
+ const broadcast = ipParts.map((b, i) => (b | (~maskParts[i] & 0xff))); //xor of ip and subnet mask
35
+ return broadcast.join('.');
36
+ }
37
+
38
+ /**
39
+ * Get all LAN broadcast addresses for every non-loopback IPv4 interface.
40
+ * Falls back to ['255.255.255.255'] if none found.
41
+ */
42
+ function getBroadcastAddresses() {
43
+ const addrs = [];
44
+ const ifaces = os.networkInterfaces();
45
+ for (const ifaceList of Object.values(ifaces)) {
46
+ for (const iface of ifaceList) {
47
+ if (iface.family === 'IPv4' && !iface.internal && iface.netmask) {
48
+ addrs.push(computeBroadcast(iface.address, iface.netmask));
49
+ }
50
+ }
51
+ }
52
+ return addrs.length > 0 ? addrs : ['255.255.255.255'];
53
+ }
54
+
55
+ /**
56
+ * Send a UDP broadcast announce on all LAN interfaces.
57
+ * Goal: Tell every device on the LAN:
58
+ * I am an IB node. Here is my identity and public key.
59
+ */
60
+ function sendBroadcast() {
61
+ if (!_socket || !_myIdentity || !_myPublicKey) return; //all are compulsory for broadcast
62
+
63
+ //first this creates JSON, then converts it into binary data UDP sends bytes, not objects
64
+ const packet = Buffer.from(JSON.stringify({
65
+ type: ANNOUNCE_TYPE,
66
+ identity: _myIdentity,
67
+ publicKey: _myPublicKey,
68
+ }));
69
+
70
+ for (const addr of getBroadcastAddresses()) {
71
+ //packet is the data we want to send,
72
+ // 0 is the offset in the buffer,
73
+ // packet.length is the number of bytes to send,
74
+ // UDP_PORT is the destination port,
75
+ // addr is the destination IP address,the callback handles any errors that occur during sending
76
+ _socket.send(packet, 0, packet.length, UDP_PORT, addr, (err) => {
77
+ if (err) console.error(`[udp] Broadcast to ${addr} failed:`, err.message);
78
+ });
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Send a goodbye broadcast so peers immediately know we are leaving.
84
+ */
85
+ function sendGoodbye() {
86
+ if (!_socket || !_myIdentity) return;
87
+
88
+ const packet = Buffer.from(JSON.stringify({
89
+ type: GOODBYE_TYPE,
90
+ identity: _myIdentity,
91
+ }));
92
+
93
+ for (const addr of getBroadcastAddresses()) {
94
+ _socket.send(packet, 0, packet.length, UDP_PORT, addr, (err) => {
95
+ if (err) console.error(`[udp] Goodbye broadcast to ${addr} failed:`, err.message);
96
+ });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Send a message directly to a peer via UDP unicast.
102
+ * This is the fallback when TCP is blocked by firewall.
103
+ *
104
+ * @param {string} peerIP - Target peer's IP address
105
+ * @param {'deliver'|'relay'} frameType - 'deliver' for direct, 'relay' for epidemic
106
+ * @param {object} message - Full message object
107
+ * @returns {Promise<{ok: boolean}>}
108
+ */
109
+ function sendDirectMessage(peerIP, frameType, message) {
110
+ return new Promise((resolve, reject) => {
111
+ if (!_socket) {
112
+ reject(new Error('UDP socket not initialized'));
113
+ return;
114
+ }
115
+
116
+ const udpType = frameType === 'deliver' ? DELIVER_TYPE : RELAY_TYPE;
117
+ const data = JSON.stringify({ type: udpType, message });
118
+
119
+ if (data.length > MAX_UDP_PAYLOAD) {
120
+ reject(new Error(`Message too large for UDP delivery (${data.length} bytes > ${MAX_UDP_PAYLOAD})`));
121
+ return;
122
+ }
123
+
124
+ const packet = Buffer.from(data);
125
+ _socket.send(packet, 0, packet.length, UDP_PORT, peerIP, (err) => {
126
+ if (err) {
127
+ console.error(`[udp] Direct send to ${peerIP} failed:`, err.message);
128
+ reject(err);
129
+ } else {
130
+ console.log(`[udp] Direct ${frameType} sent to ${peerIP} (${packet.length} bytes)`);
131
+ resolve({ ok: true });
132
+ }
133
+ });
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Basically it does 3 main jobs:
139
+ *
140
+ Start a UDP socket and listen for packets
141
+
142
+ Discover peers using broadcast
143
+
144
+ Receive messages from other peers
145
+ *
146
+ * @param {object} opts
147
+ * @param {string} opts.identity - Our identity string e.g. shivam@a3f2
148
+ * @param {string} opts.publicKey - Our RSA public key PEM
149
+ * @param {function} opts.onPeer - Called with (identity, publicKeyPem, remoteIP) on discovery
150
+ * @param {function} [opts.onPeerGoodbye] - Called with (identity) when a peer sends goodbye
151
+ * @param {function} [opts.onMessage] - Called with (message, remoteIP, isRelay) for UDP-delivered messages
152
+ * @returns {function} stop — call to shut down UDP
153
+ */
154
+ function startUDP({ identity, publicKey, onPeer, onPeerGoodbye, onMessage }) {
155
+ if (_socket) return () => stopUDP(); //if connection already exists, return the stop function without creating a new socket
156
+
157
+ _myIdentity = identity;
158
+ _myPublicKey = publicKey;
159
+
160
+ //opens a UDP socket for IPv4 with address reuse enabled (allows multiple processes to bind to the same port, useful for multiple IB instances on the same LAN)
161
+ _socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
162
+
163
+ _socket.on('error', (err) => {
164
+ console.error('[udp] Socket error:', err.message);
165
+ });
166
+
167
+ //this runs whenever a UDP packet is recieved
168
+ _socket.on('message', (msg, rinfo) => {
169
+ let packet;
170
+ try {
171
+ packet = JSON.parse(msg.toString('utf8'));
172
+ } catch {
173
+ return; // ignore malformed packets
174
+ }
175
+
176
+ //goodbye
177
+ if (packet.type === GOODBYE_TYPE && packet.identity) {
178
+ if (packet.identity === _myIdentity) return; // ignore our own
179
+ console.log(`[udp] Peer goodbye: ${packet.identity}`);
180
+ try {
181
+ if (onPeerGoodbye) onPeerGoodbye(packet.identity);
182
+ } catch (err) {
183
+ console.error('[udp] onPeerGoodbye handler error:', err.message);
184
+ }
185
+ return;
186
+ }
187
+
188
+ //direct message delivery via UDP
189
+ if (packet.type === DELIVER_TYPE && packet.message) {
190
+ console.log(`[udp] Received direct message id=${packet.message.id} from ${rinfo.address}`);
191
+ try {
192
+ if (onMessage) onMessage(packet.message, rinfo.address, false); //value false for direct delivery, so onMessage can distinguish between direct and relay messages if needed
193
+ } catch (err) {
194
+ console.error('[udp] onMessage (deliver) error:', err.message);
195
+ }
196
+ return;
197
+ }
198
+
199
+ // relay message via UDP for epidemic routing (used when destination peer is not currently on the network, so we relay to other peers in the hope that one of them will eventually meet the destination peer and deliver it)
200
+ if (packet.type === RELAY_TYPE && packet.message) {
201
+ console.log(`[udp] Received relay message id=${packet.message.id} from ${rinfo.address}`);
202
+ try {
203
+ if (onMessage) onMessage(packet.message, rinfo.address, true);
204
+ } catch (err) {
205
+ console.error('[udp] onMessage (relay) error:', err.message);
206
+ }
207
+ return;
208
+ }
209
+
210
+ // Handle announce
211
+ if (packet.type !== ANNOUNCE_TYPE) return;
212
+ if (!packet.identity || !packet.publicKey) return;
213
+
214
+ // Ignore our own broadcasts
215
+ if (packet.identity === _myIdentity) return;
216
+
217
+ console.log(`[udp] Peer discovered: ${packet.identity} at ${rinfo.address}`);
218
+ try {
219
+ onPeer(packet.identity, packet.publicKey, rinfo.address);
220
+ } catch (err) {
221
+ console.error('[udp] onPeer handler error:', err.message);
222
+ }
223
+ });
224
+
225
+ _socket.bind(UDP_PORT, () => {
226
+ _socket.setBroadcast(true);
227
+ console.log(`[udp] Listening on port ${UDP_PORT}`);
228
+
229
+
230
+ // Broadcast immediately so peers on the LAN discover us fast
231
+ sendBroadcast();
232
+
233
+ // Re-broadcast every 15 s, setInterval will automatically call sendBroadcast every 15 seconds
234
+ _broadcastInterval = setInterval(sendBroadcast, BROADCAST_INTERVAL_MS);
235
+ /**
236
+ * if nothing else is running
237
+ allow program to exit
238
+ */
239
+ _broadcastInterval.unref();
240
+
241
+ });
242
+
243
+ _socket.on('error', (err) => {
244
+ if (err.code === 'EADDRINUSE') {
245
+ console.error(`[udp] Port ${UDP_PORT} already in use.`);
246
+ }
247
+ });
248
+
249
+ return () => stopUDP();
250
+ }
251
+
252
+ function stopUDP() {
253
+ if (_broadcastInterval) {
254
+ clearInterval(_broadcastInterval);
255
+ _broadcastInterval = null;
256
+ }
257
+ if (_socket) {
258
+ _socket.close();
259
+ _socket = null;
260
+ }
261
+ console.log('[udp] Stopped.');
262
+ }
263
+
264
+ // Broadcast a goodbye (exit ke waqt call karna).
265
+ function broadcastGoodbye() {
266
+ sendGoodbye();
267
+ }
268
+
269
+ /**
270
+ * Broadcast our identity immediately on the current network.
271
+ * Called when a network change is detected.
272
+ */
273
+ function broadcastNow() {
274
+ sendBroadcast();
275
+ }
276
+
277
+ /**
278
+ * Get the current UDP socket
279
+ */
280
+ function getSocket() { return _socket; }
281
+
282
+ module.exports = { startUDP, stopUDP, broadcastNow, broadcastGoodbye, sendDirectMessage, getSocket, UDP_PORT };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { openDb } = require('./index');
4
+
5
+ /**
6
+ * Delete all messages that have expired (ttl < now) and are not delivered.
7
+ * Delivered messages are kept as a record so senders can still check
8
+ * ib status <id>. They are cleaned up after 30 days.
9
+ * Also clean up peers that haven't been seen in 1 hour, to prevent stale peer entries.
10
+ */
11
+ function cleanExpired() {
12
+ const db = openDb();
13
+ const now = Date.now();
14
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
15
+
16
+ // Delete expired undelivered/intransit messages
17
+ const r1 = db.prepare(`
18
+ DELETE FROM messages
19
+ WHERE ttl < ? AND status != 'delivered'
20
+ `).run(now);
21
+
22
+ //clean up old delivered messages
23
+ const r2 = db.prepare(`
24
+ DELETE FROM messages
25
+ WHERE status = 'delivered' AND created_at < ?
26
+ `).run(thirtyDaysAgo);
27
+
28
+ // Clean up peers not seen in 1 hour (with 15s heartbeats, a live peer would have re-announced many times)
29
+ const oneHourAgo = now - 60 * 60 * 1000;
30
+ db.prepare(`DELETE FROM peers WHERE last_seen < ?`).run(oneHourAgo);
31
+
32
+ const deleted = r1.changes + r2.changes;
33
+ if (deleted > 0) {
34
+ console.log(`[cleanup] Removed ${deleted} expired message(s) from storage.`);
35
+ }
36
+ return deleted;
37
+ }
38
+
39
+ module.exports = { cleanExpired };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const path = require('path'); //help safely create path files
4
+ const fs = require('fs'); //help read and write files, also check if file exists
5
+ const Database = require('better-sqlite3'); //importing SQLite from better-sqlite3
6
+ const { IB_DIR } = require('../identity/index'); //main folder file path
7
+
8
+ const DB_PATH = path.join(IB_DIR, 'ib.db');
9
+
10
+ let _db = null; //stores database connection, so we dont need to open the database multiple times,
11
+
12
+ //open or create the database and initialize the schema,uses cache instance also
13
+ function openDb() {
14
+ if (_db) return _db;
15
+
16
+ if (!fs.existsSync(IB_DIR)) {
17
+ fs.mkdirSync(IB_DIR, { recursive: true });
18
+ }
19
+
20
+ _db = new Database(DB_PATH); //if file exists, open ib.db
21
+ //else create ib.db
22
+
23
+ // Enable WAL mode for better concurrent read performance
24
+ _db.pragma('journal_mode = WAL');
25
+ _db.pragma('foreign_keys = ON');
26
+
27
+ // Main messages table
28
+ _db.exec(`
29
+ CREATE TABLE IF NOT EXISTS messages (
30
+ id TEXT PRIMARY KEY,
31
+ from_identity TEXT NOT NULL,
32
+ destination TEXT NOT NULL,
33
+ payload TEXT NOT NULL, -- JSON-encoded encrypted blob
34
+ status TEXT NOT NULL DEFAULT 'undelivered',
35
+ -- undelivered | intransit | delivered
36
+ hop_count INTEGER NOT NULL DEFAULT 0,
37
+ ttl INTEGER NOT NULL, -- Unix epoch ms — message dies after this
38
+ created_at INTEGER NOT NULL
39
+ );
40
+ `);
41
+
42
+ // Known peer public keys
43
+ _db.exec(`
44
+ CREATE TABLE IF NOT EXISTS peers (
45
+ identity TEXT PRIMARY KEY, -- e.g. raj@a3f2
46
+ public_key TEXT NOT NULL, -- RSA PEM
47
+ ip TEXT, -- last known IP address
48
+ last_seen INTEGER NOT NULL -- Unix epoch ms
49
+ );
50
+ `);
51
+
52
+ try {
53
+ _db.exec(`ALTER TABLE peers ADD COLUMN ip TEXT`);
54
+ } catch (_) {
55
+ // column already exists, leave it
56
+ //for safety purpose
57
+
58
+ return _db;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Close the database connection.
64
+ * Should be called on daemon shutdown.
65
+ */
66
+ function closeDb() {
67
+ if (_db) {
68
+ _db.close();
69
+ _db = null;
70
+ }
71
+ }
72
+
73
+
74
+ module.exports = { openDb, closeDb, DB_PATH };